Publish Homelab Tour Series Intro

Fix intermittently broken article/project list rendering,
implement table of contents shortcode,
implement css snippet to reduce underline-fatigue for table of contents,
remove filler article files,
generate static files.
This commit is contained in:
Joey Hafner 2024-06-28 17:25:59 -07:00
parent ce373f5564
commit c5532734a4
43 changed files with 2502 additions and 194 deletions

View File

@ -1,10 +1,18 @@
# Running local dev server
1. Ensure theme submodule is loaded: `git submodule init && git submodule update`
2. Run the server with `hugo server --buildDrafts --disableFastRender`
2. Run the server with `hugo server --noHTTPCache --ignoreCache --disableFastRender --buildDrafts`
## Including Images in Content
*Good old-fashioned `![](image.png)` markdown image embedding works just fine.*
1. Place the image file beside the content in the folder.
2. For a Featured Image, use the line `featured_image = "../pamidi.jpg"` in the frontmatter.
3. For an inline image, use `{{< image src="../pamidi.jpg" >}}`
> Note: The working directory for relative resource locations uses the name of the content file as the current location. E.g. referencing the image `./myimage.jpg` or `./myimage.jpg` from inside the `/content/projects/pamidi.md` content file, would look for those images at `/content/projects/pamidi/myimage.jpg`.
> Note: The working directory for relative resource locations uses the name of the content file as the current location. E.g. referencing the image `./myimage.jpg` or `./myimage.jpg` from inside the `/content/projects/pamidi.md` content file, would look for those images at `/content/projects/pamidi/myimage.jpg`.
## Add a Table of Contents
- To include a table of contents at the beginning of a page, add the flag `toc = true` to the frontmatter.
- To insert a table of contents inline with the text, use the `{{% toc %}}` shortcode.
- Tables of contents are configured under the `[markup]` configuration node in [`config.toml`](/config.toml).

View File

@ -21,20 +21,23 @@ enableRobotsTXT = true
enableGitInfo = false
enableEmoji = true
enableMissingTranslationPlaceholders = false
disableRSS = false
disableSitemap = false
disableRSS = true
disableSitemap = true
disable404 = false
disableHugoGeneratorInject = false
[permalinks]
posts = "/:title/"
[permalinks.section]
articles = "/articles/"
projects = "/projects/"
pages = "/"
[blackfriday]
hrefTargetBlank = true
[taxonomies]
tag = "tags"
category = "categories"
tag = "tags"
category = "categories"
series = "series"
[params]
@ -50,7 +53,7 @@ disableHugoGeneratorInject = false
enableThemeToggle = false
enableSharingButtons = true
enableGlobalLanguageMenu = false
customCSS = []
customCSS = ["/css/toc-no-underline.css"]
customJS = []
justifyContent = false # Set "text-align: justify" to .post-content.
[params.author]
@ -130,3 +133,9 @@ disableHugoGeneratorInject = false
name = "Projects"
url = "projects"
weight = 40
[markup]
[markup.tableOfContents]
endLevel = 3
ordered = false
startLevel = 1

View File

@ -1,5 +0,0 @@
+++
title = 'AI Stack With Docker on AMD'
date = 2024-05-28T17:57:59-07:00
draft = true
+++

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -1,5 +0,0 @@
+++
title = 'Homelab Security'
date = 2024-05-28T17:57:44-07:00
draft = true
+++

View File

@ -1,5 +0,0 @@
+++
title = 'Homelab Tour - May 2024'
date = 2024-05-28T17:57:28-07:00
draft = true
+++

View File

@ -0,0 +1,197 @@
+++
title = 'Homelab Tour Series - Intro'
date = 2024-06-27T09:15:13-07:00
draft = false
toc = false
+++
# There are many like it...
But this lab is built *by me and for me*. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I've gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I've gotta configure the SMTP notifications, the [DNS](https://xkcd.com/2259/), the firewalls and the subnets. I open, update, and close [issues](https://gitea.jafner.tools/Jafner/homelab/issues), [remediate leaked secrets](https://gitea.jafner.tools/Jafner/homelab/issues/128), and write documentation.
It's exhausting and exhilarating, frustrating and fulfilling, thankless and thankful. And I've never written about it before, so here's me finally getting to do that.
{{% toc %}}
# Core Services: It's about data.
[For](https://www.npr.org/2021/04/09/986005820/after-data-breach-exposes-530-million-facebook-says-it-will-not-notify-users) [myriad](https://www.forbes.com/sites/quickerbettertech/2021/07/05/a-linkedin-breach-exposes-92-of-usersand-other-small-business-tech-news/) [reasons](https://www.theverge.com/2018/5/3/17316684/twitter-password-bug-security-flaw-exposed-change-now), [I want](https://www.techtarget.com/whatis/feature/SolarWinds-hack-explained-Everything-you-need-to-know) to [maintain](https://www.bbc.com/news/technology-37232635) as much [control](https://www.theverge.com/2022/12/22/23523322/lastpass-data-breach-cloud-encrypted-password-vault-hackers) of [my data](https://www.bbc.com/news/technology-58817658) [as possible](https://cloudsecurityalliance.org/blog/2022/03/13/an-analysis-of-the-2020-zoom-breach). So I bought hard drives to store my data, and built computers around those hard drives to move my data to and fro. Lastly, I selected a few of the awesome projects others have built to tell the computers how to move my data.
![Apps.png|App icons left-to-right: Bitwarden, Gitea, Nextcloud, Zipline, Send, Home Assistant, TrueNAS Scale, VyOS](../Apps.png)
Veteran self-hosters are familiar with many of these, but I want to talk about how each of these projects helps me claw back a little bit of control over my data.
## Bitwarden: The best password manager
*Password management server.* | [Bitwarden](https://bitwarden.com/) ([Vaultwarden](https://github.com/dani-garcia/vaultwarden)) | [`bitwarden.jafner.tools`](https://bitwarden.jafner.tools) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/druid/config/vaultwarden/docker-compose.yml)
I used to use LastPass.
- *June 15, 2015* [ArsTechnica: Hack of cloud-based LastPass exposes hashed master passwords](https://arstechnica.com/information-technology/2015/06/hack-of-cloud-based-lastpass-exposes-encrypted-master-passwords/)
- *December 28, 2021* [BleepingComputer: LastPass users warned their master passwords are compromised](https://www.bleepingcomputer.com/news/security/lastpass-users-warned-their-master-passwords-are-compromised/)
- *November 30, 2022* [BleepingComputer: Lastpass says hackers accessed customer data in new breach](https://www.bleepingcomputer.com/news/security/lastpass-says-hackers-accessed-customer-data-in-new-breach/)
Bitwarden has an [excellent security track record *so far*](https://bitwarden.com/blog/third-party-security-audit/). But two factors (ha!) led to my choosing to self-host the community-built server instead of using Bitwarden's first-party cloud service:
1. Subscription-gated features. 2FA/OTP authenticator, file attachments, security reports, and more are gated behind a subscription. I wholeheartedly endorse Bitwarden's decision, but that's just enough encouragement for me to host it myself.
2. Bigger means more attractive target. The more people put their trust in Bitwarden, the more attractive a target it becomes. My personal server is unlikely to attract the attention of any individuals or organizations with the capability to penetrate *Bitwarden's* security. Of course, that only matters if I maintain good security posture everywhere else in the homelab. More on that another day.
I've had an excellent experience with Bitwarden so far. The user experience is fluid enough that I was able to onboard my family without issue.
## Gitea: Control the source
*Source control and basic CI/CD.* | [Gitea](https://about.gitea.com/) | [`gitea.jafner.tools`](https://gitea.jafner.tools/) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/druid/config/gitea)
GitHub is great. And I often question my decision to host my own Git and CI/CD server. I'm not foolish enough to worry that any of my code would be included in any high-quality AI training datasets. Really, In
I started out using a self-hosted GitLab instance, but its power and flexibility entail weight. And I got tired of the administrative toil caused by frequent and substantial updates to the entire platform.
So I spun up Gitea, set up some [runners](https://docs.gitea.com/next/usage/actions/act-runner/), and got back to work.
## Nextcloud: Corner office with a view
*Office services (files, photos, calendar, contacts).* | [Nextcloud](https://nextcloud.com/) | [`nextcloud.jafner.net`](https://nextcloud.jafner.net) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/nextcloud)
Google Drive is pretty cool and useful. I didn't care much for Docs and the other office products, but the storage, sharing, and sync functionality Drive provided was useful at school, at home, at work, and even for gaming. But it's hard-limited to 15 GB on the free version. And expanding that costs ~$5/TB mo., which is only $0.63 cheaper than buying an 8TB SAS HDD *every month*. So I decided `drives > Drive`.
And it feels luxurious. Knowing that all my phone's photos and videos are stored on my hardware and won't every touch Google's servers. I can take a full-fat video without worrying that 600 MB will eat up a bunch of my quota. Nextcloud also offers a wide swathe of plugins for other functionalities via their [App store](https://apps.nextcloud.com/).
## Manyfold: Library management for 3D models
*3D Model library manager.* | [Manyfold](https://github.com/manyfold3d/manyfold) | [`3d.jafner.net`](https://3d.jafner.net/) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/vandam)
Over the years I've spent a *lot* of money on 3D models for fantasy RPG miniatures. My collection of models is measured in *terrabytes*. And I have a deep appreciation for the creative work of the artists who make these models. But artists, I think, are not naturally inclined toward robust file organization practices. So my "collection" is really more like a pile.
But Manyfold (formerly "VanDAM") has helped enormously with the process of making that collection usable. Search and preview are the two biggest features. Automated tagging and other organizational features also help a lot.
I still have a lot of manual work to do though...
## Send: The service formerly known as Firefox Send
*Quick and secure file share. [XKCD#949](https://xkcd.com/949/).* | [Send](https://github.com/timvisee/send) | [`send.jafner.net`](https://send.jafner.net/) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/send)
The XKCD comic above articulates and experience I've had enough times. Nextcloud helps a lot when I want to share a file with someone else. But Send covers the cases where a friend wants to send me a file, or a friend is asking me how to send a file to another friend. I can just send them the link. I don't even need to explain how it works, it's built intuitively enough. And I get some peace of mind knowing that the files are encrypted end-to-end.
## Zipline: Clip that
*Media sharing server (upload screenshots, recordings).* | [Zipline](https://github.com/diced/zipline) | [`zipline.jafner.net`](https://zipline.jafner.net) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/zipline)
This service exists exclusively to let me right click the video file of a gaming highlight, hit "Share", and send the link to my friends as seamlessly as possible *while supporting high-fidelity content*. I record my gameplay at 1440p 120 FPS. Check it out:
- [Venture 5k](https://zipline.jafner.net/u/%5B2024-06-19%5D%20Neato.mp4).
- [Venture scrim highlight](https://zipline.jafner.net/u/%5B2024-06-19%5D%20Scrim%20(1:55:22-1:56:02).mp4)
- [My greatest widowmaker highlight](https://zipline.jafner.net/u/ImYtUX.mp4)
That last one's not even 120 fps, I just love to show it. To be honest though, if Youtube supported scripted/automated uploads I would just use that. But it's probably for the best that they don't.
## Home Assistant: Climate control for the Critter Cove
*Home automation and monitoring.* | [Home Assistant](https://www.home-assistant.io/) | [`homeassistant.jafner.net`](https://homeassistant.jafner.net) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/home-assistant)
I think a lot of folks start their self-hosting journey with Home Assistant. It's a fantastic tool, and it keeps some of your most important data from being reliant on [often-flakey vendors](https://gizmodo.com/the-never-ending-death-of-smart-home-gadgets-1842456125) who have little interest in supporting their product unless you're giving them money for it.
My partner and I have four reptiles, several insects, and a hamster whose climates I simply cannot be bothered to adjust manually every hour of the waking day. So I installed Home Assistant, hooked up warm-side and cool-side [Govee hygrometer/thermometers](https://us.govee.com/products/govee-bluetooth-hygrometer-thermometer-h5075?Style=1*H5075), put the heating lamps on [TP Link smart dimming plugs](https://www.kasasmart.com/us/products/smart-plugs/product-kp405), and wrote some automation to keep everybody in their happy temperature range.
## VyOS: My router is a text file
*Configuration-as-code router OS.* | [VyOS](https://vyos.io/) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/wizard/config)
One day I thought to myself, "do I know how a router works?" The answer was no, so I built one (hardware and configuration) from scratch between the hours of 10 PM and 4 AM to ensure none of my housemates would be disturbed by the requisite internet outage. I deployed the seat-of-my-pants router configuration to "production" overnight and handled about one day's worth of post-deployment support before everything was seamlessly stable.
The hardest part was crimping and terminating every single ethernet cable. At least 20 connections. Woof.
## TrueNAS: How I sleep at night
*Data safety provider.* | [TrueNAS](https://www.truenas.com/truenas-scale/) | [Configuration: Main](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/barbarian) | [Configuration: Backup](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/monk)
Underpinning every single one of the above services in one way or another is my TrueNAS deployment. It is composed of two hosts: a primary and a backup. Every day, each system takes a [differential snapshot](https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/periodicsnapshottasksscreensscale/) of each dataset, and runs a short [S.M.A.R.T. test](https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/smarttestsscreensscale/) on each disk. Nightly, the most important datasets on the primary are backed up to the backup as an [Rsync task](https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/rsynctasksscreensscale/). And every week it runs a [scrub task](https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/scrubtasksscreensscale/) to ensure the stored data still checksums correctly.
And of course, it runs an SMB server and iSCSI target to facilitate clients and applications interacting with their own little data puddle.
# Additional Services: Just for fun
In addition to the important services above, I run a handful of services just for off time.
## Plex: I wish Jellyfin supported SSO
The superior media server, Plex provides a free (as in beer) solution with a beautiful frontend and comprehensive metadata scraping. Plex has set a standard for serving movies and TV that no alternative service has been able to match. It is the unwelcome incumbent no one has mustered the resources to dethrone. Over the last dthe usernames-emails-passwords) in 2022.
But I digress...
[Jellyfin](https://jellyfin.org/) is the leading opposition. But Jellyfin's [most wanted feature requests](https://features.jellyfin.org/?view=most-wanted) paints a sorry picture of the state of things. Features I consider critical, such as [2FA](https://features.jellyfin.org/posts/26/add-support-for-two-factor-authentication-2fa), [transcoding](https://features.jellyfin.org/posts/284/convert-option-similar-to-plexs-optimise), [offline access for mobile clients](https://features.jellyfin.org/posts/218/support-offline-mode-on-android-mobile), [to-watch lists](https://features.jellyfin.org/posts/576/watchlist-like-netflix), [watch history](https://features.jellyfin.org/posts/633/watched-history), [OIDC support](https://features.jellyfin.org/posts/230/support-for-oidc) / [OAuth support](https://features.jellyfin.org/posts/271/oauth-support), [list collections to which a movie belongs](https://features.jellyfin.org/posts/540/list-all-collections-that-a-movie-belong-to-in-movie-details), and more I'm sure if I just keep scrolling. Maybe a few more years.
## 5eTools: If D&D Beyond was designed by software engineers instead of business boys
5eTools is an insanely high quality, data-driven repository for each and every piece of content ever made for D&D 5th edition. Fortunately, they have a *blocklist* feature to restrict the visible content to only the stuff I've bought the books for.
## Calibre-web: Frontend for the premiere ebook library manager
[Calibre-web](https://github.com/janeczku/calibre-web) | [Calibre](https://calibre-ebook.com/) | [`rpg.calibre.jafner.net`](https://rpg.calibre.jafner.net/) | [`sff.calibre.jafner.net`](https://sff.calibre.jafner.net/)
Sometimes, rarely, I read a book. Much more freqeuently, I want to check a section from a book. Calibre(-web) affords me access to my entire library of books and ebooks from a web browser. Frustratingly, the owner of the project has decided not to implement generic OAuth2/OIDC support. [But open-source, uh, finds a way.](https://github.com/janeczku/calibre-web/pull/2211#issuecomment-1182460156)
## Minecraft: Geoff \"itzg\" Bourne is a blessing
Hosting game servers for my friends got me into this stuff, and has been a throughline of my life for over 15 years.
And Minecraft has been a consistent presence in that domain. Today that task is easier and more polished than ever.
Two Docker images, [itzg/mc-router](https://github.com/itzg/mc-router) and [itzg/docker-minecraft-server](https://github.com/itzg/docker-minecraft-server) have handled every Minecraft server I've hosted since late 2020. The configuration is simple and declarative. The best part is the reverse proxying. In 2015 I would head to [WhatsMyIP.org](https://www.whatsmyip.org/), copy the number at the top, and send it to my friends. Then they would manually type it into the connect dialog (copy-paste can be challenging), type something wrong, get a connection error, and call me on Google Hangouts. We'd, eventually get it figured out, but now I can just say "It's `e9.jafner.net`" and that seems to stick a lot better.
# Admin Services: Help me handle all this
Nothing since has given me the same high as seeing the green padlock next to `jafner.net` for the first time. After years of typing IPs, memorizing ports, skipping "Your connection is not private" pages, and answering "What's the IP again?", that green padlock was more gratifying than the $60k piece of paper framed on my wall.
Below are the tools and services I use to make everything else work *properly*.
## Traefik: One-liner* TLS-certified subdomains
*Docker-integrated reverse proxy.* | [Traefik](https://github.com/traefik/traefik) | [Configuration: Fighter](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/traefik/docker-compose.yml) | [Configuration: Druid](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/traefik/docker-compose.yml)
*Okay, it's not literally one line. It's five.
```yml
networks:
- web
labels:
- traefik.http.routers.myservice.rule=Host(`myservice.jafner.net`)
- traefik.http.routers.myservice.tls.certresolver=lets-encrypt
```
Lines #1-2 configure a service to attach to the Docker network Traefik is monitoring.
Lines #3-5 tell Traefik what (sub)domain(s) that service should serve, and tell it to provision a fresh LetsEncrypt certificate.
- No wildcard certs.
- No manual cert requests.
- No services are exposed by default.
In addition to handling that core functionality, it also offers "middlewares" to handle additional functionality, like [forwardauth](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/traefik/config/config_addons.yaml#L49), which helps me protect services that don't support [OAuth2](https://oauth.net/2/)/[OIDC](https://www.microsoft.com/en-us/security/business/security-101/what-is-openid-connect-oidc), such as [WG-Easy](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/wireguard/docker-compose.yml#L26).
I have never been tempted to look for alternatives. I struggle to imagine how a reverse proxy could be better for my use-case without overstepping its role.
Oh, it would be nice if it handled [dispatching to other Traefik instances](https://gitea.jafner.tools/Jafner/homelab/issues/71) in a more intuitive/simple way. I haven't been able to get that working.
## Keycloak: Sign in to Jafner.net
*IAM provider* | [Keycloak](https://www.keycloak.org/) | [Configuration](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/keycloak)
It's easy to allow your password manager's "local accounts" folder slowly grow to dozens or hundreds of credentials as services are trialed, decomissioned, reinstalled, and troubleshooted (troubleshot?). Further, supporting basic account information updates for *users that aren't me* is non-trivial. How many SMTP submission API keys would I need to support each of my services sending their own "Recover your account password" emails?
Keycloak provides much-needed consolidation of account management. Just like a *real website*, anyone who wants to *do something* with one of my services (e.g. open a [Gitea issue](https://gitea.jafner.tools/issues)) can walk through the familiar account creation process. Give a first and last name, email, username, and password. You'll get a verification email in your inbox from `"Keycloak Admin" <noreply@jafner.net>`. Click the link, go back to the page you were trying to access, and *boom*, you can do stuff. Stuff I don't want you to be able to do is still gated behind manual account approval. For example, you can't just create an account and start uploading files to Nextcloud. Oh, and it supports 2FA.
One day I'll be able to manage *every single Jafner.net application's* ID and access via Keycloak, but a few have held out. But that's an entire article itself.
Until then I'll be happy that *most* applications and services either support native OAuth2 or OIDC, or are single-user and can be simply gated behind Traefik forwardauth.
## Wireguard: Road warrior who needs to reboot the Minecraft server
*Quick, easy, fast VPN plus quick, easy, fast web UI* | [Wireguard](https://www.wireguard.com/) | [WG-Easy](https://github.com/wg-easy/wg-easy) | [Configuration: Fighter](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/wireguard/docker-compose.yml) | [Configuration: Druid](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/wireguard/docker-compose.yml)
Every homelabber is faced with the question of how to administrate their server when they aren't sitting (or standing) at their desk at home. It's a great question; you're dipping your toes into [security posture](https://csrc.nist.gov/glossary/term/security_posture), and the constant tension between security and ease-of-access. Using SSH keys instead of passwords is a no-brainer, but do you also configure [SSH 2FA](https://www.digitalocean.com/community/tutorials/how-to-set-up-multi-factor-authentication-for-ssh-on-ubuntu-20-04)? Most folks don't allow SSH traffic directly through the router, but *how* do you build and configure your VPN for SSHing into your server?
The steps I take to secure my SSH hosts are detailed [here](https://gitea.jafner.tools/Jafner/homelab/src/branch/main/docs/Security.md#securing-ssh), but I'll be digging into that more in another article. In brief:
- SSH keys are matched to a user and host. E.g. `Joey_Desktop`, or `Joey_Phone`.
- Authorized keys are added only as needed. There is no template. Nothing is included by default.
- Each SSH server is configured to require pubkey authentication and disable password authentication.
- Each SSH server is configured to require 2FA via the `google-authenticator` PAM module.
- The router is not configured to port-forward any SSH traffic. Accessing SSH requires VPNing.
Lots of iteration led to this configuration. I'm sure I'll write it out at some point.
## Grafana, Prometheus, and Uptime-kuma: Observability before I knew what that meant
[Grafana](https://grafana.com/) | [Prometheus](https://prometheus.io/) | [Uptime-kuma](https://github.com/louislam/uptime-kuma) | [Configuration: Monitoring Fighter](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/monitoring) | [Configuration: Uptime-kuma Fighter](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/uptime-kuma/docker-compose.yml) | [Configuration: Monitoring Druid](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/monitoring/docker-compose.yml) | [Configuration: Uptime-kuma Druid](https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/uptime-kuma/docker-compose.yml)
Before I knew what "Site Reliability Engineering" was, I had a Grafana instance (using ye olde Telegraf and InfluxDB) showing me pretty graphs. I was exposed to the timeseries database paradigm, and how to query it in a useful way. And later down the line I integrated [Loki](https://grafana.com/docs/loki/latest/) to pull all my Docker container logs into my one pretty visualization platform. I built dashboards for monitoring host health, troubleshooting specific issues, statuspages; I built alert policies to send notifications via Discord and email; and I exported data from practically every service. And then things settled down, and that data-analytics muscle began to atrophy. My environment was stable. So I changed tact.
Uptime-kuma is simple, beautiful, and does all the things I need. HTTP and ping-based uptime monitoring, outage notifications (via email and Discord), and [status pages](https://uptime.jafner.tools/status/net).
All this lets me sleep easy knowing that if any of my services go down, I'll get a Discord notification. When someone asks me "Hey, is \<service\> down?" I can answer confidently, "Nope, works on my system."
# Closing thoughts, and looking forward
I've not written much about my lab before. It's been a challenge to resist rambling about all the challenges and iterations I walked through to get the lab to the state it's in today. And even just in the process of writing this the last couple days I've come to realize a few low-hanging fruit improvements I could make.
While I'm proud of all the things I've tought myself in this process, I could not have done it without hundreds of thousands of hours of freely-contributed projects, Q&As, documentation, tutorials, and every other kind of support.
In addition to the hundreds of individual project founders, maintainers, and contributers, I am deeply appreciative of the creators from whom inspiration flowed freely. I'm sure there's room here somewhere for an [`Appendix N: Inspirational Reading`](https://dungeonsdragons.fandom.com/wiki/Appendix_N) article.
---
Thanks for reading. If you want to contact me to chat about homelabbing, D&D, tech writing, gaming, A/V streaming tech, or because you think I can help you solve a problem, email me at [`joey@jafner.net`](mailto:joey@jafner.net) or use any of [my other socials]({{< ref "/" >}} "Homepage").

View File

@ -1,7 +0,0 @@
+++
title = 'Test Article Please Ignore'
date = 2024-05-28T22:10:00-07:00
draft = false
+++
# No Articles Yet, Check Back Later

View File

@ -2,7 +2,6 @@
title = 'About Me'
date = 2024-05-28T17:56:25-07:00
draft = false
url = '/about'
+++
My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.

View File

@ -2,5 +2,4 @@
title = 'Articles'
date = 2024-05-28T17:56:53-07:00
draft = false
url = '/articles'
+++

View File

@ -2,7 +2,6 @@
title = 'Experience'
date = 2024-05-28T17:56:47-07:00
draft = false
url = '/experience'
+++
Below is a reverse chronological listing of my employment history.

View File

@ -2,5 +2,4 @@
title = 'Projects'
date = 2024-05-28T17:56:41-07:00
draft = false
url = '/projects'
+++

View File

@ -1,5 +0,0 @@
+++
title = '5etools Docker'
date = 2024-05-28T17:58:14-07:00
draft = true
+++

View File

@ -1,5 +0,0 @@
+++
title = 'ClipIt.py'
date = 2024-05-28T17:58:27-07:00
draft = true
+++

View File

@ -0,0 +1 @@
{{ .Page.TableOfContents }}

View File

@ -23,16 +23,20 @@
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -74,7 +78,7 @@
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -93,7 +97,7 @@
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -145,7 +149,7 @@
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -158,7 +162,7 @@
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -25,16 +25,16 @@ Ive enjoyed hacking with whatever tech I could get my hands on since I was a
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -89,7 +89,7 @@ Ive enjoyed hacking with whatever tech I could get my hands on since I was a
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -108,7 +108,7 @@ Ive enjoyed hacking with whatever tech I could get my hands on since I was a
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -175,7 +175,7 @@ Ive enjoyed hacking with whatever tech I could get my hands on since I was a
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -188,7 +188,7 @@ Ive enjoyed hacking with whatever tech I could get my hands on since I was a
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="There are many like it&amp;hellip; But this lab is built by me and for me. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I&amp;rsquo;ve gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I&amp;rsquo;ve gotta configure the SMTP notifications, the DNS, the firewalls and the subnets. I open, update, and close issues, remediate leaked secrets, and write documentation." />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/articles/homelab-tour-series-intro/" />
<title>
Homelab Tour Series - Intro :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Homelab Tour Series - Intro">
<meta itemprop="description" content="There are many like it&hellip; But this lab is built by me and for me. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I&rsquo;ve gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I&rsquo;ve gotta configure the SMTP notifications, the DNS, the firewalls and the subnets. I open, update, and close issues, remediate leaked secrets, and write documentation."><meta itemprop="datePublished" content="2024-06-27T09:15:13-07:00" />
<meta itemprop="dateModified" content="2024-06-27T09:15:13-07:00" />
<meta itemprop="wordCount" content="2990"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Homelab Tour Series - Intro"/>
<meta name="twitter:description" content="There are many like it&hellip; But this lab is built by me and for me. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I&rsquo;ve gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I&rsquo;ve gotta configure the SMTP notifications, the DNS, the firewalls and the subnets. I open, update, and close issues, remediate leaked secrets, and write documentation."/>
<meta property="og:title" content="Homelab Tour Series - Intro" />
<meta property="og:description" content="There are many like it&hellip; But this lab is built by me and for me. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I&rsquo;ve gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I&rsquo;ve gotta configure the SMTP notifications, the DNS, the firewalls and the subnets. I open, update, and close issues, remediate leaked secrets, and write documentation." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/articles/homelab-tour-series-intro/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="articles" />
<meta property="article:published_time" content="2024-06-27T09:15:13-07:00" />
<meta property="article:modified_time" content="2024-06-27T09:15:13-07:00" />
<meta property="article:published_time" content="2024-06-27 09:15:13 -0700 PDT" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="post">
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/articles/homelab-tour-series-intro/">Homelab Tour Series - Intro</a></h2>
<div class="post-content">
<h1 id="there-are-many-like-it">There are many like it&hellip;</h1>
<p>But this lab is built <em>by me and for me</em>. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I&rsquo;ve gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I&rsquo;ve gotta configure the SMTP notifications, the <a href="https://xkcd.com/2259/">DNS</a>, the firewalls and the subnets. I open, update, and close <a href="https://gitea.jafner.tools/Jafner/homelab/issues">issues</a>, <a href="https://gitea.jafner.tools/Jafner/homelab/issues/128">remediate leaked secrets</a>, and write documentation.</p>
<p>It&rsquo;s exhausting and exhilarating, frustrating and fulfilling, thankless and thankful. And I&rsquo;ve never written about it before, so here&rsquo;s me finally getting to do that.</p>
<nav id="TableOfContents">
<ul>
<li><a href="#there-are-many-like-it">There are many like it&hellip;</a></li>
<li><a href="#core-services-its-about-data">Core Services: It&rsquo;s about data.</a>
<ul>
<li><a href="#bitwarden-the-best-password-manager">Bitwarden: The best password manager</a></li>
<li><a href="#gitea-control-the-source">Gitea: Control the source</a></li>
<li><a href="#nextcloud-corner-office-with-a-view">Nextcloud: Corner office with a view</a></li>
<li><a href="#manyfold-library-management-for-3d-models">Manyfold: Library management for 3D models</a></li>
<li><a href="#send-the-service-formerly-known-as-firefox-send">Send: The service formerly known as Firefox Send</a></li>
<li><a href="#zipline-clip-that">Zipline: Clip that</a></li>
<li><a href="#home-assistant-climate-control-for-the-critter-cove">Home Assistant: Climate control for the Critter Cove</a></li>
<li><a href="#vyos-my-router-is-a-text-file">VyOS: My router is a text file</a></li>
<li><a href="#truenas-how-i-sleep-at-night">TrueNAS: How I sleep at night</a></li>
</ul>
</li>
<li><a href="#additional-services-just-for-fun">Additional Services: Just for fun</a>
<ul>
<li><a href="#plex-i-wish-jellyfin-supported-sso">Plex: I wish Jellyfin supported SSO</a></li>
<li><a href="#5etools-if-dd-beyond-was-designed-by-software-engineers-instead-of-business-boys">5eTools: If D&amp;D Beyond was designed by software engineers instead of business boys</a></li>
<li><a href="#calibre-web-frontend-for-the-premiere-ebook-library-manager">Calibre-web: Frontend for the premiere ebook library manager</a></li>
<li><a href="#minecraft-geoff-itzg-bourne-is-a-blessing">Minecraft: Geoff &quot;itzg&quot; Bourne is a blessing</a></li>
</ul>
</li>
<li><a href="#admin-services-help-me-handle-all-this">Admin Services: Help me handle all this</a>
<ul>
<li><a href="#traefik-one-liner-tls-certified-subdomains">Traefik: One-liner* TLS-certified subdomains</a></li>
<li><a href="#keycloak-sign-in-to-jafnernet">Keycloak: Sign in to Jafner.net</a></li>
<li><a href="#wireguard-road-warrior-who-needs-to-reboot-the-minecraft-server">Wireguard: Road warrior who needs to reboot the Minecraft server</a></li>
<li><a href="#grafana-prometheus-and-uptime-kuma-observability-before-i-knew-what-that-meant">Grafana, Prometheus, and Uptime-kuma: Observability before I knew what that meant</a></li>
</ul>
</li>
<li><a href="#closing-thoughts-and-looking-forward">Closing thoughts, and looking forward</a></li>
</ul>
</nav>
<h1 id="core-services-its-about-data">Core Services: It&rsquo;s about data.</h1>
<p><a href="https://www.npr.org/2021/04/09/986005820/after-data-breach-exposes-530-million-facebook-says-it-will-not-notify-users">For</a> <a href="https://www.forbes.com/sites/quickerbettertech/2021/07/05/a-linkedin-breach-exposes-92-of-usersand-other-small-business-tech-news/">myriad</a> <a href="https://www.theverge.com/2018/5/3/17316684/twitter-password-bug-security-flaw-exposed-change-now">reasons</a>, <a href="https://www.techtarget.com/whatis/feature/SolarWinds-hack-explained-Everything-you-need-to-know">I want</a> to <a href="https://www.bbc.com/news/technology-37232635">maintain</a> as much <a href="https://www.theverge.com/2022/12/22/23523322/lastpass-data-breach-cloud-encrypted-password-vault-hackers">control</a> of <a href="https://www.bbc.com/news/technology-58817658">my data</a> <a href="https://cloudsecurityalliance.org/blog/2022/03/13/an-analysis-of-the-2020-zoom-breach">as possible</a>. So I bought hard drives to store my data, and built computers around those hard drives to move my data to and fro. Lastly, I selected a few of the awesome projects others have built to tell the computers how to move my data.</p>
<p><img src="../Apps.png" alt="Apps.png|App icons left-to-right: Bitwarden, Gitea, Nextcloud, Zipline, Send, Home Assistant, TrueNAS Scale, VyOS"></p>
<p>Veteran self-hosters are familiar with many of these, but I want to talk about how each of these projects helps me claw back a little bit of control over my data.</p>
<h2 id="bitwarden-the-best-password-manager">Bitwarden: The best password manager</h2>
<p><em>Password management server.</em> | <a href="https://bitwarden.com/">Bitwarden</a> (<a href="https://github.com/dani-garcia/vaultwarden">Vaultwarden</a>) | <a href="https://bitwarden.jafner.tools"><code>bitwarden.jafner.tools</code></a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/druid/config/vaultwarden/docker-compose.yml">Configuration</a></p>
<p>I used to use LastPass.</p>
<ul>
<li><em>June 15, 2015</em> <a href="https://arstechnica.com/information-technology/2015/06/hack-of-cloud-based-lastpass-exposes-encrypted-master-passwords/">ArsTechnica: Hack of cloud-based LastPass exposes hashed master passwords</a></li>
<li><em>December 28, 2021</em> <a href="https://www.bleepingcomputer.com/news/security/lastpass-users-warned-their-master-passwords-are-compromised/">BleepingComputer: LastPass users warned their master passwords are compromised</a></li>
<li><em>November 30, 2022</em> <a href="https://www.bleepingcomputer.com/news/security/lastpass-says-hackers-accessed-customer-data-in-new-breach/">BleepingComputer: Lastpass says hackers accessed customer data in new breach</a></li>
</ul>
<p>Bitwarden has an <a href="https://bitwarden.com/blog/third-party-security-audit/">excellent security track record <em>so far</em></a>. But two factors (ha!) led to my choosing to self-host the community-built server instead of using Bitwarden&rsquo;s first-party cloud service:</p>
<ol>
<li>Subscription-gated features. 2FA/OTP authenticator, file attachments, security reports, and more are gated behind a subscription. I wholeheartedly endorse Bitwarden&rsquo;s decision, but that&rsquo;s just enough encouragement for me to host it myself.</li>
<li>Bigger means more attractive target. The more people put their trust in Bitwarden, the more attractive a target it becomes. My personal server is unlikely to attract the attention of any individuals or organizations with the capability to penetrate <em>Bitwarden&rsquo;s</em> security. Of course, that only matters if I maintain good security posture everywhere else in the homelab. More on that another day.</li>
</ol>
<p>I&rsquo;ve had an excellent experience with Bitwarden so far. The user experience is fluid enough that I was able to onboard my family without issue.</p>
<h2 id="gitea-control-the-source">Gitea: Control the source</h2>
<p><em>Source control and basic CI/CD.</em> | <a href="https://about.gitea.com/">Gitea</a> | <a href="https://gitea.jafner.tools/"><code>gitea.jafner.tools</code></a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/druid/config/gitea">Configuration</a></p>
<p>GitHub is great. And I often question my decision to host my own Git and CI/CD server. I&rsquo;m not foolish enough to worry that any of my code would be included in any high-quality AI training datasets. Really, In</p>
<p>I started out using a self-hosted GitLab instance, but its power and flexibility entail weight. And I got tired of the administrative toil caused by frequent and substantial updates to the entire platform.</p>
<p>So I spun up Gitea, set up some <a href="https://docs.gitea.com/next/usage/actions/act-runner/">runners</a>, and got back to work.</p>
<h2 id="nextcloud-corner-office-with-a-view">Nextcloud: Corner office with a view</h2>
<p><em>Office services (files, photos, calendar, contacts).</em> | <a href="https://nextcloud.com/">Nextcloud</a> | <a href="https://nextcloud.jafner.net"><code>nextcloud.jafner.net</code></a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/nextcloud">Configuration</a></p>
<p>Google Drive is pretty cool and useful. I didn&rsquo;t care much for Docs and the other office products, but the storage, sharing, and sync functionality Drive provided was useful at school, at home, at work, and even for gaming. But it&rsquo;s hard-limited to 15 GB on the free version. And expanding that costs ~$5/TB mo., which is only $0.63 cheaper than buying an 8TB SAS HDD <em>every month</em>. So I decided <code>drives &gt; Drive</code>.</p>
<p>And it feels luxurious. Knowing that all my phone&rsquo;s photos and videos are stored on my hardware and won&rsquo;t every touch Google&rsquo;s servers. I can take a full-fat video without worrying that 600 MB will eat up a bunch of my quota. Nextcloud also offers a wide swathe of plugins for other functionalities via their <a href="https://apps.nextcloud.com/">App store</a>.</p>
<h2 id="manyfold-library-management-for-3d-models">Manyfold: Library management for 3D models</h2>
<p><em>3D Model library manager.</em> | <a href="https://github.com/manyfold3d/manyfold">Manyfold</a> | <a href="https://3d.jafner.net/"><code>3d.jafner.net</code></a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/vandam">Configuration</a></p>
<p>Over the years I&rsquo;ve spent a <em>lot</em> of money on 3D models for fantasy RPG miniatures. My collection of models is measured in <em>terrabytes</em>. And I have a deep appreciation for the creative work of the artists who make these models. But artists, I think, are not naturally inclined toward robust file organization practices. So my &ldquo;collection&rdquo; is really more like a pile.</p>
<p>But Manyfold (formerly &ldquo;VanDAM&rdquo;) has helped enormously with the process of making that collection usable. Search and preview are the two biggest features. Automated tagging and other organizational features also help a lot.</p>
<p>I still have a lot of manual work to do though&hellip;</p>
<h2 id="send-the-service-formerly-known-as-firefox-send">Send: The service formerly known as Firefox Send</h2>
<p><em>Quick and secure file share. <a href="https://xkcd.com/949/">XKCD#949</a>.</em> | <a href="https://github.com/timvisee/send">Send</a> | <a href="https://send.jafner.net/"><code>send.jafner.net</code></a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/send">Configuration</a></p>
<p>The XKCD comic above articulates and experience I&rsquo;ve had enough times. Nextcloud helps a lot when I want to share a file with someone else. But Send covers the cases where a friend wants to send me a file, or a friend is asking me how to send a file to another friend. I can just send them the link. I don&rsquo;t even need to explain how it works, it&rsquo;s built intuitively enough. And I get some peace of mind knowing that the files are encrypted end-to-end.</p>
<h2 id="zipline-clip-that">Zipline: Clip that</h2>
<p><em>Media sharing server (upload screenshots, recordings).</em> | <a href="https://github.com/diced/zipline">Zipline</a> | <a href="https://zipline.jafner.net"><code>zipline.jafner.net</code></a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/zipline">Configuration</a></p>
<p>This service exists exclusively to let me right click the video file of a gaming highlight, hit &ldquo;Share&rdquo;, and send the link to my friends as seamlessly as possible <em>while supporting high-fidelity content</em>. I record my gameplay at 1440p 120 FPS. Check it out:</p>
<ul>
<li><a href="https://zipline.jafner.net/u/%5B2024-06-19%5D%20Neato.mp4">Venture 5k</a>.</li>
<li><a href="https://zipline.jafner.net/u/%5B2024-06-19%5D%20Scrim%20(1:55:22-1:56:02).mp4">Venture scrim highlight</a></li>
<li><a href="https://zipline.jafner.net/u/ImYtUX.mp4">My greatest widowmaker highlight</a></li>
</ul>
<p>That last one&rsquo;s not even 120 fps, I just love to show it. To be honest though, if Youtube supported scripted/automated uploads I would just use that. But it&rsquo;s probably for the best that they don&rsquo;t.</p>
<h2 id="home-assistant-climate-control-for-the-critter-cove">Home Assistant: Climate control for the Critter Cove</h2>
<p><em>Home automation and monitoring.</em> | <a href="https://www.home-assistant.io/">Home Assistant</a> | <a href="https://homeassistant.jafner.net"><code>homeassistant.jafner.net</code></a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/fighter/config/home-assistant">Configuration</a></p>
<p>I think a lot of folks start their self-hosting journey with Home Assistant. It&rsquo;s a fantastic tool, and it keeps some of your most important data from being reliant on <a href="https://gizmodo.com/the-never-ending-death-of-smart-home-gadgets-1842456125">often-flakey vendors</a> who have little interest in supporting their product unless you&rsquo;re giving them money for it.</p>
<p>My partner and I have four reptiles, several insects, and a hamster whose climates I simply cannot be bothered to adjust manually every hour of the waking day. So I installed Home Assistant, hooked up warm-side and cool-side <a href="https://us.govee.com/products/govee-bluetooth-hygrometer-thermometer-h5075?Style=1*H5075">Govee hygrometer/thermometers</a>, put the heating lamps on <a href="https://www.kasasmart.com/us/products/smart-plugs/product-kp405">TP Link smart dimming plugs</a>, and wrote some automation to keep everybody in their happy temperature range.</p>
<h2 id="vyos-my-router-is-a-text-file">VyOS: My router is a text file</h2>
<p><em>Configuration-as-code router OS.</em> | <a href="https://vyos.io/">VyOS</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/wizard/config">Configuration</a></p>
<p>One day I thought to myself, &ldquo;do I know how a router works?&rdquo; The answer was no, so I built one (hardware and configuration) from scratch between the hours of 10 PM and 4 AM to ensure none of my housemates would be disturbed by the requisite internet outage. I deployed the seat-of-my-pants router configuration to &ldquo;production&rdquo; overnight and handled about one day&rsquo;s worth of post-deployment support before everything was seamlessly stable.</p>
<p>The hardest part was crimping and terminating every single ethernet cable. At least 20 connections. Woof.</p>
<h2 id="truenas-how-i-sleep-at-night">TrueNAS: How I sleep at night</h2>
<p><em>Data safety provider.</em> | <a href="https://www.truenas.com/truenas-scale/">TrueNAS</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/barbarian">Configuration: Main</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/monk">Configuration: Backup</a></p>
<p>Underpinning every single one of the above services in one way or another is my TrueNAS deployment. It is composed of two hosts: a primary and a backup. Every day, each system takes a <a href="https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/periodicsnapshottasksscreensscale/">differential snapshot</a> of each dataset, and runs a short <a href="https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/smarttestsscreensscale/">S.M.A.R.T. test</a> on each disk. Nightly, the most important datasets on the primary are backed up to the backup as an <a href="https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/rsynctasksscreensscale/">Rsync task</a>. And every week it runs a <a href="https://www.truenas.com/docs/scale/24.04/scaleuireference/dataprotection/scrubtasksscreensscale/">scrub task</a> to ensure the stored data still checksums correctly.</p>
<p>And of course, it runs an SMB server and iSCSI target to facilitate clients and applications interacting with their own little data puddle.</p>
<h1 id="additional-services-just-for-fun">Additional Services: Just for fun</h1>
<p>In addition to the important services above, I run a handful of services just for off time.</p>
<h2 id="plex-i-wish-jellyfin-supported-sso">Plex: I wish Jellyfin supported SSO</h2>
<p>The superior media server, Plex provides a free (as in beer) solution with a beautiful frontend and comprehensive metadata scraping. Plex has set a standard for serving movies and TV that no alternative service has been able to match. It is the unwelcome incumbent no one has mustered the resources to dethrone. Over the last dthe usernames-emails-passwords) in 2022.</p>
<p>But I digress&hellip;</p>
<p><a href="https://jellyfin.org/">Jellyfin</a> is the leading opposition. But Jellyfin&rsquo;s <a href="https://features.jellyfin.org/?view=most-wanted">most wanted feature requests</a> paints a sorry picture of the state of things. Features I consider critical, such as <a href="https://features.jellyfin.org/posts/26/add-support-for-two-factor-authentication-2fa">2FA</a>, <a href="https://features.jellyfin.org/posts/284/convert-option-similar-to-plexs-optimise">transcoding</a>, <a href="https://features.jellyfin.org/posts/218/support-offline-mode-on-android-mobile">offline access for mobile clients</a>, <a href="https://features.jellyfin.org/posts/576/watchlist-like-netflix">to-watch lists</a>, <a href="https://features.jellyfin.org/posts/633/watched-history">watch history</a>, <a href="https://features.jellyfin.org/posts/230/support-for-oidc">OIDC support</a> / <a href="https://features.jellyfin.org/posts/271/oauth-support">OAuth support</a>, <a href="https://features.jellyfin.org/posts/540/list-all-collections-that-a-movie-belong-to-in-movie-details">list collections to which a movie belongs</a>, and more I&rsquo;m sure if I just keep scrolling. Maybe a few more years.</p>
<h2 id="5etools-if-dd-beyond-was-designed-by-software-engineers-instead-of-business-boys">5eTools: If D&amp;D Beyond was designed by software engineers instead of business boys</h2>
<p>5eTools is an insanely high quality, data-driven repository for each and every piece of content ever made for D&amp;D 5th edition. Fortunately, they have a <em>blocklist</em> feature to restrict the visible content to only the stuff I&rsquo;ve bought the books for.</p>
<h2 id="calibre-web-frontend-for-the-premiere-ebook-library-manager">Calibre-web: Frontend for the premiere ebook library manager</h2>
<p><a href="https://github.com/janeczku/calibre-web">Calibre-web</a> | <a href="https://calibre-ebook.com/">Calibre</a> | <a href="https://rpg.calibre.jafner.net/"><code>rpg.calibre.jafner.net</code></a> | <a href="https://sff.calibre.jafner.net/"><code>sff.calibre.jafner.net</code></a></p>
<p>Sometimes, rarely, I read a book. Much more freqeuently, I want to check a section from a book. Calibre(-web) affords me access to my entire library of books and ebooks from a web browser. Frustratingly, the owner of the project has decided not to implement generic OAuth2/OIDC support. <a href="https://github.com/janeczku/calibre-web/pull/2211#issuecomment-1182460156">But open-source, uh, finds a way.</a></p>
<h2 id="minecraft-geoff-itzg-bourne-is-a-blessing">Minecraft: Geoff &quot;itzg&quot; Bourne is a blessing</h2>
<p>Hosting game servers for my friends got me into this stuff, and has been a throughline of my life for over 15 years.
And Minecraft has been a consistent presence in that domain. Today that task is easier and more polished than ever.</p>
<p>Two Docker images, <a href="https://github.com/itzg/mc-router">itzg/mc-router</a> and <a href="https://github.com/itzg/docker-minecraft-server">itzg/docker-minecraft-server</a> have handled every Minecraft server I&rsquo;ve hosted since late 2020. The configuration is simple and declarative. The best part is the reverse proxying. In 2015 I would head to <a href="https://www.whatsmyip.org/">WhatsMyIP.org</a>, copy the number at the top, and send it to my friends. Then they would manually type it into the connect dialog (copy-paste can be challenging), type something wrong, get a connection error, and call me on Google Hangouts. We&rsquo;d, eventually get it figured out, but now I can just say &ldquo;It&rsquo;s <code>e9.jafner.net</code>&rdquo; and that seems to stick a lot better.</p>
<h1 id="admin-services-help-me-handle-all-this">Admin Services: Help me handle all this</h1>
<p>Nothing since has given me the same high as seeing the green padlock next to <code>jafner.net</code> for the first time. After years of typing IPs, memorizing ports, skipping &ldquo;Your connection is not private&rdquo; pages, and answering &ldquo;What&rsquo;s the IP again?&rdquo;, that green padlock was more gratifying than the $60k piece of paper framed on my wall.</p>
<p>Below are the tools and services I use to make everything else work <em>properly</em>.</p>
<h2 id="traefik-one-liner-tls-certified-subdomains">Traefik: One-liner* TLS-certified subdomains</h2>
<p><em>Docker-integrated reverse proxy.</em> | <a href="https://github.com/traefik/traefik">Traefik</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/traefik/docker-compose.yml">Configuration: Fighter</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/traefik/docker-compose.yml">Configuration: Druid</a></p>
<p>*Okay, it&rsquo;s not literally one line. It&rsquo;s five.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span><span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">web</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">traefik.http.routers.myservice.rule=Host(`myservice.jafner.net`)</span>
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">traefik.http.routers.myservice.tls.certresolver=lets-encrypt</span>
</span></span></code></pre></div><p>Lines #1-2 configure a service to attach to the Docker network Traefik is monitoring.
Lines #3-5 tell Traefik what (sub)domain(s) that service should serve, and tell it to provision a fresh LetsEncrypt certificate.</p>
<ul>
<li>No wildcard certs.</li>
<li>No manual cert requests.</li>
<li>No services are exposed by default.</li>
</ul>
<p>In addition to handling that core functionality, it also offers &ldquo;middlewares&rdquo; to handle additional functionality, like <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/traefik/config/config_addons.yaml#L49">forwardauth</a>, which helps me protect services that don&rsquo;t support <a href="https://oauth.net/2/">OAuth2</a>/<a href="https://www.microsoft.com/en-us/security/business/security-101/what-is-openid-connect-oidc">OIDC</a>, such as <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/wireguard/docker-compose.yml#L26">WG-Easy</a>.</p>
<p>I have never been tempted to look for alternatives. I struggle to imagine how a reverse proxy could be better for my use-case without overstepping its role.</p>
<p>Oh, it would be nice if it handled <a href="https://gitea.jafner.tools/Jafner/homelab/issues/71">dispatching to other Traefik instances</a> in a more intuitive/simple way. I haven&rsquo;t been able to get that working.</p>
<h2 id="keycloak-sign-in-to-jafnernet">Keycloak: Sign in to Jafner.net</h2>
<p><em>IAM provider</em> | <a href="https://www.keycloak.org/">Keycloak</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/keycloak">Configuration</a></p>
<p>It&rsquo;s easy to allow your password manager&rsquo;s &ldquo;local accounts&rdquo; folder slowly grow to dozens or hundreds of credentials as services are trialed, decomissioned, reinstalled, and troubleshooted (troubleshot?). Further, supporting basic account information updates for <em>users that aren&rsquo;t me</em> is non-trivial. How many SMTP submission API keys would I need to support each of my services sending their own &ldquo;Recover your account password&rdquo; emails?</p>
<p>Keycloak provides much-needed consolidation of account management. Just like a <em>real website</em>, anyone who wants to <em>do something</em> with one of my services (e.g. open a <a href="https://gitea.jafner.tools/issues">Gitea issue</a>) can walk through the familiar account creation process. Give a first and last name, email, username, and password. You&rsquo;ll get a verification email in your inbox from <code>&quot;Keycloak Admin&quot; &lt;noreply@jafner.net&gt;</code>. Click the link, go back to the page you were trying to access, and <em>boom</em>, you can do stuff. Stuff I don&rsquo;t want you to be able to do is still gated behind manual account approval. For example, you can&rsquo;t just create an account and start uploading files to Nextcloud. Oh, and it supports 2FA.</p>
<p>One day I&rsquo;ll be able to manage <em>every single Jafner.net application&rsquo;s</em> ID and access via Keycloak, but a few have held out. But that&rsquo;s an entire article itself.
Until then I&rsquo;ll be happy that <em>most</em> applications and services either support native OAuth2 or OIDC, or are single-user and can be simply gated behind Traefik forwardauth.</p>
<h2 id="wireguard-road-warrior-who-needs-to-reboot-the-minecraft-server">Wireguard: Road warrior who needs to reboot the Minecraft server</h2>
<p><em>Quick, easy, fast VPN plus quick, easy, fast web UI</em> | <a href="https://www.wireguard.com/">Wireguard</a> | <a href="https://github.com/wg-easy/wg-easy">WG-Easy</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/wireguard/docker-compose.yml">Configuration: Fighter</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/wireguard/docker-compose.yml">Configuration: Druid</a></p>
<p>Every homelabber is faced with the question of how to administrate their server when they aren&rsquo;t sitting (or standing) at their desk at home. It&rsquo;s a great question; you&rsquo;re dipping your toes into <a href="https://csrc.nist.gov/glossary/term/security_posture">security posture</a>, and the constant tension between security and ease-of-access. Using SSH keys instead of passwords is a no-brainer, but do you also configure <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-multi-factor-authentication-for-ssh-on-ubuntu-20-04">SSH 2FA</a>? Most folks don&rsquo;t allow SSH traffic directly through the router, but <em>how</em> do you build and configure your VPN for SSHing into your server?</p>
<p>The steps I take to secure my SSH hosts are detailed <a href="https://gitea.jafner.tools/Jafner/homelab/src/branch/main/docs/Security.md#securing-ssh">here</a>, but I&rsquo;ll be digging into that more in another article. In brief:</p>
<ul>
<li>SSH keys are matched to a user and host. E.g. <code>Joey_Desktop</code>, or <code>Joey_Phone</code>.</li>
<li>Authorized keys are added only as needed. There is no template. Nothing is included by default.</li>
<li>Each SSH server is configured to require pubkey authentication and disable password authentication.</li>
<li>Each SSH server is configured to require 2FA via the <code>google-authenticator</code> PAM module.</li>
<li>The router is not configured to port-forward any SSH traffic. Accessing SSH requires VPNing.</li>
</ul>
<p>Lots of iteration led to this configuration. I&rsquo;m sure I&rsquo;ll write it out at some point.</p>
<h2 id="grafana-prometheus-and-uptime-kuma-observability-before-i-knew-what-that-meant">Grafana, Prometheus, and Uptime-kuma: Observability before I knew what that meant</h2>
<p><a href="https://grafana.com/">Grafana</a> | <a href="https://prometheus.io/">Prometheus</a> | <a href="https://github.com/louislam/uptime-kuma">Uptime-kuma</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/monitoring">Configuration: Monitoring Fighter</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/fighter/config/uptime-kuma/docker-compose.yml">Configuration: Uptime-kuma Fighter</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/monitoring/docker-compose.yml">Configuration: Monitoring Druid</a> | <a href="https://gitea.jafner.tools/Jafner/homelab/src/commit/94e0aed892812adcfd94bd84e588eb474dd7abda/druid/config/uptime-kuma/docker-compose.yml">Configuration: Uptime-kuma Druid</a></p>
<p>Before I knew what &ldquo;Site Reliability Engineering&rdquo; was, I had a Grafana instance (using ye olde Telegraf and InfluxDB) showing me pretty graphs. I was exposed to the timeseries database paradigm, and how to query it in a useful way. And later down the line I integrated <a href="https://grafana.com/docs/loki/latest/">Loki</a> to pull all my Docker container logs into my one pretty visualization platform. I built dashboards for monitoring host health, troubleshooting specific issues, statuspages; I built alert policies to send notifications via Discord and email; and I exported data from practically every service. And then things settled down, and that data-analytics muscle began to atrophy. My environment was stable. So I changed tact.</p>
<p>Uptime-kuma is simple, beautiful, and does all the things I need. HTTP and ping-based uptime monitoring, outage notifications (via email and Discord), and <a href="https://uptime.jafner.tools/status/net">status pages</a>.</p>
<p>All this lets me sleep easy knowing that if any of my services go down, I&rsquo;ll get a Discord notification. When someone asks me &ldquo;Hey, is &lt;service&gt; down?&rdquo; I can answer confidently, &ldquo;Nope, works on my system.&rdquo;</p>
<h1 id="closing-thoughts-and-looking-forward">Closing thoughts, and looking forward</h1>
<p>I&rsquo;ve not written much about my lab before. It&rsquo;s been a challenge to resist rambling about all the challenges and iterations I walked through to get the lab to the state it&rsquo;s in today. And even just in the process of writing this the last couple days I&rsquo;ve come to realize a few low-hanging fruit improvements I could make.</p>
<p>While I&rsquo;m proud of all the things I&rsquo;ve tought myself in this process, I could not have done it without hundreds of thousands of hours of freely-contributed projects, Q&amp;As, documentation, tutorials, and every other kind of support.</p>
<p>In addition to the hundreds of individual project founders, maintainers, and contributers, I am deeply appreciative of the creators from whom inspiration flowed freely. I&rsquo;m sure there&rsquo;s room here somewhere for an <a href="https://dungeonsdragons.fandom.com/wiki/Appendix_N"><code>Appendix N: Inspirational Reading</code></a> article.</p>
<hr>
<p>Thanks for reading. If you want to contact me to chat about homelabbing, D&amp;D, tech writing, gaming, A/V streaming tech, or because you think I can help you solve a problem, email me at <a href="mailto:joey@jafner.net"><code>joey@jafner.net</code></a> or use any of <a href="https://jafner.dev/" title="Homepage">my other socials</a>.</p>
</div>
</article>
<hr />
<div class="post-info">
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -23,36 +23,35 @@
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Articles">
<meta itemprop="description" content=""><meta itemprop="datePublished" content="2024-05-28T17:56:53-07:00" />
<meta itemprop="dateModified" content="2024-05-28T17:56:53-07:00" />
<meta itemprop="wordCount" content="0"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta itemprop="description" content="Personal site for Joey Hafner">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Articles"/>
<meta name="twitter:description" content=""/>
<meta name="twitter:description" content="Personal site for Joey Hafner"/>
<meta property="og:title" content="Articles" />
<meta property="og:description" content="" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/articles/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="page" />
<meta property="article:published_time" content="2024-05-28T17:56:53-07:00" />
<meta property="article:modified_time" content="2024-05-28T17:56:53-07:00" />
<meta property="og:description" content="Personal site for Joey Hafner" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://jafner.dev/articles/" /><meta property="og:image" content="https://jafner.dev" />
@ -60,10 +59,9 @@
<meta property="article:published_time" content="2024-05-28 17:56:53 -0700 PDT" />
<link rel="alternate" type="application/rss+xml" href="https://jafner.dev/articles/index.xml" title="Jafner.dev" />
@ -81,7 +79,7 @@
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -100,7 +98,7 @@
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -118,31 +116,40 @@
<div class="content">
<main class="post">
<main class="posts">
<h1>Articles</h1>
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/articles/">Articles</a></h2>
<div class="posts-group">
<div class="post-year">2024</div>
<div class="post-content">
<ul class="posts-list">
<li class="post-item">
<a href="https://jafner.dev/articles/homelab-tour-series-intro/" class="post-item-inner">
<span class="post-title">Homelab Tour Series - Intro</span>
<span class="post-day">
Jun 27
</span>
</a>
</li>
</ul>
</div>
</article>
<div class="pagination">
<div class="pagination__buttons">
</div>
</div>
<hr />
<div class="post-info">
</div>
</main>
</div>
@ -163,7 +170,7 @@
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -176,7 +183,7 @@
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Articles on Jafner.dev</title>
<link>https://jafner.dev/articles/</link>
<description>Recent content in Articles on Jafner.dev</description>
<generator>Hugo -- gohugo.io</generator>
<language>en</language>
<copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright>
<lastBuildDate>Thu, 27 Jun 2024 09:15:13 -0700</lastBuildDate>
<atom:link href="https://jafner.dev/articles/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>Homelab Tour Series - Intro</title>
<link>https://jafner.dev/articles/homelab-tour-series-intro/</link>
<pubDate>Thu, 27 Jun 2024 09:15:13 -0700</pubDate>
<guid>https://jafner.dev/articles/homelab-tour-series-intro/</guid>
<description>There are many like it&amp;hellip; But this lab is built by me and for me. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I&amp;rsquo;ve gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I&amp;rsquo;ve gotta configure the SMTP notifications, the DNS, the firewalls and the subnets. I open, update, and close issues, remediate leaked secrets, and write documentation.</description>
</item>
</channel>
</rss>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>https://jafner.dev/articles/</title>
<link rel="canonical" href="https://jafner.dev/articles/">
<meta name="robots" content="noindex">
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=https://jafner.dev/articles/">
</head>
</html>

View File

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="No Articles Yet, Check Back Later " />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/articles/test-article-please-ignore/" />
<title>
Test Article Please Ignore :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Test Article Please Ignore">
<meta itemprop="description" content="No Articles Yet, Check Back Later "><meta itemprop="datePublished" content="2024-05-28T22:10:00-07:00" />
<meta itemprop="dateModified" content="2024-05-28T22:10:00-07:00" />
<meta itemprop="wordCount" content="6"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Test Article Please Ignore"/>
<meta name="twitter:description" content="No Articles Yet, Check Back Later "/>
<meta property="og:title" content="Test Article Please Ignore" />
<meta property="og:description" content="No Articles Yet, Check Back Later " />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/articles/test-article-please-ignore/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="articles" />
<meta property="article:published_time" content="2024-05-28T22:10:00-07:00" />
<meta property="article:modified_time" content="2024-05-28T22:10:00-07:00" />
<meta property="article:published_time" content="2024-05-28 22:10:00 -0700 PDT" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="post">
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/articles/test-article-please-ignore/">Test Article Please Ignore</a></h2>
<div class="post-content">
<h1 id="no-articles-yet-check-back-later">No Articles Yet, Check Back Later</h1>
</div>
</article>
<hr />
<div class="post-info">
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -23,16 +23,20 @@
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -75,7 +79,7 @@
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -94,7 +98,7 @@
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -147,7 +151,7 @@
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -160,7 +164,7 @@
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -0,0 +1,12 @@
nav a:link {
text-decoration: none;
}
nav a:visited {
text-decoration: none;
}
nav a:hover {
text-decoration: underline;
}
nav a:active {
text-decoration: none;
}

View File

@ -25,16 +25,16 @@ Spearheaded implementation of Bamboo pipelines to automate deployment of New Rel
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -89,7 +89,7 @@ Spearheaded implementation of Bamboo pipelines to automate deployment of New Rel
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -108,7 +108,7 @@ Spearheaded implementation of Bamboo pipelines to automate deployment of New Rel
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -188,7 +188,7 @@ Spearheaded implementation of Bamboo pipelines to automate deployment of New Rel
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -201,7 +201,7 @@ Spearheaded implementation of Bamboo pipelines to automate deployment of New Rel
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -24,16 +24,20 @@
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -76,7 +80,7 @@
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -95,7 +99,7 @@
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -145,7 +149,7 @@
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -158,7 +162,7 @@
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -7,27 +7,48 @@
<generator>Hugo -- gohugo.io</generator>
<language>en</language>
<copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright>
<lastBuildDate>Tue, 28 May 2024 17:56:53 -0700</lastBuildDate>
<lastBuildDate>Thu, 27 Jun 2024 09:15:13 -0700</lastBuildDate>
<atom:link href="https://jafner.dev/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>Homelab Tour Series - Intro</title>
<link>https://jafner.dev/articles/homelab-tour-series-intro/</link>
<pubDate>Thu, 27 Jun 2024 09:15:13 -0700</pubDate>
<guid>https://jafner.dev/articles/homelab-tour-series-intro/</guid>
<description>There are many like it&amp;hellip; But this lab is built by me and for me. Just as I am the sole (or primary) beneficiary of its value, I am also the sole owner. I&amp;rsquo;ve gotta pay for all the hard drives, the network switches, the API keys, the power supplies, the rack rails. I&amp;rsquo;ve gotta configure the SMTP notifications, the DNS, the firewalls and the subnets. I open, update, and close issues, remediate leaked secrets, and write documentation.</description>
</item>
<item>
<title>Pamidi - Control PulseAudio with a MIDI device</title>
<link>https://jafner.dev/projects/pamidi/</link>
<pubDate>Tue, 28 May 2024 17:58:19 -0700</pubDate>
<guid>https://jafner.dev/projects/pamidi/</guid>
<description>My first project posted publicly. Initially I didn&amp;rsquo;t expect to post it, but I didn&amp;rsquo;t see any other solutions to the problem I was having, so I figured my solution could inspire someone else to do it better.&#xA;Problem: Physical Volume Knobs for Applications While using Windows, I installed an application called MIDI Mixer to map the physical volume knobs of my Behringer X-Touch Mini (and the accompanying mute and media control buttons) to individual applications on my PC.</description>
</item>
<item>
<title>Articles</title>
<link>https://jafner.dev/articles/</link>
<link>https://jafner.dev/page/articles/</link>
<pubDate>Tue, 28 May 2024 17:56:53 -0700</pubDate>
<guid>https://jafner.dev/articles/</guid>
<guid>https://jafner.dev/page/articles/</guid>
<description></description>
</item>
<item>
<title>Experience</title>
<link>https://jafner.dev/experience/</link>
<link>https://jafner.dev/page/experience/</link>
<pubDate>Tue, 28 May 2024 17:56:47 -0700</pubDate>
<guid>https://jafner.dev/experience/</guid>
<guid>https://jafner.dev/page/experience/</guid>
<description>Below is a reverse chronological listing of my employment history.&#xA;[2021-11 to 2024-02] Site Reliability Engineer @ American Eagle From November 2021 through February 2024, I worked on a long-term contract with American Eagle to design and implement observability tooling for cloud infrastructure and apps.&#xA;Spearheaded implementation of Bamboo pipelines to automate deployment of New Relic/Terraform observability codebase. That reduced mean-time-to-deployment for our commits by more than 10x. Increased deployment frequency from ~weekly to ~twice daily.</description>
</item>
<item>
<title>Projects</title>
<link>https://jafner.dev/page/projects/</link>
<pubDate>Tue, 28 May 2024 17:56:41 -0700</pubDate>
<guid>https://jafner.dev/page/projects/</guid>
<description></description>
</item>
<item>
<title>About Me</title>
<link>https://jafner.dev/about/</link>
<link>https://jafner.dev/page/about/</link>
<pubDate>Tue, 28 May 2024 17:56:25 -0700</pubDate>
<guid>https://jafner.dev/about/</guid>
<guid>https://jafner.dev/page/about/</guid>
<description>My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.&#xA;Im always working on a diverse array of tech projects. Whether its my homelab, or spending 3 hours wrestling with a script to save me 30 seconds.&#xA;Ive enjoyed hacking with whatever tech I could get my hands on since I was a teen, and that interest was overclocked in 2019 when I found Docker and started my homelab.</description>
</item>
</channel>

View File

@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.
Im always working on a diverse array of tech projects. Whether its my homelab, or spending 3 hours wrestling with a script to save me 30 seconds.
Ive enjoyed hacking with whatever tech I could get my hands on since I was a teen, and that interest was overclocked in 2019 when I found Docker and started my homelab." />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/page/about/" />
<title>
About Me :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="About Me">
<meta itemprop="description" content="My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.
Im always working on a diverse array of tech projects. Whether its my homelab, or spending 3 hours wrestling with a script to save me 30 seconds.
Ive enjoyed hacking with whatever tech I could get my hands on since I was a teen, and that interest was overclocked in 2019 when I found Docker and started my homelab."><meta itemprop="datePublished" content="2024-05-28T17:56:25-07:00" />
<meta itemprop="dateModified" content="2024-05-28T17:56:25-07:00" />
<meta itemprop="wordCount" content="85"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="About Me"/>
<meta name="twitter:description" content="My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.
Im always working on a diverse array of tech projects. Whether its my homelab, or spending 3 hours wrestling with a script to save me 30 seconds.
Ive enjoyed hacking with whatever tech I could get my hands on since I was a teen, and that interest was overclocked in 2019 when I found Docker and started my homelab."/>
<meta property="og:title" content="About Me" />
<meta property="og:description" content="My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.
Im always working on a diverse array of tech projects. Whether its my homelab, or spending 3 hours wrestling with a script to save me 30 seconds.
Ive enjoyed hacking with whatever tech I could get my hands on since I was a teen, and that interest was overclocked in 2019 when I found Docker and started my homelab." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/page/about/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="page" />
<meta property="article:published_time" content="2024-05-28T17:56:25-07:00" />
<meta property="article:modified_time" content="2024-05-28T17:56:25-07:00" />
<meta property="article:published_time" content="2024-05-28 17:56:25 -0700 PDT" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="post">
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/page/about/">About Me</a></h2>
<div class="post-content">
<p>My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.</p>
<p>Im always working on a diverse array of tech projects. Whether its my homelab, or spending 3 hours wrestling with a script to save me 30 seconds.</p>
<p>Ive enjoyed hacking with whatever tech I could get my hands on since I was a teen, and that interest was overclocked in 2019 when I found Docker and started my homelab.</p>
<p>Im always excited about opportunities to learn new technologies and build useful things.</p>
</div>
</article>
<hr />
<div class="post-info">
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="" />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/page/articles/" />
<title>
Articles :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Articles">
<meta itemprop="description" content=""><meta itemprop="datePublished" content="2024-05-28T17:56:53-07:00" />
<meta itemprop="dateModified" content="2024-05-28T17:56:53-07:00" />
<meta itemprop="wordCount" content="0"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Articles"/>
<meta name="twitter:description" content=""/>
<meta property="og:title" content="Articles" />
<meta property="og:description" content="" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/page/articles/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="page" />
<meta property="article:published_time" content="2024-05-28T17:56:53-07:00" />
<meta property="article:modified_time" content="2024-05-28T17:56:53-07:00" />
<meta property="article:published_time" content="2024-05-28 17:56:53 -0700 PDT" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="post">
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/page/articles/">Articles</a></h2>
<div class="post-content">
</div>
</article>
<hr />
<div class="post-info">
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="Below is a reverse chronological listing of my employment history.
[2021-11 to 2024-02] Site Reliability Engineer @ American Eagle From November 2021 through February 2024, I worked on a long-term contract with American Eagle to design and implement observability tooling for cloud infrastructure and apps.
Spearheaded implementation of Bamboo pipelines to automate deployment of New Relic/Terraform observability codebase. That reduced mean-time-to-deployment for our commits by more than 10x. Increased deployment frequency from ~weekly to ~twice daily." />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/page/experience/" />
<title>
Experience :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Experience">
<meta itemprop="description" content="Below is a reverse chronological listing of my employment history.
[2021-11 to 2024-02] Site Reliability Engineer @ American Eagle From November 2021 through February 2024, I worked on a long-term contract with American Eagle to design and implement observability tooling for cloud infrastructure and apps.
Spearheaded implementation of Bamboo pipelines to automate deployment of New Relic/Terraform observability codebase. That reduced mean-time-to-deployment for our commits by more than 10x. Increased deployment frequency from ~weekly to ~twice daily."><meta itemprop="datePublished" content="2024-05-28T17:56:47-07:00" />
<meta itemprop="dateModified" content="2024-05-28T17:56:47-07:00" />
<meta itemprop="wordCount" content="217"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Experience"/>
<meta name="twitter:description" content="Below is a reverse chronological listing of my employment history.
[2021-11 to 2024-02] Site Reliability Engineer @ American Eagle From November 2021 through February 2024, I worked on a long-term contract with American Eagle to design and implement observability tooling for cloud infrastructure and apps.
Spearheaded implementation of Bamboo pipelines to automate deployment of New Relic/Terraform observability codebase. That reduced mean-time-to-deployment for our commits by more than 10x. Increased deployment frequency from ~weekly to ~twice daily."/>
<meta property="og:title" content="Experience" />
<meta property="og:description" content="Below is a reverse chronological listing of my employment history.
[2021-11 to 2024-02] Site Reliability Engineer @ American Eagle From November 2021 through February 2024, I worked on a long-term contract with American Eagle to design and implement observability tooling for cloud infrastructure and apps.
Spearheaded implementation of Bamboo pipelines to automate deployment of New Relic/Terraform observability codebase. That reduced mean-time-to-deployment for our commits by more than 10x. Increased deployment frequency from ~weekly to ~twice daily." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/page/experience/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="page" />
<meta property="article:published_time" content="2024-05-28T17:56:47-07:00" />
<meta property="article:modified_time" content="2024-05-28T17:56:47-07:00" />
<meta property="article:published_time" content="2024-05-28 17:56:47 -0700 PDT" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="post">
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/page/experience/">Experience</a></h2>
<div class="post-content">
<p>Below is a reverse chronological listing of my employment history.</p>
<h2 id="2021-11-to-2024-02-site-reliability-engineer--american-eagle">[2021-11 to 2024-02] Site Reliability Engineer @ American Eagle</h2>
<p>From November 2021 through February 2024, I worked on a long-term contract with American Eagle to design and implement observability tooling for cloud infrastructure and apps.</p>
<ul>
<li>Spearheaded implementation of Bamboo pipelines to automate deployment of New Relic/Terraform observability codebase.
<ul>
<li>That reduced mean-time-to-deployment for our commits by more than 10x.</li>
<li>Increased deployment frequency from ~weekly to ~twice daily.</li>
</ul>
</li>
<li>Collaborated with internal development teams to design human-optimized observability and alerting toolkits.</li>
<li>Authored and maintained documentation for our New Relic/Terraform codebase.</li>
</ul>
<h2 id="2021-08-to-2021-11-devops-engineer--upwork-contractor">[2021-08 to 2021-11] DevOps Engineer @ UpWork Contractor</h2>
<p>From August through November 2021, I collaborated with solo- and small-team projects to build automation pipelines, guide offshore software development teams toward client needs, and secure cloud resources to improve security posture. I also implemented data resilience and disaster recovery protocols with attention to ease-of-use and documentation. If you have an UpWork client account, you can view my engineering profile here.</p>
<h2 id="2021-03-to-2021-08-technical-writer--upwork-contractor">[2021-03 to 2021-08] Technical Writer @ UpWork Contractor</h2>
<p>From March through August 2021, I wrote technical documentation and articles for clients on UpWork. I loved working with open-source project developers to refine and articulate their vision. If you have an UpWork client account, you can view my technical writing profile here.</p>
</div>
</article>
<hr />
<div class="post-info">
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -23,16 +23,20 @@
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -75,7 +79,7 @@
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -94,7 +98,7 @@
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -126,7 +130,7 @@
<ul class="posts-list">
<li class="post-item">
<a href="https://jafner.dev/articles/" class="post-item-inner">
<a href="https://jafner.dev/page/articles/" class="post-item-inner">
<span class="post-title">Articles</span>
<span class="post-day">
@ -137,7 +141,7 @@
</li>
<li class="post-item">
<a href="https://jafner.dev/experience/" class="post-item-inner">
<a href="https://jafner.dev/page/experience/" class="post-item-inner">
<span class="post-title">Experience</span>
<span class="post-day">
@ -148,7 +152,18 @@
</li>
<li class="post-item">
<a href="https://jafner.dev/about/" class="post-item-inner">
<a href="https://jafner.dev/page/projects/" class="post-item-inner">
<span class="post-title">Projects</span>
<span class="post-day">
May 28
</span>
</a>
</li>
<li class="post-item">
<a href="https://jafner.dev/page/about/" class="post-item-inner">
<span class="post-title">About Me</span>
<span class="post-day">
@ -188,7 +203,7 @@
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -201,7 +216,7 @@
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -11,23 +11,30 @@
<atom:link href="https://jafner.dev/page/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>Articles</title>
<link>https://jafner.dev/articles/</link>
<link>https://jafner.dev/page/articles/</link>
<pubDate>Tue, 28 May 2024 17:56:53 -0700</pubDate>
<guid>https://jafner.dev/articles/</guid>
<guid>https://jafner.dev/page/articles/</guid>
<description></description>
</item>
<item>
<title>Experience</title>
<link>https://jafner.dev/experience/</link>
<link>https://jafner.dev/page/experience/</link>
<pubDate>Tue, 28 May 2024 17:56:47 -0700</pubDate>
<guid>https://jafner.dev/experience/</guid>
<guid>https://jafner.dev/page/experience/</guid>
<description>Below is a reverse chronological listing of my employment history.&#xA;[2021-11 to 2024-02] Site Reliability Engineer @ American Eagle From November 2021 through February 2024, I worked on a long-term contract with American Eagle to design and implement observability tooling for cloud infrastructure and apps.&#xA;Spearheaded implementation of Bamboo pipelines to automate deployment of New Relic/Terraform observability codebase. That reduced mean-time-to-deployment for our commits by more than 10x. Increased deployment frequency from ~weekly to ~twice daily.</description>
</item>
<item>
<title>Projects</title>
<link>https://jafner.dev/page/projects/</link>
<pubDate>Tue, 28 May 2024 17:56:41 -0700</pubDate>
<guid>https://jafner.dev/page/projects/</guid>
<description></description>
</item>
<item>
<title>About Me</title>
<link>https://jafner.dev/about/</link>
<link>https://jafner.dev/page/about/</link>
<pubDate>Tue, 28 May 2024 17:56:25 -0700</pubDate>
<guid>https://jafner.dev/about/</guid>
<guid>https://jafner.dev/page/about/</guid>
<description>My name is Joey Hafner. Im a computing engineer located in Tacoma, WA.&#xA;Im always working on a diverse array of tech projects. Whether its my homelab, or spending 3 hours wrestling with a script to save me 30 seconds.&#xA;Ive enjoyed hacking with whatever tech I could get my hands on since I was a teen, and that interest was overclocked in 2019 when I found Docker and started my homelab.</description>
</item>
</channel>

View File

@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="" />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/page/projects/" />
<title>
Projects :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Projects">
<meta itemprop="description" content=""><meta itemprop="datePublished" content="2024-05-28T17:56:41-07:00" />
<meta itemprop="dateModified" content="2024-05-28T17:56:41-07:00" />
<meta itemprop="wordCount" content="0"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Projects"/>
<meta name="twitter:description" content=""/>
<meta property="og:title" content="Projects" />
<meta property="og:description" content="" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/page/projects/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="page" />
<meta property="article:published_time" content="2024-05-28T17:56:41-07:00" />
<meta property="article:modified_time" content="2024-05-28T17:56:41-07:00" />
<meta property="article:published_time" content="2024-05-28 17:56:41 -0700 PDT" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="post">
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/page/projects/">Projects</a></h2>
<div class="post-content">
</div>
</article>
<hr />
<div class="post-info">
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="" />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/projects/" />
<title>
Projects :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Projects">
<meta itemprop="description" content="Personal site for Joey Hafner">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Projects"/>
<meta name="twitter:description" content="Personal site for Joey Hafner"/>
<meta property="og:title" content="Projects" />
<meta property="og:description" content="Personal site for Joey Hafner" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://jafner.dev/projects/" /><meta property="og:image" content="https://jafner.dev" />
<link rel="alternate" type="application/rss+xml" href="https://jafner.dev/projects/index.xml" title="Jafner.dev" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="posts">
<h1>Projects</h1>
<div class="posts-group">
<div class="post-year">2024</div>
<ul class="posts-list">
<li class="post-item">
<a href="https://jafner.dev/projects/pamidi/" class="post-item-inner">
<span class="post-title">Pamidi - Control PulseAudio with a MIDI device</span>
<span class="post-day">
May 28
</span>
</a>
</li>
</ul>
</div>
<div class="pagination">
<div class="pagination__buttons">
</div>
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Projects on Jafner.dev</title>
<link>https://jafner.dev/projects/</link>
<description>Recent content in Projects on Jafner.dev</description>
<generator>Hugo -- gohugo.io</generator>
<language>en</language>
<copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright>
<lastBuildDate>Tue, 28 May 2024 17:58:19 -0700</lastBuildDate>
<atom:link href="https://jafner.dev/projects/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>Pamidi - Control PulseAudio with a MIDI device</title>
<link>https://jafner.dev/projects/pamidi/</link>
<pubDate>Tue, 28 May 2024 17:58:19 -0700</pubDate>
<guid>https://jafner.dev/projects/pamidi/</guid>
<description>My first project posted publicly. Initially I didn&amp;rsquo;t expect to post it, but I didn&amp;rsquo;t see any other solutions to the problem I was having, so I figured my solution could inspire someone else to do it better.&#xA;Problem: Physical Volume Knobs for Applications While using Windows, I installed an application called MIDI Mixer to map the physical volume knobs of my Behringer X-Touch Mini (and the accompanying mute and media control buttons) to individual applications on my PC.</description>
</item>
</channel>
</rss>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>https://jafner.dev/projects/</title>
<link rel="canonical" href="https://jafner.dev/projects/">
<meta name="robots" content="noindex">
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=https://jafner.dev/projects/">
</head>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@ -0,0 +1,441 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="My first project posted publicly. Initially I didn&amp;rsquo;t expect to post it, but I didn&amp;rsquo;t see any other solutions to the problem I was having, so I figured my solution could inspire someone else to do it better.
Problem: Physical Volume Knobs for Applications While using Windows, I installed an application called MIDI Mixer to map the physical volume knobs of my Behringer X-Touch Mini (and the accompanying mute and media control buttons) to individual applications on my PC." />
<meta name="keywords" content="" />
<meta name="robots" content="noodp" />
<meta name="theme-color" content="" />
<link rel="canonical" href="https://jafner.dev/projects/pamidi/" />
<title>
Pamidi - Control PulseAudio with a MIDI device :: Jafner.dev — Hello Friend NG Theme
</title>
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
<meta itemprop="name" content="Pamidi - Control PulseAudio with a MIDI device">
<meta itemprop="description" content="My first project posted publicly. Initially I didn&rsquo;t expect to post it, but I didn&rsquo;t see any other solutions to the problem I was having, so I figured my solution could inspire someone else to do it better.
Problem: Physical Volume Knobs for Applications While using Windows, I installed an application called MIDI Mixer to map the physical volume knobs of my Behringer X-Touch Mini (and the accompanying mute and media control buttons) to individual applications on my PC."><meta itemprop="datePublished" content="2024-05-28T17:58:19-07:00" />
<meta itemprop="dateModified" content="2024-05-28T17:58:19-07:00" />
<meta itemprop="wordCount" content="1958"><meta itemprop="image" content="https://jafner.dev" />
<meta itemprop="keywords" content="" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://jafner.dev" /><meta name="twitter:title" content="Pamidi - Control PulseAudio with a MIDI device"/>
<meta name="twitter:description" content="My first project posted publicly. Initially I didn&rsquo;t expect to post it, but I didn&rsquo;t see any other solutions to the problem I was having, so I figured my solution could inspire someone else to do it better.
Problem: Physical Volume Knobs for Applications While using Windows, I installed an application called MIDI Mixer to map the physical volume knobs of my Behringer X-Touch Mini (and the accompanying mute and media control buttons) to individual applications on my PC."/>
<meta property="og:title" content="Pamidi - Control PulseAudio with a MIDI device" />
<meta property="og:description" content="My first project posted publicly. Initially I didn&rsquo;t expect to post it, but I didn&rsquo;t see any other solutions to the problem I was having, so I figured my solution could inspire someone else to do it better.
Problem: Physical Volume Knobs for Applications While using Windows, I installed an application called MIDI Mixer to map the physical volume knobs of my Behringer X-Touch Mini (and the accompanying mute and media control buttons) to individual applications on my PC." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://jafner.dev/projects/pamidi/" /><meta property="og:image" content="https://jafner.dev" /><meta property="article:section" content="projects" />
<meta property="article:published_time" content="2024-05-28T17:58:19-07:00" />
<meta property="article:modified_time" content="2024-05-28T17:58:19-07:00" />
<meta property="article:published_time" content="2024-05-28 17:58:19 -0700 PDT" />
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
<span class="logo__text ">
$ ~/</span>
<span class="logo__cursor" style=
"
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
<span class="menu-trigger">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</span>
</span>
</span>
</header>
<div class="content">
<main class="post">
<div class="post-info">
</p>
</div>
<article>
<h2 class="post-title"><a href="https://jafner.dev/projects/pamidi/">Pamidi - Control PulseAudio with a MIDI device</a></h2>
<div class="post-content">
<img src="../pamidi.jpg" class="left" />
<p>My first project posted publicly. Initially I didn&rsquo;t expect to post it, but I didn&rsquo;t see any other solutions to the problem I was having, so I figured my solution could inspire someone else to do it better.</p>
<h1 id="problem-physical-volume-knobs-for-applications">Problem: Physical Volume Knobs for Applications</h1>
<p>While using Windows, I installed an application called <a href="https://www.midi-mixer.com/">MIDI Mixer</a> to map the physical volume knobs of my <a href="https://www.behringer.com/product.html?modelCode=0808-AAF">Behringer X-Touch Mini</a> (and the accompanying mute and media control buttons) to individual applications on my PC. The most common use-case for me was turning my Spotify music up or down without alt-tabbing from my Overwatch game. My microphone was also mapped into the software.</p>
<p>It&rsquo;s just nice to have a physical interface for these things. But when I tried switching to Pop!_OS a couple years ago, I first <a href="https://www.reddit.com/r/linuxaudio/comments/owqi6j/linux_equivalent_to_midi_mixer_functionality/">inquired at /r/linuxaudio</a> about replicating that functionality on Linux. While I was able to find <a href="https://github.com/solarnz/pamidicontrol">a similar project</a>, nothing really scratched the itch, so I dug in to build my own.</p>
<h1 id="solution-pulseaudio-and-xdotool-in-a-bash-script">Solution: PulseAudio and Xdotool in a Bash Script</h1>
<p>I built <a href="https://github.com/Jafner/pamidi"><code>pamidi</code></a>. We&rsquo;ll break it down here, with the benefit of retrospect.</p>
<h2 id="dependencies">Dependencies</h2>
<p>Two utilities are critical to the function of the script: <a href="https://github.com/jordansissel/xdotool"><code>xdotool</code></a> and <a href="https://man.archlinux.org/man/pacmd.1"><code>pacmd</code></a>. Additionally, we presume you&rsquo;re running SystemD with PulseAudio.</p>
<ul>
<li><code>xdotool</code> is used to get the current focused window. This lets us drastically simplify the UX of mapping the volume knobs to specific applications.</li>
<li><code>pacmd</code> is used to change volume and toggle mute for PulseAudio streams.</li>
</ul>
<h2 id="code-breakdown">Code Breakdown</h2>
<p>Let&rsquo;s take a look at <a href="https://github.com/Jafner/pamidi/blob/main/pamidi.sh">the code</a>. The source is annotated with comments, but we&rsquo;ll just look at the code here.</p>
<h3 id="initialize-the-service">Initialize the Service</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>initialize<span style="color:#f92672">(){</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Initializing&#34;</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Checking for xdotool&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> ! hash xdotool &amp;&gt; /dev/null; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;xdotool could not be found, exiting&#34;</span>
</span></span><span style="display:flex;"><span> exit <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;xdotool found&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Waiting for pulseaudio service to start...&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">while</span> <span style="color:#f92672">[[</span> <span style="color:#66d9ef">$(</span>systemctl --machine<span style="color:#f92672">=</span>joey@.host --user is-active --quiet pulseaudio<span style="color:#66d9ef">)</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Pulseaudio service not started, waiting...&#34;</span>
</span></span><span style="display:flex;"><span> sleep <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Waiting for X-TOUCH MINI to be connected...&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">while</span> <span style="color:#f92672">[[</span> ! <span style="color:#66d9ef">$(</span>lsusb | grep <span style="color:#e6db74">&#34;X-TOUCH MINI&#34;</span><span style="color:#66d9ef">)</span> <span style="color:#f92672">]]</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;X-TOUCH MINI not connected. Waiting...&#34;</span>
</span></span><span style="display:flex;"><span> sleep <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span> col_1_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> col_2_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> col_3_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> col_4_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> col_5_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> col_6_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> col_7_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> col_8_app_pid<span style="color:#f92672">=</span>-1
</span></span><span style="display:flex;"><span> assign_profile_1
</span></span><span style="display:flex;"><span> print_col_app_ids
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;Initialized pamidi&#34;</span>
</span></span><span style="display:flex;"><span> notify-send <span style="color:#e6db74">&#34;Initialized pamidi&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><ol>
<li>First we check to ensure <code>xdotool</code> is installed. <code>hash</code> is a weird choice for checking the presence of a command. Would probably use <code>which</code> today.</li>
<li>Next we wait until we see that the PulseAudio SystemD unit&rsquo;s status is &ldquo;active&rdquo;. But, uh&hellip; I&rsquo;m not sure why I needed that <code>--machine=joey@.host</code> flag.</li>
<li>We wait until <code>lsusb</code> reports the <code>X-TOUCH MINI</code> as connected.</li>
<li>We set up our 8 variables for storing application PIDs.</li>
<li>We invoke a yet-to-be-implemented function <code>assign_profile_1</code>. It does nothing.</li>
<li>We print the PIDs bound to each knob to the console. And then we send an OS notification that the service is initialized.</li>
</ol>
<p>Cool. Now how does it actually work?</p>
<h3 id="change-volume-mackie-vs-standard">Change Volume: Mackie vs. Standard</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>change_volume_mackie<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#f92672">((</span> $2 &gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">64</span> <span style="color:#f92672">))</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span> vol_change<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-</span><span style="color:#66d9ef">$(</span>expr $2 - 64<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span> vol_change<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;+</span>$2<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> app_pid<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> all_sink_inputs<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>pacmd list-sink-inputs<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> all_sink_inputs<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>paste <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> &lt;<span style="color:#f92672">(</span>printf <span style="color:#e6db74">&#39;%s&#39;</span> <span style="color:#e6db74">&#34;</span>$all_sink_inputs<span style="color:#e6db74">&#34;</span> | grep <span style="color:#e6db74">&#39;application.process.id&#39;</span> | cut -d<span style="color:#e6db74">&#39;&#34;&#39;</span> -f 2<span style="color:#66d9ef">)</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> &lt;(printf &#39;%s&#39; &#34;</span>$all_sink_inputs<span style="color:#e6db74">&#34; | grep &#39;index: &#39; | rev | cut -d&#39; &#39; -f 1 | rev))&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;</span>$all_sink_inputs<span style="color:#e6db74">&#34;</span> | <span style="color:#66d9ef">while</span> read line ; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span> pid<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span>$line<span style="color:#e6db74">&#34;</span> | cut -f1<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span>$pid<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span> stream_id<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span>$line<span style="color:#e6db74">&#34;</span> | cut -f2<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> pactl set-sink-input-volume $stream_id $vol_change% 2&gt; /dev/null
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><ol>
<li>We take two positional arguments for this function: application PID, and volume delta.
<ul>
<li>The use of volume delta is the primary differentiator between Mackie mode and standard mode. In Mackie mode, turning the knob returns a <em>change</em> in volume. Values from 0-63 represent <code>-63</code> through <code>-1</code> and values from 64-127 represent <code>+0</code> through <code>+63</code>. We use this to set a <code>volume_change</code> variable.</li>
</ul>
</li>
<li>In order to change volume, we need the sink ID matching the PID for the application we&rsquo;ve bound to a particular knob. We do this in a very roundabout way.
<ol>
<li>We get a list of all PulseAudio sink-inputs (playback streams) with their detailed properties. We need the index (sink ID) and application process ID (our PID).</li>
<li>We do some pipe gymnastics to convert that to an array of tuples in the form <code>&lt;sink-id&gt; &lt;application-pid&gt;</code>. Then we iterate over that list to match the provided application PID.</li>
</ol>
</li>
<li>Lastly, we use <code>pactl set-sink-input-volume</code> to change the volume.
<ul>
<li><code>$stream_id</code> determines which sink-input is affected. Like <code>4</code>.</li>
<li><code>$vol_change</code> is a string of a signed integer in <code>|0-63|</code>. Like <code>-12</code> or <code>+62</code></li>
</ul>
</li>
</ol>
<p>Note: In Standard mode, we use the same <code>pactl</code> command, but the volume argument is prepended with a sign to increment/decrement the volume, rather than set it.</p>
<h3 id="toggle-mute-mute-on-and-mute-off">Toggle Mute, Mute On, and Mute Off</h3>
<p>Instead of posting the full functions, which are highly repetitive, we&rsquo;ll just look at how they differ from each other.</p>
<p>We follow the same process as in change volume to get the stream ID from the PID.</p>
<ul>
<li>Toggle mute: <code>pactl set-sink-input-mute $stream_id toggle</code></li>
<li>Mute on: <code>pactl set-sink-input-mute $stream_id on</code></li>
<li>Mute off: <code>pactl set-sink-input-mute $stream_id off</code></li>
</ul>
<h3 id="get-stream-index-from-pid">Get Stream Index from PID</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>get_stream_index_from_pid<span style="color:#f92672">(){</span>
</span></span><span style="display:flex;"><span> all_sink_inputs<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>pacmd list-sink-inputs<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> all_sink_inputs<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>paste <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> &lt;<span style="color:#f92672">(</span>printf <span style="color:#e6db74">&#39;%s&#39;</span> <span style="color:#e6db74">&#34;</span>$all_sink_inputs<span style="color:#e6db74">&#34;</span> | grep <span style="color:#e6db74">&#39;application.process.id&#39;</span> | cut -d<span style="color:#e6db74">&#39;&#34;&#39;</span> -f 2<span style="color:#66d9ef">)</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> &lt;(printf &#39;%s&#39; &#34;</span>$all_sink_inputs<span style="color:#e6db74">&#34; | grep &#39;index: &#39; | rev | cut -d&#39; &#39; -f 1 | rev))&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> stream_ids<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;</span>$all_sink_inputs<span style="color:#e6db74">&#34;</span> | <span style="color:#66d9ef">while</span> read line ; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span> pid<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span>$line<span style="color:#e6db74">&#34;</span> | cut -f1<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span>$pid<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span> echo <span style="color:#e6db74">&#34;</span>$line<span style="color:#e6db74">&#34;</span> | cut -f2
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>This function does all the gymnastics we repeat in every other function. We just don&rsquo;t use this function anywhere in the code.</p>
<h3 id="get-binary-from-pid">Get Binary from PID</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>get_binary_from_pid<span style="color:#f92672">(){</span>
</span></span><span style="display:flex;"><span> output<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>paste -d<span style="color:#e6db74">&#34;\t&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> &lt;<span style="color:#f92672">(</span>printf <span style="color:#e6db74">&#39;%s&#39;</span> <span style="color:#e6db74">&#34;</span>$output<span style="color:#e6db74">&#34;</span> | grep <span style="color:#e6db74">&#39;application.process.id&#39;</span> | cut -d<span style="color:#e6db74">&#39;&#34;&#39;</span> -f 2<span style="color:#66d9ef">)</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> &lt;(printf &#39;%s&#39; &#34;</span>$output<span style="color:#e6db74">&#34; | grep &#39;application.process.binary&#39; | cut -d&#39;&#34;</span><span style="color:#960050;background-color:#1e0010">&#39;</span> -f 2<span style="color:#f92672">))</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> echo &#34;</span>$output<span style="color:#e6db74">&#34; | while read line ; do
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> pid=</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span>$line<span style="color:#e6db74">&#34;</span> | cut -f1<span style="color:#66d9ef">)</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if [[ &#34;</span>$pid<span style="color:#e6db74">&#34; == &#34;</span>$1<span style="color:#e6db74">&#34; ]]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> echo &#34;</span>$line<span style="color:#e6db74">&#34; | cut -f2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> done
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">}
</span></span></span></code></pre></div><p>This function is not used anywhere. It requires that <code>$output</code> contain the raw response from <code>pactl list-sink-inputs</code>. It creates an array of tuples in the form <code>&lt;pid&gt; &lt;application binary&gt;</code>, and then prints the name of the application binary matching the PID passed to the function as the first positional argument.</p>
<h3 id="bind-application">Bind Application</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>bind_application<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span> window_pid<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>xdotool getactivewindow getwindowpid<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> window_name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>xdotool getactivewindow getwindowname<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> col_id<span style="color:#f92672">=</span>$1
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;</span>$col_id<span style="color:#e6db74">&#34;</span> in
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;1&#34;</span> <span style="color:#f92672">)</span> col_1_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;2&#34;</span> <span style="color:#f92672">)</span> col_2_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;3&#34;</span> <span style="color:#f92672">)</span> col_3_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;4&#34;</span> <span style="color:#f92672">)</span> col_4_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;5&#34;</span> <span style="color:#f92672">)</span> col_5_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;6&#34;</span> <span style="color:#f92672">)</span> col_6_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;7&#34;</span> <span style="color:#f92672">)</span> col_7_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;8&#34;</span> <span style="color:#f92672">)</span> col_8_app_pid<span style="color:#f92672">=</span>$window_pid <span style="color:#f92672">&amp;&amp;</span> notify-send <span style="color:#e6db74">&#34;Set knob </span>$col_id<span style="color:#e6db74"> to </span>$window_name<span style="color:#e6db74">&#34;</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">esac</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>This function is called when we press down on one of the knobs. It binds the currently focused application to that knob. Very nice UX, and reletively simply implemented. It takes the index of the knob pressed as its one positional argument.</p>
<ol>
<li>Use <code>xdotool</code> to get the pid and name of the currently active window.</li>
<li>Assign the window PID to that knob, and send an OS notification to let the user know what happened.</li>
</ol>
<h3 id="main-mackie-and-standard">Main: Mackie and Standard</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>main_mackie<span style="color:#f92672">(){</span>
</span></span><span style="display:flex;"><span> aseqdump -p <span style="color:#e6db74">&#34;X-TOUCH MINI&#34;</span> | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> <span style="color:#66d9ef">while</span> IFS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34; ,&#34;</span> read src ev1 ev2 ch label1 data1 label2 data2 rest; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#34;</span>$ev1<span style="color:#e6db74"> </span>$ev2<span style="color:#e6db74"> </span>$data1<span style="color:#e6db74"> </span>$data2<span style="color:#e6db74">&#34;</span> in
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 1</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 32&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">1</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 89&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_1_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 87&#34;</span>* <span style="color:#f92672">)</span> print_col_app_ids ;; <span style="color:#75715e"># bottom button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 16&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_1_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 2</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 33&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">2</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 90&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_2_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 88&#34;</span>* <span style="color:#f92672">)</span> ;; <span style="color:#75715e"># bottom button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 17&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_2_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 3</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 34&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">3</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 40&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_3_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 91&#34;</span>* <span style="color:#f92672">)</span> media_prev ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 18&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_3_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 4</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 35&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">4</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 41&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_4_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 92&#34;</span>* <span style="color:#f92672">)</span> media_next ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 19&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_4_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 5</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 36&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">5</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 42&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_5_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 86&#34;</span>* <span style="color:#f92672">)</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 20&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_5_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 6</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 37&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">6</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 43&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_6_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 93&#34;</span>* <span style="color:#f92672">)</span> media_stop ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 21&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_6_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 7</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 38&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">7</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 44&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_7_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 94&#34;</span>* <span style="color:#f92672">)</span> media_play_pause ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 22&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_7_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># column 8</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 39&#34;</span>* <span style="color:#f92672">)</span> bind_application <span style="color:#ae81ff">8</span> ;; <span style="color:#75715e"># knob press</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 45&#34;</span>* <span style="color:#f92672">)</span> toggle_mute $col_8_app_pid ;; <span style="color:#75715e"># top button</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 95&#34;</span>* <span style="color:#f92672">)</span> ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Control change 23&#34;</span>* <span style="color:#f92672">)</span> change_volume $col_8_app_pid $data2 ;; <span style="color:#75715e"># knob turn</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># layer a and b buttons</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 84&#34;</span>* <span style="color:#f92672">)</span> assign_profile_1 ;;
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;Note on 85&#34;</span>* <span style="color:#f92672">)</span> assign_profile_2 ;;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">esac</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>This one took a lot of trial and error, and this function is where we would need to implement profiles for different devices.</p>
<ol>
<li>We use <code>aseqdump</code> to attach to the ALSA output stream of the &ldquo;X-TOUCH MINI&rdquo; device (<code>-p &quot;X-TOUCH-MINI</code>).</li>
<li>We read each line in a while loop, and set variables according to the format used by the X-Touch Mini in <code>aseqdump</code>.
<ul>
<li>The sequence <code>$ev1 $ev2 $data1</code> is used to determine which physical interaction was used. Its values look like &ldquo;Note on 36&rdquo; or &ldquo;Control change 18&rdquo;, which represent Knob 5 Press and Knob 3 Turn, respectively.</li>
<li>For knob turn interactions, we pass the <code>$data2</code> value to the change_volume function, otherwise it is discarded.</li>
</ul>
</li>
</ol>
<p>Note: The difference between Mackie and standard here is the mapping between <code>$data1</code> and the physical interaction. E.g. Knob 5 Press in Mackie mode sends &ldquo;Note on 36&rdquo;, and in standard mode it sends &ldquo;Control change 13 127&rdquo;.</p>
<h2 id="future-work">Future Work</h2>
<p>This script was amateurish, and today I don&rsquo;t need the functionality it provides. It&rsquo;s unlikely I will continue to work on it, but as an exercise, there are a few layers of improvements I would make:</p>
<ol>
<li>Remove <code>--machine=joey@.host</code> from the PulseAudio service up check.</li>
<li>Improve the tragic state of optimization for the change volume functions. We <em>do not</em> need to get the entire list of running audio sinks every time we increment or decrement the volume.</li>
<li>Eliminate the repetitiveness of the change volume and mute functions.</li>
<li>Map interactions to ALSA sequence entries more programmatically (e.g. <code>&quot;Note on&quot;) bind_application $(($data1 - 31)) ;;</code>) This can apply to both Mackie and standard mode.</li>
<li>Modularize functions to make it more portable between input devices.</li>
<li>Rewrite in a proper programming language. Python, or Go, or Rust.</li>
</ol>
<h1 id="conclusion">Conclusion</h1>
<p>This was a fun project. I learned a bit about MIDI and ALSA, a bit about Bash and Systemd, and I built something useful.</p>
<p>It was <a href="https://www.reddit.com/r/linuxaudio/comments/qf59fx/a_little_bash_script_to_control_application/">received kindly</a>, despite its vast room for improvement. And some folks are <a href="https://www.reddit.com/r/linuxaudio/comments/1cr9w68/linux_equivalent_to_pcpanel_or_midi_mixer/">still encountering</a> this need, so maybe it&rsquo;s worth revisiting.</p>
<p>Today, I use a <a href="https://www.tc-helicon.com/product.html?modelCode=0803-AAB">GoXLR Mini</a> with <a href="https://github.com/GoXLR-on-Linux/goxlr-utility/">GoXLR-on-Linux/GoXLR-utility</a> to get much of the functionality I was wanting. It&rsquo;s missing some things (like dynamically rebinding faders to applications), but has some nice features pamidi could never replicate, such as microphone audio processing.</p>
</div>
</article>
<hr />
<div class="post-info">
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>&copy; 2023</span>
<span><a href="https://jafner.dev"></a></span>
<span><a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a></span>
<span><a href="https://jafner.dev/posts/index.xml" target="_blank" title="rss"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
</footer>
</div>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
</body>
</html>

View File

@ -23,16 +23,20 @@
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -75,7 +79,7 @@
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -94,7 +98,7 @@
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -147,7 +151,7 @@
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -160,7 +164,7 @@
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -3,18 +3,33 @@
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://jafner.dev/articles/</loc>
<lastmod>2024-05-28T17:56:53-07:00</lastmod>
<lastmod>2024-06-27T09:15:13-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/articles/homelab-tour-series-intro/</loc>
<lastmod>2024-06-27T09:15:13-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/</loc>
<lastmod>2024-06-27T09:15:13-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/projects/pamidi/</loc>
<lastmod>2024-05-28T17:58:19-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/projects/</loc>
<lastmod>2024-05-28T17:58:19-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/page/articles/</loc>
<lastmod>2024-05-28T17:56:53-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/page/</loc>
<lastmod>2024-05-28T17:56:53-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/experience/</loc>
<loc>https://jafner.dev/page/experience/</loc>
<lastmod>2024-05-28T17:56:47-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/about/</loc>
<loc>https://jafner.dev/page/projects/</loc>
<lastmod>2024-05-28T17:56:41-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/page/about/</loc>
<lastmod>2024-05-28T17:56:25-07:00</lastmod>
</url><url>
<loc>https://jafner.dev/categories/</loc>

View File

@ -23,16 +23,20 @@
<link rel="stylesheet" href="https://jafner.dev/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" href="/main.949191c1dcc9c4a887997048b240354e47152016d821198f89448496ba42e491.css" integrity="sha256-lJGRwdzJxKiHmXBIskA1TkcVIBbYIRmPiUSElrpC5JE=">
<link rel="stylesheet" type="text/css" href="/css/toc-no-underline.css">
<link rel="apple-touch-icon" sizes="180x180" href="https://jafner.dev/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://jafner.dev/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://jafner.dev/favicon-16x16.png">
<link rel="manifest" href="https://jafner.dev/site.webmanifest">
<link rel="mask-icon" href="https://jafner.dev/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="https://jafner.dev/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="">
@ -75,7 +79,7 @@
<div class="container">
<header class="header">
<span class="header__inner">
<a href="https://jafner.dev/" style="text-decoration: none;">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">&gt;</span>
@ -94,7 +98,7 @@
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="https://jafner.dev/about">About</a></li><li><a href="https://jafner.dev/articles">Articles</a></li><li><a href="https://jafner.dev/experience">Experience</a></li><li><a href="https://jafner.dev/projects">Projects</a></li>
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/experience">Experience</a></li><li><a href="/articles">Articles</a></li><li><a href="/projects">Projects</a></li>
</ul>
</nav>
@ -147,7 +151,7 @@
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
<span>Powered by <a href="http://gohugo.io">Hugo</a></span><span>Theme made with &#10084; by <a href="https://github.com/rhazdon">Djordje Atlialp</a></span>
</div>
</div>
@ -160,7 +164,7 @@
<script type="text/javascript" src="https://jafner.dev/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>
<script type="text/javascript" src="/bundle.min.205d491810c28f95aa953fae884e1c27abe13fdf93ec63b882d0036b248d4a6282eb2d134e4e7225c6ad6e86db87b08488a361ca4a7383d01fcff43f3d57b9c3.js" integrity="sha512-IF1JGBDCj5WqlT&#43;uiE4cJ6vhP9&#43;T7GO4gtADaySNSmKC6y0TTk5yJcatbobbh7CEiKNhykpzg9Afz/Q/PVe5ww=="></script>

View File

@ -0,0 +1,12 @@
nav a:link {
text-decoration: none;
}
nav a:visited {
text-decoration: none;
}
nav a:hover {
text-decoration: underline;
}
nav a:active {
text-decoration: none;
}