<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>uhojin</title><description>blog</description><link>https://blog.ghwls.com/</link><language>en</language><item><title>Lofi Air Traffic Control Mixer</title><link>https://blog.ghwls.com/posts/lofi_atc/post/</link><guid isPermaLink="true">https://blog.ghwls.com/posts/lofi_atc/post/</guid><description>Because I can... for educational purposes</description><pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Why?&lt;/h1&gt;
&lt;p&gt;Literally. Why would anyone make this? or why would anyone want something like this? for educational purposes, obviously...&lt;/p&gt;
&lt;p&gt;I made &lt;a href=&quot;https://lofi-atc.ghwls.com&quot;&gt;lofi-atc.ghwls.com&lt;/a&gt; because the original &lt;a href=&quot;https://lofiatc.com&quot;&gt;lofiatc.com&lt;/a&gt; is no longer functioning. Plus the alternatives didn&apos;t give me that &quot;comfy&quot; feeling.
&lt;img src=&quot;main.png&quot; alt=&quot;Mixer Preview Image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Naturally, I searched around for an alternative, but I figured I could just make one for a quick side project. For that reason, rust was chosen and svelte for front. I don&apos;t have a need to have different views or pages, which made svelte perfect candidate, and personally, writing react components gives me that mobile app development feeling, which I&apos;m not particularly a big fan of.&lt;/p&gt;
&lt;h2&gt;Preview&lt;/h2&gt;
&lt;p&gt;Because pictures are better than words, and I really don&apos;t think anyone will be reading nor clicking on my link...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;music-selection.png&quot; alt=&quot;Music selection menu&quot; /&gt;
The selection menu is pretty self explanatory, only design challenge I had for this was deciding what to do for stations with multiple broadcasts such as YYZ, and making them paged like below seemed like the most obvious solution.
&lt;img src=&quot;station-selection.png&quot; alt=&quot;Station selection menu&quot; /&gt;
&lt;strong&gt;Remaining challenges&lt;/strong&gt; for Station Picker:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Show station status without spamming their servers&lt;/li&gt;
&lt;li&gt;Automate top stations, also without spamming&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;but the current version is good enough so until I hear user complaints (which I wont), it&apos;s going to stay as is.&lt;/p&gt;
&lt;h2&gt;Oversimplified App Architecture&lt;/h2&gt;
&lt;p&gt;I didn&apos;t want to use node or some other javascript based backend because I wanted something light, since I knew I was going to be self-hosting on my server.&lt;/p&gt;
&lt;p&gt;So the options were down to &lt;strong&gt;Rust vs. Go&lt;/strong&gt;, and after thinking about potentially adding zig to the line up, I decided that I wouldn&apos;t really benefit from this decision making process and decided to go with Rust. Then the reality set in, and I decided to vibe code, which lead me down to only caring about performance of the languages, since the code would be mediocre at best, and major benefit of Go being rapid development really fell off once I factored in &quot;&lt;strong&gt;vibe-coding&lt;/strong&gt;&quot;.&lt;/p&gt;
&lt;p&gt;Since I wanted to self-host, and went the simplest route of serving the pre-built static site from the rust backend. I exposed that server port via cloudflare tunnel and made the front end accessible from the internet.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;arch.png&quot; alt=&quot;Image of over simplified architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The rust server starts FFmpeg process that plays audio to the audio engine. The youtube connection happens via yt-dlp command, and &lt;a href=&quot;https://liveatc.net&quot;&gt;LiveATC.net&lt;/a&gt; provides the Air Traffic Control audio stream via their website.&lt;/p&gt;
&lt;p&gt;Proxies are created by a request sent to the backend&apos;s endpoint, which returns a url from the backend, which is the audio source, which can be played from the browser.&lt;/p&gt;
&lt;h3&gt;Quick Svelte Glazing&lt;/h3&gt;
&lt;p&gt;I love using &lt;a href=&quot;https://svelte.dev&quot;&gt;Svelte&lt;/a&gt; whenever I can, which is rarely unless I&apos;m working on a hobby project, but I&apos;ve yet to try SvelteKit, so I can&apos;t say much about that side of Svelte. Nonetheless the experience feels much direct compared to classic seperate file based web development experience. I like that I can edit the style and add the functionality within the same file, less context switching in my head, maybe that will matter less with the vibe-coding era, but we&apos;ll see where it leads. Regardless for simple pages I really enjoy the experience over existing frontend solutions out there.&lt;/p&gt;
&lt;p&gt;The tiny build size also is a bonus, it also allows one to host a nice web page off of a microcontroller dev board, such as an ESP32, which I&apos;ll be writing about another time.
&lt;img src=&quot;svelte-build-size.png&quot; alt=&quot;Image of build size&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Future plans&lt;/h2&gt;
&lt;p&gt;I do want to improve the music source/providers to include custom playlists from YouTube or even Spotify.&lt;/p&gt;
&lt;p&gt;Also need to polish the top stations list, which is another challenge since the LiveATC website doesn&apos;t really provide any APIs, and I&apos;m pretty sure scraping is not allowed, but when did that stop anyone on the internet from scraping...&lt;/p&gt;
&lt;p&gt;Oh and I should employ some DevOps practices as part of self-deployment learning experience so I don&apos;t have to manually build and move the files around every time, and think of better ways to manage the processes to minimize downtime, even though it&apos;s over complicating just for a hobby project that no one is using.&lt;/p&gt;
&lt;h2&gt;Finishing thoughts&lt;/h2&gt;
&lt;p&gt;I like how the aesthetic came out, I think the use of array font was a nice touch, but I might look into swapping it out for the new &lt;a href=&quot;https://vercel.com/font?type=pixel&quot;&gt;pixelated font from Vercel&lt;/a&gt;. The site is at a &quot;good-enough&quot; point for me so I&apos;m not really sure when I&apos;ll feel motivated to actually work on it more. Until then, it&apos;s most likely just going to be some maintenance.&lt;/p&gt;
</content:encoded></item><item><title>Tikrolling</title><link>https://blog.ghwls.com/posts/tik_rolling/post/</link><guid isPermaLink="true">https://blog.ghwls.com/posts/tik_rolling/post/</guid><description>Never giving it up.</description><pubDate>Thu, 08 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note[Date Advisory]
This was back in the summer of 2018
:::&lt;/p&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;I used to work for a local ISP, during my time there I was fortunate enough to gain some experience with hardware, while working a helpdesk job.&lt;/p&gt;
&lt;p&gt;They were growing and also provided wireless services across the county, the paid hotspot service was built using MikroTik hardware.&lt;/p&gt;
&lt;p&gt;It being a rural WISP, the single programmer was a wizard named Geoff who migrated the original customer management software from 1994 into a php backend. Everyone had their own thing, but Geoff was the wizard in programming.&lt;/p&gt;
&lt;p&gt;One day at work, I found a box full of &quot;dead&quot; equipments, they appeared to be no longer booting properly. I asked another tech if these were okay to use, and I got told they were dead so I just decided to perform a reset using their &lt;a href=&quot;https://help.mikrotik.com/docs/spaces/ROS/pages/24805390/Netinstall&quot;&gt;Netinstall&lt;/a&gt; tool which reinstalls RouterOS.&lt;/p&gt;
&lt;p&gt;To my surprise they started working again, and I was able to WinBox into them. So I asked Geoff more about our MikroTik usage.&lt;/p&gt;
&lt;h2&gt;The Bait&lt;/h2&gt;
&lt;p&gt;Back in the box, I found a RouterBoard they were using for outdoors and a hAP which was a rental unit for hotspot customers. The hAP&apos;s been through Geoff&apos;s script to have been configured and registered to our network. So I obviously had to show him that the broken batch wasn&apos;t actually broken, and decided to give it to him so he could program them again.&lt;/p&gt;
&lt;h2&gt;and Switch&lt;/h2&gt;
&lt;p&gt;I couldn&apos;t just give it back to him without doing something to them, so I decided to rickroll him because office pranks I guess.&lt;/p&gt;
&lt;p&gt;He was the one who told me that you could make the devices beep at different frequencies.
So I decided to get busy that night, since he only was in office 3 times a week and it was the perfect time to do it. Plus Mike thought it would be hilarious, so he encouraged me when I asked him.&lt;/p&gt;
&lt;h3&gt;Music is Math&lt;/h3&gt;
&lt;p&gt;Each notes had corresponding frequencies, I looked up the sheet for the infamous &quot;Never Gonna Give You Up&quot; by Rick Astley and then looked up the frequencies for the notes that I need, then started writing out some chicken scratches
&lt;img src=&quot;math.jpg&quot; alt=&quot;Notes on notes&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With this I was able to write a script for MikroTik&apos;s RouterOS and make it play it on boot instead of the standard beep:
&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/00U_kFnLjck&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;I figured it was good enough and decided to call it.&lt;/p&gt;
&lt;h1&gt;Big Success&lt;/h1&gt;
&lt;p&gt;Geoff loved it, and decided to listen to the whole script. I felt pretty good for bringing back a device that was deemed unusable, which was now beeping on his desk, I think it was a win-win for everyone, maybe other than how he got rickrolled.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://forum.mikrotik.com/t/some-music/95593/39&quot;&gt;Forum&apos;s Music Thread&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Has a great resource that are posted more recently, such as playing the tik like a piano. The songs on there is some crazy work though. I&apos;ll leave with the final video that I recovered from my old snapchat archive.
&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://youtube.com/embed/U-qTn0nR7IQ&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Journey to the DMZ</title><link>https://blog.ghwls.com/posts/ssh_incident_report/post/</link><guid isPermaLink="true">https://blog.ghwls.com/posts/ssh_incident_report/post/</guid><description>Bruteforce attack? In the current year?</description><pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::important[Incident Summary]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Designated local media server to be a DMZ host&lt;/li&gt;
&lt;li&gt;Probed initially on 24/12/31 @ 23:52 by SSH Bot&lt;/li&gt;
&lt;li&gt;First IP Address 216.107.162.242, more logged from different locations&lt;/li&gt;
&lt;li&gt;Implemented Fail2Ban, Public Key-based Authentication&lt;/li&gt;
&lt;li&gt;Resumed Normal Operation 25/01/03
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;House Keeping&lt;/h1&gt;
&lt;p&gt;I was spending time with my friends with the new years right around the corner. Just minding my own business, feeling pretty good about my server&apos;s uptime.
&lt;img src=&quot;uptime.png&quot; alt=&quot;Server uptime&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Suddenly the media server laptop&apos;s fans were on max RPM, trying to take off. So I checked processes using &lt;code&gt;htop&lt;/code&gt; and found multiple sshd processes from random public IPs.
&lt;img src=&quot;attempts.png&quot; alt=&quot;Multiple sshd processes&quot; /&gt;&lt;/p&gt;
&lt;p&gt;These IPs appeared to be registered on &lt;a href=&quot;https://www.abuseipdb.com/&quot;&gt;AbuseIP DB&lt;/a&gt; which is a registry of reported IP addresses that has a record of abusive activity. Kind of like an online criminal database.&lt;/p&gt;
&lt;p&gt;When I looked the IPs up and got a match, I was able to put together what was going on.&lt;/p&gt;
&lt;h1&gt;It Wasn&apos;t Very Effective...&lt;/h1&gt;
&lt;p&gt;It did manage odnw to slow down my connection and slow the server down in a form of Denial of Service attack. Other than that, nothing of value was lost, or stolen, or exposed.&lt;/p&gt;
&lt;h2&gt;What to do?&lt;/h2&gt;
&lt;p&gt;Now I had unwanted guests knocking on the door of my server, and it&apos;s not like I can just call up the internet police to get rid of them.&lt;/p&gt;
&lt;p&gt;I had few ways to deal with the problem at hand.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ban the &quot;guest&quot; after they make &lt;code&gt;n&lt;/code&gt; amount of failed attempts - Fail2Ban&lt;/li&gt;
&lt;li&gt;Only allow &quot;guests&quot; with a pre-authorized pass - SSH Key based Authentication&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both sounded like a good idea, but I wanted to take the least path of resistance, so I decided to go with fail2ban.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/fail2ban/fail2ban&quot;&gt;Fail2Ban&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Available on Debian repo, simply had to install it using &lt;code&gt;apt&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I copied &lt;code&gt;/etc/fail2ban/jail.conf&lt;/code&gt; to &lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt; and went through the configuration file and enabled options such as ban time.&lt;/p&gt;
&lt;p&gt;Then restarted the service via &lt;code&gt;systemctl restart fail2ban&lt;/code&gt;, then left the system on overnight to see how it would play out.&lt;/p&gt;
&lt;p&gt;I woke up the next morning to annoying max RPM fan noises, so I needed a better solution.&lt;/p&gt;
&lt;h3&gt;SSH Key Based Authentication&lt;/h3&gt;
&lt;p&gt;Maybe it was a skill issue on my end, maybe the attacker just has a crazy botnet, but that didn&apos;t matter. I just needed to get this spam sshd process to stop.&lt;/p&gt;
&lt;p&gt;I copied my public key from my daily driver laptop using&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-copy-id user@prometheus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I logged into the server, then disabled password based authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# HostbasedAuthentication
...
# To disable tunneled clear text passwrds, change to no here!
PasswordAuthentication no
#PermitEmptyPasswords no
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restarted SSH service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally there was peace, and everything went back to normal.&lt;/p&gt;
&lt;h1&gt;Learning Points&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Key based Authentication is good&lt;/li&gt;
&lt;li&gt;Managing that key is a different problem&lt;/li&gt;
&lt;li&gt;Forward specific ports, instead of exposing it all.&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Desktop Migration Notes</title><link>https://blog.ghwls.com/posts/desktop-migration/</link><guid isPermaLink="true">https://blog.ghwls.com/posts/desktop-migration/</guid><description>BTW, I use... nevermind.</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Not a Guide&lt;/h1&gt;
&lt;p&gt;This is merely just my chicken scratches for installing &quot;GNU/&quot;Linux on my desktop, so I can squeeze the last drop of performance out of it.&lt;/p&gt;
&lt;p&gt;I wanted to use UEFI only with the sole purpose of using &lt;a href=&quot;https://www.rodsbooks.com/refind/index.html&quot;&gt;rEFInd&lt;/a&gt; as my boot manager, because I can, and it&apos;s one of those things that has just worked for me in the past, and have continuted to do so.&lt;/p&gt;
&lt;p&gt;Maybe I&apos;ll try out the GRUB skins sometime in the future, but for now rEFInd minimal dark theme feels just right for my personal taste.&lt;/p&gt;
&lt;h2&gt;LiveUSB&lt;/h2&gt;
&lt;p&gt;Used the latest Arch ISO, booted into the live environment, and started following the installation guide from the infamous &lt;a href=&quot;https://wiki.archlinux.org/title/Installation_guide&quot;&gt;ArchWiki&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Display&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;# echo 3 &amp;gt; /sys/class/graphics/fbcon/rotate_all&lt;/code&gt; to rotate clockwise, if using a monitor vertically.&lt;/p&gt;
&lt;h3&gt;Disk Partition&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;# fdisk /dev/sda&lt;/code&gt; because I know my SATA ports and order them everytime I install new storage :)&lt;/p&gt;
&lt;p&gt;docker uses &lt;code&gt;/var&lt;/code&gt; which requires more maintenance if on a separate partition.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1G EFI &lt;code&gt;/dev/sda1 -&amp;gt; &quot;/boot&quot;&lt;/code&gt; Just in case of wanting to distro hop.&lt;/li&gt;
&lt;li&gt;4G Swap &lt;code&gt;/dev/sda2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;920G root &lt;code&gt;/dev/sda3 -&amp;gt; &quot;/&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Savage Installation&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::tip
&lt;code&gt;passwd&lt;/code&gt; to create live account passwword for SSH access to install from remote.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;.tmux.conf&lt;/strong&gt;: &lt;code&gt;&amp;lt;bind&amp;gt; + : resize-pane -R 20&lt;/code&gt; for pane resizing
:::&lt;/p&gt;
&lt;h3&gt;Mirrorlist&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;reflector --verbose --latest 5 --sort rate --save /etc/pacman.d/mirrorlist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to overwrite the mirrorlist with top 5 fast mirrors.&lt;/p&gt;
&lt;p&gt;:::note[Author Note]
I was tempted to do a hardened install, but I&apos;ll save that for later.
:::&lt;/p&gt;
&lt;p&gt;Installed base packages and other packages that I might need on the system, such as &lt;code&gt;vim&lt;/code&gt; because I need something to get stuck in. I grabbed &lt;code&gt;man-db, man-pages&lt;/code&gt; noob essentials that I most likey will never read unless my internet goes down, and some other firmware packages.&lt;/p&gt;
&lt;h4&gt;Official Repo&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;linux-firmware-qlogic&lt;/code&gt;
&lt;code&gt;intel-ucode&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;Arch User Repo&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ast-firmware&lt;/code&gt; -&amp;gt; ast&lt;/li&gt;
&lt;li&gt;&lt;code&gt;upd72020x-fw&lt;/code&gt; -&amp;gt; xhci_pci&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wd719x-firmware&lt;/code&gt; -&amp;gt; wd719x&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aic94xx-firmware&lt;/code&gt; -&amp;gt; aic94xx&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Following the installation guide to generate fstab, grabbed UUID from &lt;code&gt;/etc/fstab&lt;/code&gt; to use with boot loader.&lt;/p&gt;
&lt;h2&gt;chroot - Installed System&lt;/h2&gt;
&lt;p&gt;After chroot-ing into the installed system, continued following the wiki.&lt;/p&gt;
&lt;p&gt;Re-using the previous hostname:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo yggdrasil &amp;gt; /etc/hostname

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bootloader&lt;/h3&gt;
&lt;p&gt;I also wiped the pre-existing EFI paritions that was created by Windows, because I didn&apos;t like that there was 2 recovery partitions in front of the EFI partition.&lt;/p&gt;
&lt;h4&gt;rEFInd&lt;/h4&gt;
&lt;p&gt;Since we&apos;re on a UEFI installation, it should &quot;just work&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pacman -S refind
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then &lt;code&gt;refind-install&lt;/code&gt; was enough to create an entry in &lt;code&gt;/boot/refind_linux.conf&lt;/code&gt;, however it used my live usb, and  also used labels instead of disk UUID, which I didn&apos;t like.&lt;/p&gt;
&lt;p&gt;Replaced the USB label with UUID from fstab earlier to specify &lt;code&gt;&quot;/&quot;&lt;/code&gt; mount point &lt;code&gt;UUID=e192a67b-b649-4fde-a673-92f99a9acca5&lt;/code&gt;. Could&apos;ve added &lt;code&gt;initrd=&lt;/code&gt; entry for loading microcode before initial file system, but &lt;code&gt;/etc/mkinitcpio.conf&lt;/code&gt;&apos;s HOOKS array contained &lt;code&gt;microcode&lt;/code&gt; which will generated a combined image.&lt;/p&gt;
&lt;p&gt;I was pretty sure that I&apos;ve read that rEFInd will find the correct initframfs automatically from &lt;code&gt;/boot&lt;/code&gt; if nothing is specified, but I just went ahead and manually specifed both in my &lt;code&gt;refind_linux.conf&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Reboot&lt;/h2&gt;
&lt;p&gt;Unmounted the mounted partitions following the wiki, rebooted, removed the install USB, then successfully booted into bare system.&lt;/p&gt;
&lt;h3&gt;Networking&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;I totally didn&apos;t forget to install DHCP... I &lt;strong&gt;totally&lt;/strong&gt; meant to use manual configuration...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;All the links were set to down.
:::tip
&lt;code&gt;ss -atu&lt;/code&gt; to show all TCP sockets with port numbers
:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ip li set enp8s0 up&lt;/code&gt; - didn&apos;t get any IP because no DHCP client is installed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip addr add 192.168.0.82/24 dev enp8s0&lt;/code&gt; - manually assign IP.&lt;/li&gt;
&lt;li&gt;Still only able to reach local network - &lt;code&gt;ip route show&lt;/code&gt; returned empty table.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip route add default via 192.168.0.1 dev enp8s0&lt;/code&gt; - manual entry for gateway.&lt;/li&gt;
&lt;li&gt;Unable to resolve domain names - created a manual entry in &lt;code&gt;/etc/resolv.conf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nameserver 9.9.9.9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now everything was working.&lt;/p&gt;
&lt;p&gt;:::tip
&lt;strong&gt;Optionally&lt;/strong&gt; install NetworkManager and enable it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pacman -S networkmanager
systemctl enable NetworkManager.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;Then I installed a SSH server and enabled it so I could access this machine from my laptop.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pacman -S openssh
systemctl enable sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[Knowledge +1]
When dealing with systemd services with &lt;code&gt;systemctl&lt;/code&gt;, &lt;code&gt;enable&lt;/code&gt; means it will start on boot, while &lt;code&gt;start&lt;/code&gt; doesn&apos;t necessarily mean it will start on next boot.
:::&lt;/p&gt;
&lt;h1&gt;Setup &amp;amp; Personalization&lt;/h1&gt;
&lt;p&gt;After networking was persistent, I created a user account for daily use, then added it to &lt;code&gt;/etc/sudoers&lt;/code&gt;, then I went to get a different shell, because pretty colours.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.zsh.org/&quot;&gt;Zsh&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve been pavlov&apos;d to use &lt;a href=&quot;https://github.com/ohmyzsh/ohmyzsh?tab=readme-ov-file#getting-started&quot;&gt;Oh My Zsh&lt;/a&gt;, just because of the powerline theme, maybe I&apos;ll look for alternatives, but not right now.&lt;/p&gt;
&lt;p&gt;Since I&apos;ll only be using &lt;code&gt;user&lt;/code&gt; over SSH, there won&apos;t be a need to install supported fonts since a patched font is already installed on my laptop, and root prompt won&apos;t be messed up.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/romkatv/powerlevel10k?tab=readme-ov-file#installation&quot;&gt;Powerlevel10k&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;s&gt;Formerly known as Powerlevel9k&lt;/s&gt; Clean modern prompt theme for the visually dry command line.&lt;/p&gt;
&lt;p&gt;Specify the theme in &lt;code&gt;.zshrc&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
ZSH_THEME=&quot;powerlevel10/powerlevel10k&quot;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then reload the configs by running &lt;code&gt;source ~/.zshrc&lt;/code&gt;, which makes you go through a setup wizard to choose and customize the powerlevel theme. I chose something different from my laptop to visually set apart the environment at a glance.&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://wiki.archlinux.org/title/Security&quot;&gt;Security&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;User&lt;/h3&gt;
&lt;p&gt;Following the ArchWiki, I added a 4 seconds of delay between failed login attempts. Then edited the &lt;code&gt;pam_faillock.so&lt;/code&gt; to uncomment the defaults just to make sure they were being used for my own sanity.&lt;/p&gt;
&lt;p&gt;:::tip[Unlocking a User]&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;faillock --user *username* --reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;I&apos;ll limit the process counts later...&lt;/p&gt;
&lt;p&gt;Same with setting up Wayland, I don&apos;t really have a need for GUI yet, I do plan on trying Valve&apos;s new game so maybe.&lt;/p&gt;
&lt;p&gt;I remember the argument of X11 + i3 vs Wayland + Sway, when Wayland was still relatively new. I&apos;ll try Wayland this time, whenever I get around to installing a graphical environment.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&apos;m not sure if that&apos;s the best combo for gaming, but it&apos;s the one I want to try, so whatever.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before I lock down the root account, I wanted to make sure &lt;code&gt;visudo&lt;/code&gt; would use &lt;code&gt;rvim&lt;/code&gt; to edit, and I also enabled insults, because I thought it would be funny to have it on my local system.&lt;/p&gt;
&lt;p&gt;:::tip[Wiki Says...]
Always use &lt;code&gt;visudo&lt;/code&gt; to edit &lt;code&gt;/etc/sudoers&lt;/code&gt; because it will error check before copying over the contents.
:::&lt;/p&gt;
&lt;p&gt;Just had to make sure &lt;code&gt;Defaults targetpw&lt;/code&gt; or &lt;code&gt;rootpw&lt;/code&gt; was set in the &lt;code&gt;/etc/sudoers&lt;/code&gt;. Tested by adding &lt;code&gt;Defaults env_reset,timestamp_timeout=0&lt;/code&gt;, so I could verify that it asks for my password instead of root password. Commented it because I will leave the default grace period.&lt;/p&gt;
&lt;p&gt;I created a group for ssh, added my user to it, then edited &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; to only limit ssh login to users that only belong to that group.&lt;/p&gt;
&lt;p&gt;There&apos;s more user &amp;amp; permission separationg I could do, but I&apos;ll settle for disabling root account with the configuration I have so far.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;passwd -l root&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ll need to setup other users for docker &amp;amp; other services later.&lt;/p&gt;
&lt;h2&gt;Package Management&lt;/h2&gt;
&lt;p&gt;I love pacman, don&apos;t get me wrong, but picking an AUR helper was a choice paralysis hell for me last time. It appears that yaourt has been deprecated.&lt;/p&gt;
&lt;p&gt;I&apos;ll build from source untill I feel like I need to get a helper this time, and it looks like pacman can be used to do most of the things now.&lt;/p&gt;
&lt;h1&gt;Wrapping Up&lt;/h1&gt;
&lt;p&gt;I&apos;ll finish configuring power management, GUI &amp;amp; multimedia, and networking and other optimization such as TRIM for SSDs later, since this covers most of the installation.&lt;/p&gt;
</content:encoded></item><item><title>Media Server Setup Notes</title><link>https://blog.ghwls.com/posts/prometheus_notes/post/</link><guid isPermaLink="true">https://blog.ghwls.com/posts/prometheus_notes/post/</guid><description>Getting chained for sharing forbidden tech is an ancient concept.</description><pubDate>Sun, 16 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://commons.wikimedia.org/wiki/File:Jan_Cossiers_-_Prometheus_Carrying_Fire.jpg&quot;&gt;Jan Cossiers - Prometheus Carrying Fire&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Preamble&lt;/h1&gt;
&lt;p&gt;I usually don&apos;t assign hostnames to my devices names. Even if I do, I have to grow some sort of attachment to it.&lt;/p&gt;
&lt;p&gt;When I do name it, I like to give them some meaningful name, for example my desktop is named &lt;strong&gt;Yggdrasil&lt;/strong&gt; because I use it to connect to all my devices, and my old phone&apos;s hostname was &lt;strong&gt;Hermes&lt;/strong&gt;, which is just making me cringe writing this intro, but you get the idea, something mythical and thematic to their purpose.&lt;/p&gt;
&lt;p&gt;I&apos;ve been wanting to turn my old MacBook Pro into a server for a while, but I didn&apos;t really have a purpose for it. So I decided to self host a media server, and assigned the hostname &lt;strong&gt;Prometheus&lt;/strong&gt;, because it&apos;ll be chained to the wall, and sharing media. It might raise a problem when I want to try to setup the &lt;a href=&quot;https://prometheus.io/&quot;&gt;monitoring service by the same name&lt;/a&gt;, but we&apos;ll deal with that when I get there.&lt;/p&gt;
&lt;h2&gt;Hardware History&lt;/h2&gt;
&lt;p&gt;I managed to get my hands on a used &lt;strong&gt;2010 MacBook Pro&lt;/strong&gt; back in 2017 before I started my school. It was pretty beaten up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Broken left hinge&lt;/li&gt;
&lt;li&gt;Cracked trackpad&lt;/li&gt;
&lt;li&gt;Old battery&lt;/li&gt;
&lt;li&gt;HDD&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I needed XCode for college, I sure wasn&apos;t going to pay full price for a new one, so I decided to fix it up and give it a last breath. Thankfully I had access to vendor pricing for some parts.&lt;/p&gt;
&lt;p&gt;I found a dead MacBook Pro at work that another tech was using it to get parts off of, which had compatible screen and trackpad, which I received for &lt;strong&gt;free&lt;/strong&gt; (I got other free hardware from my ISP summer job, which I&apos;ll write about some other time).&lt;/p&gt;
&lt;p&gt;Overall, I spent &lt;strong&gt;$40 CAD&lt;/strong&gt; on a new battery, and about &lt;strong&gt;$100 CAD&lt;/strong&gt; on 500GB SSD, which seems like a lot now, but it was a good deal back then.&lt;/p&gt;
&lt;p&gt;The SSD came with an adapter to replace the optical drive, and I made a mistake of not getting a new charger.&lt;/p&gt;
&lt;p&gt;My Franken MacBook saw some immediate improvements after replacing the HDD with an SSD, such as lower temperature and faster boot speed.&lt;/p&gt;
&lt;h3&gt;Prep for Media Server&lt;/h3&gt;
&lt;p&gt;I used Apple&apos;s &lt;a href=&quot;https://support.apple.com/en-ca/102655#startup&quot;&gt;recovery tool&lt;/a&gt; to reset the laptop to the latest available version, however it felt sluggish after some usage, so I decided to make my annual pilgrimage to &lt;a href=&quot;https://distrowatch.com/&quot;&gt;DistroWatch&lt;/a&gt; and as always, left feeling even more unsatisfied. I narrowed it down to &lt;a href=&quot;https://www.debian.org/&quot;&gt;Debian&lt;/a&gt; vs &lt;a href=&quot;https://www.freebsd.org/&quot;&gt;FreeBSD&lt;/a&gt;, and I decided to save BSD for next time.&lt;/p&gt;
&lt;p&gt;Did a standard fresh install with these options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enable SSH&lt;/strong&gt; - since I planned on running it headless&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disable DHCP&lt;/strong&gt; - I already assigned a static IP&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Setup Notes&lt;/h1&gt;
&lt;p&gt;I had 3 main goals for this project:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make my network more fault tolerant - Done via UPS&lt;/li&gt;
&lt;li&gt;Have an automated media stack - &lt;a href=&quot;https://jellyfin.org/&quot;&gt;Jellyfin&lt;/a&gt; and other *arr stack.&lt;/li&gt;
&lt;li&gt;Securely access the said media stack from anywhere - &lt;a href=&quot;https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/#:~:text=A%20reverse%20proxy%20is%20a,security%2C%20performance%2C%20and%20reliability&quot;&gt;Reverse Proxy&lt;/a&gt; behind a domain&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I followed &lt;a href=&quot;https://zerodya.net/self-host-jellyfin-media-streaming-stack/&quot;&gt;this guide&lt;/a&gt; for setting up the Media stack. I set upthe reverse proxy myself using nginx, first manually, then used &lt;a href=&quot;https://nginxproxymanager.com/&quot;&gt;NginxProxyManager&lt;/a&gt; a.k.a. NPM, not to be confused with Node Packaage Manager :)&lt;/p&gt;
&lt;h2&gt;Planning&lt;/h2&gt;
&lt;p&gt;Since I was doing this for educational purposes, I made a diagram to show my approach, and updated the diagram from the guide to match my setup.
&lt;img src=&quot;network.png&quot; alt=&quot;Network Diagram&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As much as I like Excalidraw, the font really is horrible to read at a glance&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Jellyfin has a client for &lt;a href=&quot;https://www.tizen.org/about/&quot;&gt;Tizen&lt;/a&gt; available on their &lt;a href=&quot;https://github.com/jellyfin/jellyfin-tizen?tab=readme-ov-file#jellyfin-for-tizen&quot;&gt;github&lt;/a&gt;, but you have to build it on your own. The guide links to a &lt;a href=&quot;https://github.com/jeppevinkel/jellyfin-tizen-builds?tab=readme-ov-file#installation&quot;&gt;repo by jeppevinkel&lt;/a&gt; with other pre-built versions which I&apos;ve used.&lt;/p&gt;
&lt;h2&gt;Implementing&lt;/h2&gt;
&lt;p&gt;The diagram below goes into the services that are running on the media server.
&lt;img src=&quot;containers2.png&quot; alt=&quot;Service Diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BSDs have &lt;a href=&quot;https://www.freebsd.org/doc/handbook/jails.html&quot;&gt;jails&lt;/a&gt; which are linux&apos;s version of containers. Maybe it was a good decision to use Debian for this, since I would&apos;ve had to build a version of Jellyfin for FreeBSD based NAS on my own.&lt;/p&gt;
&lt;h3&gt;Media Stack&lt;/h3&gt;
&lt;p&gt;It was pretty straight forward setup experience following the guide. Changed my timezone accordingly using a neat macro &lt;code&gt;:%s/old/new/g&lt;/code&gt; 😎&lt;/p&gt;
&lt;p&gt;I learned some good maintenance commands from the guide, and that the volumes are mapped physical:virtual&lt;/p&gt;
&lt;h3&gt;Reverse Proxy&lt;/h3&gt;
&lt;p&gt;I decided to use a NPM container for better management experience, but later learned that it got slower security updates, so I will update this in the future migration phase.&lt;/p&gt;
&lt;p&gt;I could&apos;ve used my public IP to access it, but I can&apos;t be bothered to remember it, so I decided to get a domain.&lt;/p&gt;
&lt;p&gt;The setup was easy, port 81 was used for management, I issued a certificate using Let&apos;s Encrypt, and directed the requests to goto Jellyfin at the corresponding IP and port.&lt;/p&gt;
&lt;h4&gt;Resetting Nginx Proxy Manager&apos;s Password&lt;/h4&gt;
&lt;p&gt;I forgot the password for the web interface, so I had to access the container via cli and use sqlite3 to disable the user, then was able to recover the account using the default account.&lt;/p&gt;
&lt;h4&gt;DNS hosting&lt;/h4&gt;
&lt;p&gt;GitHub&apos;s student developer pack came with a free year of .tech domain from &lt;a href=&quot;https://get.tech/github-student-developer-pack&quot;&gt;get.tech&lt;/a&gt; so I devided to try it out.&lt;/p&gt;
&lt;p&gt;Every hosting service offers a different dashboard, therefore varying user experience. I added an A record and a SRV record for the domain to point to my public IP, then it was live in few minutes.&lt;/p&gt;
&lt;h4&gt;SSL Certificates&lt;/h4&gt;
&lt;p&gt;I planned on using subdomains to separate the services on my newly acquired domain. I had 2 options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use acme.sh to get a wildcard certificate&lt;/li&gt;
&lt;li&gt;manage multiple certificates for each subdomain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To be honest I just opted to issue multiple certificates for each subdomain for the time being, since I couldn&apos;t really figure out how to get a wildcard certificate for my provider. Once I migrate my domain to Cloudflare then I might give it another attempt, but for now this got the job done.&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;When I was actually using the media server, I noticed that my RAM usage was hitting cap and would cause the machine to hang sometimes, I simply assigned more swap space and problem was magically solved.&lt;/p&gt;
&lt;p&gt;Other than the laptop fan whirring very loudly when someone is watching something when I&apos;m trying to sleep, I have no complaints, one day I&apos;ll set it free from it&apos;s eternal punishment.&lt;/p&gt;
</content:encoded></item><item><title>Aeron Repair Horror Story</title><link>https://blog.ghwls.com/posts/aeron_repair/post/</link><guid isPermaLink="true">https://blog.ghwls.com/posts/aeron_repair/post/</guid><description>This is a true story.</description><pubDate>Sat, 10 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Memes are dangerous&lt;/h1&gt;
&lt;p&gt;Well known for it&apos;s signature design, a staple of tech startups, the &lt;a href=&quot;https://www.hermanmiller.com/products/seating/office-chairs/aeron-chair/&quot;&gt;Aeron&lt;/a&gt; is a highly coveted chair for individuals that usually lead a sedentary lifestyle.&lt;/p&gt;
&lt;h2&gt;In Pop Culture&lt;/h2&gt;
&lt;p&gt;I don&apos;t actually remember when I was first introduced to the chair, but I knew I wanted one as soon as I saw it. Since then, I&apos;ve been noticing them in TV shows from the early 2000s, such as &lt;em&gt;House&lt;/em&gt; (2004) and &lt;em&gt;The Office&lt;/em&gt; (2005).&lt;/p&gt;
&lt;p&gt;It also makes an appearance on &lt;em&gt;The Simpsons&lt;/em&gt; (S16 E19):
&lt;img src=&quot;god.png&quot; alt=&quot;The Simpsons - Season 16, Episode 19&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Finding the Chair&lt;/h1&gt;
&lt;p&gt;I&apos;ve always wanted one ever since I knew of it&apos;s existence, but growing up in a rural town with no car, I couldn&apos;t really go out of my way to buy one, but when I found one in my size for a decent price, I immediately asked my friend for a ride.&lt;/p&gt;
&lt;h2&gt;&apos;Tis but a Scratch&lt;/h2&gt;
&lt;p&gt;It was in a pretty good condition for a chair made in 2001, but it still had some problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dysfunctional &lt;strong&gt;gas cylinder&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Lightly damaged &lt;strong&gt;lumbar support&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&quot;Broken&quot; &lt;strong&gt;tilt control&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Loose &lt;strong&gt;left arm rest&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Loose &lt;strong&gt;seat mesh&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After getting told these things I was still determined to buy it.&lt;/p&gt;
&lt;h2&gt;Diagnosis &amp;amp; Treatment&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Gas cylinder replacement from Amazon for about $50.&lt;/li&gt;
&lt;li&gt;Lumbar support was still usable, glued it together and found a replacement just in case.&lt;/li&gt;
&lt;li&gt;Tilt control wasn&apos;t actually broken, I just had to put the wire back into the proper slot.&lt;/li&gt;
&lt;li&gt;After opening up the left arm, discovered that a piece of plastic was broken. I found a replacement, but decided I wasn&apos;t going to buy it since it didn&apos;t affect the functionality that much.&lt;/li&gt;
&lt;li&gt;There&apos;s a foam that goes between the mesh and the seat frame. It was compressed due to the age of the chair, so I got a new one for about $20, which resulted in a taut seat mesh.
&lt;img src=&quot;foam.png&quot; alt=&quot;Seat foam comparison&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Apart from that, I ordered a set of caster wheels which was recommended by many owners online.&lt;/p&gt;
&lt;h3&gt;The realization&lt;/h3&gt;
&lt;p&gt;I was able to do all the minor repairs, but replacing the gas cynlinder was a different story.&lt;/p&gt;
&lt;p&gt;I had to somehow remove the broken cylinder that had been stuck to the chair for the last 20 years.&lt;/p&gt;
&lt;p&gt;So I went down to the hardware store and got a pipe wrench.
&lt;img src=&quot;chair-1.png&quot; alt=&quot;Pipe wrench on cylinder&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Success&lt;/h3&gt;
&lt;p&gt;It took me about 9 hours on and off to remove the cylinder, at which point I was able to kick the base off.
&lt;img src=&quot;chair-2.png&quot; alt=&quot;Removed cylinder from chair&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Looking back at it, I could&apos;ve just taken the chair outside and poured some hot water on the piece that was stuck, but it&apos;s done now.&lt;/p&gt;
&lt;h1&gt;Review&lt;/h1&gt;
&lt;p&gt;I enjoy sitting on this chair, I&apos;ve grown attached to it and it&apos;s definitely the highest quality mesh chair I&apos;ve sat on so far. I would definitely remove the cylinder differently next time (if I&apos;m ever forced to do it again).
&lt;img src=&quot;chair-3.png&quot; alt=&quot;Chair at my desk&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>i3blocks Air Quality Module</title><link>https://blog.ghwls.com/posts/i3blocks-aqi-module/</link><guid isPermaLink="true">https://blog.ghwls.com/posts/i3blocks-aqi-module/</guid><description>A simple module to keep track of air quality</description><pubDate>Wed, 17 Jul 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note[&lt;em&gt;Hello!&lt;/em&gt;]
&lt;em&gt;This is post is old and possibly outdated. I&apos;m keeping it here for reference. (2024/09/01)&lt;/em&gt;
:::&lt;/p&gt;
&lt;h1&gt;Overview&lt;/h1&gt;
&lt;p&gt;Making API requests then displaying response on &lt;a href=&quot;https://i3wm.org/i3bar/&quot;&gt;i3bar&lt;/a&gt;.
Using &lt;a href=&quot;https://curl.se/&quot;&gt;cURL&lt;/a&gt;,&lt;a href=&quot;https://linux.die.net/man/1/sed&quot;&gt;sed&lt;/a&gt;, and &lt;a href=&quot;https://linux.die.net/man/1/awk&quot;&gt;AWK&lt;/a&gt; to retrieve &amp;amp; manipulate the response data.
This post will cover the process of making a module for &lt;a href=&quot;https://vivien.github.io/i3blocks/&quot;&gt;i3blocks&lt;/a&gt; to display the local air quality index (&lt;a href=&quot;https://en.wikipedia.org/wiki/Air_quality_index&quot;&gt;AQI&lt;/a&gt;).&lt;/p&gt;
&lt;h1&gt;Why?&lt;/h1&gt;
&lt;p&gt;During my stay in South Korea, I&apos;ve noticed that air quality was a rising isse. The residents are mostly blaming it on the neighbouring country, but I&apos;m not going to get into that.&lt;/p&gt;
&lt;p&gt;Rather than fixing the bigger problem, I&apos;ve decided to write a small module for my status bar to keep me updated on the local air quality. I&apos;m doing little things that I can actually achieve.&lt;/p&gt;
&lt;p&gt;It&apos;s called a &quot;module&quot; but under the hood, they&apos;re simply a group of commands to display a number on my status bar. Nothing new there.
There are existing modules out there to show weather reports, and I knew where to get the AQI from, so all I had to do was simply put the two together.&lt;/p&gt;
&lt;h2&gt;Preparation&lt;/h2&gt;
&lt;p&gt;First I needed to grab the air quality data from somewhere, then I came across this &lt;a href=&quot;aqicn.org&quot;&gt;website&lt;/a&gt;. By default, it shows your location, local air quality forecast, and explains what the numbers mean.&lt;/p&gt;
&lt;p&gt;Further down the page, there&apos;s a &lt;a href=&quot;https://aqicn.org/api&quot;&gt;link&lt;/a&gt; to their API, which returns more information than I needed, so I still needed to put in some effor to make the information usable for the status bar.&lt;/p&gt;
&lt;p&gt;I needed to get a token to access the API, which was well documented on the initial setup section of the page. Once I got my token, I was able to make a successful request using cURL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s http://api.wapi.info/feed/here/?token=yourtoken
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which returns following format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;status&quot;:&quot;ok&quot;,
    &quot;data&quot;:
    {
        &quot;aqi&quot;:69,
        &quot;idx&quot;:xxxx,
        &quot;attributions&quot;:
        [
            {
                &quot;url&quot;:&quot;https://www.airkorea.or.kr/&quot;,
                &quot;name&quot;:&quot;South Air Korea Environment Corporation&quot;
            },
            {
                &quot;url&quot;:&quot;https://waqi.info/&quot;,
                &quot;name&quot;:&quot;World Air Quality Index Project&quot;
            }
        ],
        &quot;city&quot;:
        {
            &quot;geo&quot;:[latitude,longitude],
            &quot;name&quot;:&quot;Some City, Some Place, Somewhere, South Korea&quot;,
            &quot;url&quot;:&quot;https://aqicn.org/city/korea/some/place&quot;
        },
        &quot;time&quot;:
        {
            &quot;s&quot;:&quot;2019-07-10 22:00:00&quot;,
            &quot;tz&quot;:&quot;+09:00&quot;,
            &quot;v&quot;:1562796000
        },
        &quot;debug&quot;:
        {
            &quot;sync&quot;:&quot;2019-07-10T22:26:57+09:00&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pretty standard JSON response. Using &lt;code&gt;here&lt;/code&gt; returns the local air quality, the website also contains a &lt;a href=&quot;aqicn.org/city/all&quot;&gt;list of all available cities&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Now What?&lt;/h2&gt;
&lt;p&gt;I saved the response to a file, which was done with pipes and redirection.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s http://api.wapi.info/feed/here?token=mytoken &amp;gt; ~/file/location/airdata
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now with a file to work with, I had to trim the response because I only needed to AQI value.&lt;/p&gt;
&lt;p&gt;I decided to use &lt;em&gt;sed&lt;/em&gt; and &lt;em&gt;awk&lt;/em&gt; for this text manipulation, there&apos;s a better way to do this probably, but this is how I did it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sed &quot;s/,/\n/g;s/:/ /g&quot; ~/file/location/airdata | awk /data/ | awk &quot;{print \$3}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Breakdown&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sed&lt;/code&gt; is a stream editior, it reads the file line by line and performs the operations that are given.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s/,/\n/g&lt;/code&gt; replaces all the commas with new line character &lt;code&gt;\n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s/:/ /g&lt;/code&gt; replaces all the colons with spaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;awk /data/&lt;/code&gt; prints the line that contains the word &quot;data&quot;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;awk &quot;{print \$3}&quot;&lt;/code&gt; prints the third word of the line.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Output for each command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;status&quot; &quot;ok&quot; &quot;data&quot; {&quot;aqi&quot; 69
&quot;data&quot; {&quot;aqi&quot; 69
69
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Nice&lt;/strong&gt;, now that I have what I need, I can move on to the i3blocks part of this.&lt;/p&gt;
&lt;h3&gt;Some Assembly Required&lt;/h3&gt;
&lt;p&gt;Luke Smith&apos;s &lt;a href=&quot;https://github.com/LukeSmithxyz/voidrice/blob/fb456c4d98ac44497294617411c1100943d14a40/.local/bin/statusbar/weather&quot;&gt;weather module&lt;/a&gt; for LARBS was a good place to start.&lt;/p&gt;
&lt;p&gt;I had to modify the script to point to the correct directoy and file, which will be created by the script later.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rm -f &quot;$HOME/.local/share/airdata&quot; ;}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This will clear pre-existing data file when the script runs.&lt;/p&gt;
&lt;p&gt;I modified the &lt;code&gt;getforecast()&lt;/code&gt; function into &lt;code&gt;get_air_quality()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;get_air_quality() {
    ping -q -c 1 1.1.1.1 &amp;gt; /dev/null || exit 1
    curl -s &quot;http://api.wapi.info/feed/here?token=mytoken&quot; &amp;gt; &quot;$HOME/.local/share/airdata&quot; || exit 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check if device is connected to the internet.&lt;/li&gt;
&lt;li&gt;Then send a request to the API.&lt;/li&gt;
&lt;li&gt;Save the response into a file called &lt;code&gt;airdata&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Which will be read by the script. Then I modified the &lt;code&gt;showforecast()&lt;/code&gt; function into &lt;code&gt;show_air_quality()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show_air_quality() {
    sed &quot;s/,/\n/g;s/:/ /g&quot; $HOME/.local/share/airdata | awk /data/ | awk &quot;{print \&quot;AQI: \&quot; \$3}&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function will return something that looks like &lt;code&gt;AQI: 69&lt;/code&gt;, which will be displayed on the status bar.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case $BLOCK_BUTTON in
    2) get_air_quality &amp;amp;&amp;amp; show_air_quailty ;;
    3) pgrep -x dunst &amp;gt;/dev/null &amp;amp;&amp;amp; notify-send &quot;Air Quality Module&quot; &quot;\- Middle click to update forecast.
-AQI - Air Quality Index
   0-50: Good
 51-100: Moderate
101-150: Mildly Unhealthy
151-200: Unhealthy&quot; ;;
esac

if [ &quot;$(stat -c %y &quot;$HOME/.local/share/airdata&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 | awk &apos;{print $1}&apos;)&quot; != &quot;$(date &apos;+%Y-%m-%d&apos;)&quot; ]
	then rm $HOME/.local/share/airdata &amp;amp;&amp;amp; get_air_quality &amp;amp;&amp;amp; show_air_quality
	else show_air_quality
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[Mouse Actions]
Each case corresponds to different mouse actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1) Left click&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2) Middle click&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3) Right click&lt;/code&gt;
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For this module, I&apos;ve yet to find a use for left clicking on the module, so I decided to leave it out. I added some descriptions for the numbers. Saved the file as &quot;airquality&quot;, then added it to my i3blocks config gile, then set it to update every 30 minutes.&lt;/p&gt;
&lt;h3&gt;Final Touches&lt;/h3&gt;
&lt;p&gt;Still need to make the script executable.
&lt;code&gt;chmod +x ~/.local/bin/statusbar/airquality&lt;/code&gt;
in my case.&lt;/p&gt;
&lt;h1&gt;Wrapping Up&lt;/h1&gt;
&lt;p&gt;Once I reloaded the i3wm, I was agreeted with an air quality index. The module responds to mouse actions as expected, and I&apos;m happy with the result.&lt;/p&gt;
&lt;p&gt;Writing this module was a good practice, since I got to write something that I found useful, and it was pretty easy to do so since most of the work was already done for me. I got to make something I could use everyday.&lt;/p&gt;
</content:encoded></item></channel></rss>