diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2026-01-01 17:36:39 +0200 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2026-01-02 16:21:05 +0200 |
| commit | c1856d1f3edbc4b32be5860233dede88db16a7bb (patch) | |
| tree | b3a5e2f600c0079bd04d7ab2bb459b4664ba66a1 | |
| parent | 6d7e34af89ef1038d213ff8ea325bb84d4a00d4a (diff) | |
Start migrating content
49 files changed, 2249 insertions, 225 deletions
diff --git a/config.toml b/config.toml index 1e2184f..7c3b926 100644 --- a/config.toml +++ b/config.toml @@ -5,7 +5,7 @@ title = "Jan Tuomi" description = "Minimalist, digital gardener, senior software engineer." default_language = "en" generate_feeds = true -feed_filenames = ["atom.xml"] +feed_filenames = ["atom.xml", "feed.xml"] author = "Jan Tuomi" generate_sitemap = true generate_robots_txt = true @@ -18,10 +18,11 @@ taxonomies = [{ name = "kind", feed = true }] # Whether to do syntax highlighting # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola highlight_code = true +highlight_theme = "css" error_on_missing_highlight = true external_links_target_blank = true bottom_footnotes = true -lazy_async_image = true +lazy_async_image = false insert_anchor_links = "heading" [extra] diff --git a/content/_index.md b/content/_index.md deleted file mode 100644 index 1365db2..0000000 --- a/content/_index.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Jan Tuomi -template: index.html ---- - -<br> -<a href="/files/Jan_Tuomi_headshot.jpg"><img src="/files/Jan_Tuomi_headshot_220.jpg" style="float: right; width: 175px; margin: 0 0 0 20px;"></a> -Hi! My name is Jan. I am a senior software engineer based in Finland. Welcome to my personal blog and digital garden. - -I like keeping things simple and pragmatic. I'm a life-long learner, but I don't go chasing trends. - -I enjoy spending time with my fiancée and our dog, tinkering with computer hardware and software, games of all kinds but mostly video-, tabletop and role-playing, electronics, music, language learning and linguistics. - -I created this site as a creative outlet for expressing myself and sharing my experiences. - -<div style="height: 5px;"></div> - -{% import "macros/latest_posts.html" as latest_posts %} -{{ latest_posts::latest_posts(limit=3) }} - -The [archive](archive) page is a listing of my posts. I post sporadically about various subjects and with varying levels of effort. - -The [linklog](linklog) page contains a curated aggregation of bookmarks, saved blog posts and essays, books, series, papers or anything else that I feel like sharing or recommending, stored in [Linkhut](https://ln.ht/~jant). - -On the [now](now) page I write what's currently going on in my life. Stuff that some would write onto their social media page. - -The [projects](projects) page contains info about my hobby projects. diff --git a/content/posts/_index.md b/content/posts/_index.md index 9673e56..51d0d41 100644 --- a/content/posts/_index.md +++ b/content/posts/_index.md @@ -2,5 +2,3 @@ title: Posts sort_by: date --- - -foobar diff --git a/content/posts/atk16-my-homegrown-computer.md b/content/posts/atk16-my-homegrown-computer.md new file mode 100644 index 0000000..afad92f --- /dev/null +++ b/content/posts/atk16-my-homegrown-computer.md @@ -0,0 +1,17 @@ +--- +title: ATK16 – My homegrown computer +date: 2024-05-01 +extra: + kind: project +--- + + + +**ATK16** is a learning project I've been working on for some time. It started off as a 16-bit, reduced instruction set computer (RISC) CPU design but evolved into an entire computer with peripherals and software. + +Instead of repeating myself, I'm going to direct you to the [ATK16 project page](../projects/ATK16). It's not complete by any means, but I've written a bunch about the project and my motivations already! [¹](#footnote) + +--- + +<div id="footnote"></div> +1. There's actually more text in there than in my bachelor's thesis. diff --git a/content/posts/cassettes.md b/content/posts/cassettes.md new file mode 100644 index 0000000..efb9bd9 --- /dev/null +++ b/content/posts/cassettes.md @@ -0,0 +1,28 @@ +--- +title: Cassettes +date: 2024-03-27 +extra: + kind: note +--- + +I like the idea of cassettes. They are cheap and analog, and there’s a culture of sharing and trading them. I like the sound of tape saturation and hysteresis. + +They feel much more approachable than vinyls. I dislike snobbery. + +## So I bought a cassette player + + + +It’s a Sony TCM-459V and it’s only a bit noisy and wobbly. It can record too. + +After listening to all of my tapes (not very many, see picture), A side and B side, I can safely say that I like cassettes. + +## Maintenance + +The manual says that the innards need cleaning (rubbing alcohol + q-tip) after every 10 hours of operation. I don’t think this unit has ever been cleaned. + +The belts might also need changing since they apparently go bad after a while. + +## Tapes catch on fire + +I read on the internet that peoples’ tapes have spontaneously combusted. This is a thing to avoid. diff --git a/content/posts/cookbook.md b/content/posts/cookbook.md new file mode 100644 index 0000000..fc2a04a --- /dev/null +++ b/content/posts/cookbook.md @@ -0,0 +1,10 @@ +--- +title: Cookbook +date: 2024-03-28 +extra: + kind: project +--- + +> This note relates to the project [Cookbook](../projects/cookbook). + +I have started a terse cookbook project for myself. Our household struggles with coming up with simple meals to prepare when doing the week's shopping. This should act as inspiration. diff --git a/content/posts/deploying-the-garden.md b/content/posts/deploying-the-garden.md new file mode 100644 index 0000000..ef3821d --- /dev/null +++ b/content/posts/deploying-the-garden.md @@ -0,0 +1,38 @@ +--- +title: Deploying the garden +date: 2024-03-26 +extra: + kind: note +--- + +Every respectable tech blog must contain two posts: + +- ”This site now has RSS” +- ”This is how this site is deployed” + +Other content is optional. + +## Writing on the go + +Content updates go like this: + +1. I write Markdown in a special directory in my Obsidian Vault +2. The change is synced to my Digital Ocean server via Obsidian Sync (I run an Obsidian process on my server) +3. A file system monitor script picks it up and runs a build script +4. Built HTML files are served with Nginx + +This process gives me full control over the content wherever I have access to Obsidian. Making writing as easy as possible is the principle behind this design. + +[This is considered a good idea in the frog community.](https://www.todepond.com/wikiblogarden/art/never-stop-writing/on-your-phone) + +Making changes to the build process itself, or the HTML template, requires a computer, however. + +## Parentheses + +Most of the heavy lifting is done by `pandoc`. The glue parts use an obscure and ancient language with many curved sigils. Give it a try. + + + +## Hosting + +My small Digital Ocean server also moonlights as a container image registry. I build the scripts into an OCI image, push it on the server and run it with `docker-compose`. Don’t over-engineer when you want things _done_. diff --git a/content/posts/diddle-event-scheduler.md b/content/posts/diddle-event-scheduler.md new file mode 100644 index 0000000..cd71faf --- /dev/null +++ b/content/posts/diddle-event-scheduler.md @@ -0,0 +1,20 @@ +--- +title: Diddle, a minimalist event scheduler +date: 2024-03-12 +extra: + kind: project +--- + +I don't like Doodle. It pushes ads and its premium subscription relentlessly while simultaneously feeling like a clumsy, lumbering beast. There is also a lot to be desired in terms of accessibility and mobile-friendliness. + +There are alternatives, like [dudle](https://dud-poll.inf.tu-dresden.de/) or [framadate](https://framadate.org/abc/en/). I could probably make do with those. However, to me this felt like an opportunity to spin up a quick MVP. This MVP turned into _diddle_. + +Diddle ([live instance](https://diddle.jan.systems/), [Github](https://github.com/jantuomi/diddle)) is a minimalist tool that focuses on fast load times, accessibility and pragmatism. The feature set is based on my own personal requirements. It works great for quickly filling out a poll on mobile. + +**Note**: The instance hosted on my domain might go down any second. Use it at your own discretion. + + + +The first 80% of the project was done in two evenings. The last 80% was done during the next week. The tech stack is the _get-shit-done_ stack: **Python** + **Flask** + **PostgreSQL**. There is no required client side javascript: all client side logic is strictly for [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). You can use "dumb" browsers like w3m or lynx to enjoy Diddle just fine. + +Check out the README on Github for more info. diff --git a/content/posts/directory-aware-git.md b/content/posts/directory-aware-git.md new file mode 100644 index 0000000..bee31a8 --- /dev/null +++ b/content/posts/directory-aware-git.md @@ -0,0 +1,42 @@ +--- +title: Directory aware `git` +date: 2024-05-07 +extra: + kind: note +--- + +> Update 2024-12-22: The functionality described here is available natively in modern versions of Git, see more recent note [Path-conditional config and SSH key signing in `git`](../archive/git-path-conditional-config-and-ssh-key-signing). + +> Update 2024-05-10: Fixed a bug in the provided script. + +> Update 2024-05-14: Added user name and email config features. + +I have separate SSH keys and email addresses for personal projects and work projects. My work-related Git projects reside in `~/Projects/Work`. Without a "directory aware" git config, I would have to define `GIT_SSH_COMMAND` when cloning to use the correct key, as well as set up each work repository to use the work key for interacting with the remote, as well as the `user.name` and `user.email` configuration values manually using `git config` commands. This is naturally very error prone and is an easy thing to forget. + +To avoid these issues, here's a script that wraps `git`: + +```bash +#!/bin/bash + +# If GIT_SSH_COMMAND is set, skip automatic key selection +if [ -n $GIT_SSH_COMMAND ]; then + echo # skip +elif [[ $PWD == *Projects/Work* ]]; then + echo "dir-aware-git: using work profile" + export GIT_SSH_COMMAND='ssh -i ~/.ssh/work_rsa' + export GIT_AUTHOR_NAME='<my work name>' + export GIT_AUTHOR_EMAIL='<my work email>' +fi + +git "$@" +``` + +Update the `GIT_` environment values as well as the directory matcher in the `elif` conditional to suit your needs. + +Save the script as `directory-aware-git`, make it executable with `chmod +x directory-aware-git` and put it in some directory that is in `$PATH`. For me, that would be `~/.local/bin/`. Then, add this alias to your shell config, updated with your selected path: + +```bash +alias git=~/.local/bin/directory-aware-git +``` + +Enjoy 👍 diff --git a/content/posts/eurobsdcon-2025-conf-notes.md b/content/posts/eurobsdcon-2025-conf-notes.md new file mode 100644 index 0000000..6894315 --- /dev/null +++ b/content/posts/eurobsdcon-2025-conf-notes.md @@ -0,0 +1,89 @@ +--- +title: "EuroBSDCon 2025: conference notes" +date: 2025-10-17 +extra: + kind: note +--- + +EuroBSDCon is one of the events organized under the BSD community umbrella, focusing on all things related to and descending from 4.4 Berkeley Software Distribution. This year's event was held between September 25th and 28th in Zagreb, Croatia, but the city and country change every year. + +My event experience consisted of two days of "trainings", sometimes also referred to as labs, and two days of conference proper. In addition to these, there were also dev summits and smaller "conference-in-a-conference" kind of deals related to some BSD subsystems like `bhyve`, but those were not relevant to me so I skipped them outright. + +The event was held at the Faculty of Electrical Engineering and Computing (University of Zagreb). The venue was nostalgic in a way since it reminded me a lot of the building where I started my studies in Aalto University. Functional. + + + +Recordings from the presentation rooms can be found on [Youtube](https://www.youtube.com/playlist?list=PLskKNopggjc6bnhXOX5MQbaPKEvohzjYH). You might have to check the talk schedule to find the timestamp of a given talk. + +## Kernel deep-dive + +I attended a two day lab by Prof. Kirk McKusick, who is a well known figure in the BSD world, having worked on core operating system components in the formative years of the BSD lineage. I really enjoyed his relaxed but still intensive teaching style. The lab, or maybe _interactive lecture_ would be more fitting, was an introduction to the FreeBSD kernel subsystems, explaining how things like I/O, scheduling, filesystems and networking work in the kernel. + +> Note that the FreeBSD kernel is not the same as the Linux kernel, or the other \*BSD kernels either for that matter. It is FreeBSD exclusive, but it shares history with other systems that descend from 4.4BSD, which is the common ancestor for many Unix-like OSs. + +You see, I'm not a huge kernel guy (at least not yet) so I was a bit worried that the lab content would go over my head. What happened though surprised me: I understood something like 90% of the material. Apparently I had the necessary prerequisite knowledge after all, or maybe Kirk was just so good at teaching. Maybe both. + + + +## Favorite talks + +There was a healthy range of themes in the talk schedule. Some were straightforward demos of recent development efforts, some were about history (mostly 1970–80s, the childhood years of the BSD project). Some war stories as well, explaining how the speaker et al. dealt with some hindrance. There were also some, I would say _not so BSD-related_, talks about things like data and identity ownership, federated social media, freedom. There is a bunch of overlap between BSD enthusiasts and digital freedom enthusiasts so it was only fitting. + +Here's my top 3, based on overall vibes. + +### _Lessons learned Open Sourcing the UK's Covid Tracing App_, by Terence Eden + +Terence was the third in line in the Saturday's opening keynote schedule. Terence was one of the key engineers behind the open source Covid tracing app that the UK developed and used. The keynote handled themes of public relations, talking about software licenses to lawyers, protecting privacy of developers from the wrath of the public, successful platform launches and shutdowns... a lot of stuff delivered with great pacing and enthusiasm. + +Timestamped link to this talk: [Youtube](https://www.youtube.com/live/qdEMmM5g27M?t=1221) + +### _Enhancing Unix Education through Chaos Engineering and Gamification using FreeBSD_, by Benedict Reuschling, Andreas Kirschner + +Benedict and Andreas had noticed that their students at Hochschule Darmstadt were not very proficient when it came to administering a UNIX system and debugging issues on it. They devised a platform where their students use exercise environments (with root access) and solve problems like DNS problems against the clock. The fastest student teams get winning points on a leaderboard. + +The talk format was a mix of a war story and tech demo: they explained their pedagogical conundrum and walked the viewers through their solution, which was based on FreeBSD and liberal use of jails. + +Timestamped link to this talk: [Youtube](https://www.youtube.com/live/FgtTVzYFEF0?t=23860) + +### _AI slop attacks on the curl project_, by Daniel Stenberg + +Daniel is the main developer in the `curl` project. In his Sunday keynote he explained how the project is being pestered by AI generated, low effort security issue reports that take up a lot of contributor time to review. Why not just block those people outright? Daniel argues that they take all reports indicating a critical vulnerability seriously; they can not allow to mistakenly disregard a valid report. + +Daniel is an entertaining speaker with big responsibility in the open source world. + +Sadly, the talk seems to not be recorded on Youtube. + +## Community + +I was curious to see what the people are like. I knew that BSD is a pretty niche thing so I was afraid the community might be hard to get into, but I was happy to notice that everyone that I talked to seemed very welcoming and excited to share. + +I noticed that there was very little "hierarchy" at the event. Speakers, sponsors and attendees were all mingling together at lunches and at social events. I didn't really see signs of cliques or circles that didn't accommodate newcomers. + +Since the event, I have familiarised myself with BSD community channels such as the [FreeBSD IRC channels](https://wiki.freebsd.org/IRC/Channels) on Libera.Chat and the various forums of [BSD Cafe](https://wiki.bsd.cafe/), hosted by Stefano Marinelli, who was also speaking at the conference. + + + +## There and back again, and the touristy bits + +There were no direct flights from Helsinki to Zagreb, so I had to layover in Germany both on the way there and on the way back. Leaving Finland, the departure time was gnarly: around five in the morning. Good thing I can sleep in economy. I had plenty of time to change planes, and got to the destination without a hitch. + +I stayed in B&B Boutique Casablanca, which was a nice bed & breakfast place with friendly staff. + +Traditional & local foods were my primary target when looking for restaurants. My top picks: + +- **ćevapi** at _Plac_ ([Tripadvisor](https://www.tripadvisor.com/Restaurant_Review-g294454-d7075337-Reviews-Plac-Zagreb_Central_Croatia.html)), pictured below +- **štrukli** at _La Štruk_ ([Website](https://www.lastruk.com/)) + + + +The return trip was more eventful. The plane didn't start boarding until it was more than an hour late. We were never told the reason. That delay caused me to miss my connecting flight 😑 + +I was rerouted via Frankfurt which turned my nice "get home for dinner" schedule into a gruesome "you're home sometime between midnight and 2 AM" trek. Luckily I got a seat on both of the rerouted flights (that was not a given). I was home after 1 AM. + +I have then learned that there would have been a very nice direct route from Finland to Ljubljana, which lies just across the Croatian border, accompanied by a bus route that would have taken me from city to city in a couple of hours. Now I know. + +## The next event + +...will be held in Brussels, Belgium. It's closer to Finland (👍) but I've heard that it's kind of a dull city to visit. Whether that's true or not, I don't know really, never been there before. But I'd prefer going a bit farther away from my home to escape the weather 😅 + +[EuroBSDCon 2026 website](https://2026.eurobsdcon.org/) diff --git a/content/posts/git-path-conditional-config-and-ssh-key-signing.md b/content/posts/git-path-conditional-config-and-ssh-key-signing.md new file mode 100644 index 0000000..4814669 --- /dev/null +++ b/content/posts/git-path-conditional-config-and-ssh-key-signing.md @@ -0,0 +1,67 @@ +--- +title: Path-conditional config and SSH key signing in `git` +date: 2024-12-21 +extra: + kind: note +--- + +Today I learned about two recent-ish Git features. + +## Conditional configs + +Since Git version 2.13, it is possible to use different Git configurations based on values such as the current branch, or what's useful in my use case, path prefix: + +```bash +# File: .gitconfig + +[includeIf "gitdir:~/Projects/Work/"] + path = .gitconfig-work +``` + +[🔗 `includeIf` in Git documentation](https://git-scm.com/docs/git-config#_includes) + +I personally use separate Git identities and SSH keys for personal and work projects. With `includeIf`, using the right ones is a breeze. Just make sure to `includeIf` after your default configuration, in order for precedence to function as expected. + +```bash +# File: .gitconfig-work + +[core] + sshCommand = ssh -i $HOME/.ssh/<work key name> -o IdentitiesOnly=yes +[user] + email = <work email> + signingkey = $HOME/.ssh/<work key name> +[gpg] + format = ssh +[commit] + gpgsign = true +``` + +I'm happy that this is supported natively, so I don't have to use any wrapper scripts, such as those described in [a previous note](../archive/directory-aware-git). + +## SSH key signing + +The above config snippet also includes another recent addition: + +```bash +[user] + signingkey = $HOME/.ssh/<key name> +[gpg] + format = ssh +``` + +[🔗 `gpg.format = ssh` in Git documentation](https://git-scm.com/docs/gitformat-signature#Documentation/gitformat-signature.txt-codesshcodeSSH) + +You can use the same SSH keys that you use to authenticate your pushes to sign your commits. This is a nice for multiple reasons: + +- Most developers already have an SSH key +- PGP keys are not as popular as SSH keys, and suffer from usability issues +- Managing one key is less work than managing multiple keys + +After configuring your key as a signing key, you need to enable commit signing either with `git commit -S` or the inaccurately named configuration value: + +```bash +[commit] + gpgsign = true +``` + +Your Git server must support SSH key signing for your commits to show up as _Verified_. At least GitHub and modern versions of GitLab support this. Just make sure your corresponding public key is listed as a _Signing key_ (GitHub) or an _Authentication & signing key_ (GitLab). diff --git a/content/posts/home-server-part-1.md b/content/posts/home-server-part-1.md new file mode 100644 index 0000000..dda0c69 --- /dev/null +++ b/content/posts/home-server-part-1.md @@ -0,0 +1,78 @@ +--- +title: "A home server journey, part 1: Motivation" +date: 2025-06-30 +description: | + This is the first post in a blog series chronicling my progress with designing and setting up a new, physical FreeBSD home server. +extra: + kind: note +--- + +This is the first post in a blog series chronicling my progress with designing and setting up a new, physical FreeBSD home server. I'm guessing this will become at least a three-parter, but that depends on how things actually go, and how in-depth I happen to write. This first part is about my motivations and requirements, whereas the latter ones will be more technical-deepdive-like. + +{{ toc() }} + + + +## I need a place to run code and store files + +I have pretty simple requirements for a server. I need to run a bunch of services, most of which are Node.js or Python apps, and serve some static websites and files. These are primarily personal applications: tracking finances, or keeping track of house chores, but also hobby projects that I host and expose to the world. + +I also need to store files somewhere, like everyone that partakes in the modern digital society. There isn't that much data to store, really. I don't take that many photos or videos, which tend to be the main driver for purchasing more cloud storage, but I have some Ableton project files (music production) that take up some space. Currently all my project files fit in a couple dozen gigabytes. I store them alongside my documents in Google Drive, using a real-time Drive sync app. This does not include the audio samples and recordings in my projects: I would have to buy a restrictively expensive plan to store those in Drive. + +I have a trusty Ubuntu server, chugging away in some rack in a DigitalOcean datacenter in Amsterdam. Even though DigitalOcean pricing is pretty reasonable for individuals in general, my monthly price tag is around ~35€, after taxes. That sum is based on the cost of running a medium sized x86-64 instance and storing some backup snapshots from that machine. Times twelve, that comes to around 420€ per year. To me, that is a sizeable amount of money. + + + +> 🤔 I would like to run a smaller instance, but I needed a certain level of RAM to reliably run Obsidian Sync. For whatever reason, less memory would have the app crash intermittently. + +Having upgraded to instance to the current size, it is hard to downscale back to a smaller machine. It is possible, of course. But instead of doing that, I started to consider an alternative: maybe I could save some coins and get some hardware of my own, like in the prehistoric times, before cloud services became the de-facto way to run anything. With one year's cloud costs, I could surely get a capable small machine. + + + +## Ownership and control + +Money is not the only motivator. I dislike the idea of having the continuity of my digital presence rely on American / multinational corporations that have no real responsibility when it comes to ensuring my files stay intact, containers running and website up. Google could ban my account without batting an eyelid, which would directly make me lose access to my collection of personal photos, important documents, and various other files I prefer not lose. DigitalOcean could flag my server for suspicious activity. + +All this isn't very likely, but it still happens. And even if it didn't, I don't like having a soulless, corporate entity being the arbitrator on whether I can access my own digital life or not. I would prefer to have the probability of an account-frozen catastrophe be exactly zero instead of 0.1%. + +> 🤔 In terms of cloud provider trustworthiness, I think DigitalOcean is pretty high up in the rankings. I haven't seen much ire against them.🤔 + +I could provide these capabilities to myself, by managing my own hardware and networking. I could set up a NAS (network attached storage) box for the files, and run the services locally on either the same machine or some other hardware. It would have to be publicly accessible (Internet), just like my DigitalOcean VPS. It would have to provide _at least_ the same guarantees as the status quo: periodic restorable backups, redundant storage and at-rest encryption for stored files. + +> As an alternative, I could move my stuff to a European, maybe even Finnish, company. There are some [shared hosting](https://en.wikipedia.org/wiki/Shared_web_hosting_service) providers that exist. I don't really consider this to be that good of an option. I feel that if I'm moving out of the public cloud, I might as well go all the way. + +To replace something as generally trusted and widely used as Google Drive requires some proper thinking. I haven't previously set up any redundant disk storage. My systems have never followed the [3-2-1 backup rule](https://en.wikipedia.org/wiki/Backup#3-2-1_Backup_Rule). + + + +Reaching for full ownership and control is not solely an objective aim. It is in part psychological, an attempt to get back control in a world of weakening privacy and walled content gardens. It is driven by emotion. And this is completely fine: humans are allowed to be motivated by emotion, especially when it comes to personal, hobby-adjacent things. To claim otherwise would be dishonest. + +## Physical electronics are fun, tinkering is fun + +There is an inherent value in physical things. People collect music on CDs, cassette tapes, and vinyls. Playing games on real hardware is seen differently from emulation. A computer running in my home is "more real" than a resource-sharing virtual machine in some datacenter. + +It is nice to own, instead of rent. It is nice to not be bound by a contract, or a service agreement. Having fewer dependencies, legal or technical, is often favorable. + +I can pick out the components and design the infrastructure for my system. I know the moving parts, I know that if something fails, one of the cogs in the machine has stopped turning. I then replace the cog. There is no mystery, at least in an [unknown-unknowns](https://en.wikipedia.org/wiki/There_are_unknown_unknowns) kind of way. + +As long as things are not that serious, maintaining and administrating physical systems can be pretty fun. + +## Simplicity begets reliability + +Physical components are one thing, software is another. I have been running Ubuntu on the VPS for a long time, but I can think of multiple reasons to reconsider that choice. + +When I picked Ubuntu years ago, I didn't know what kind of programs I would be running on the machine. I opted for a Linux distribution that is well supported and offers relatively recent versions of software. I was also uncertain about whether or not I would need to run graphical software. Ubuntu ships with `snap` and `flatpak` , both of which are used to run desktop apps. It is now clear to me that I don't need any of this; stable server software is all I need. + +I am inspired by simplicity. I feel that systems are often improved by removing, not adding. + +I don't understand what is going on in a Ubuntu installation. There are so many components that interact, it's hard to make sense of what affects what. There are sediment layers of Linux history in there, with some Canonical-added goodness in between. To be fair, I don't think I understand the inner workings of any Linux distribution, for that matter. + +A friend had recently set up a bunch of machines using FreeBSD. The elegance of simplicity lures me in. FreeBSD is well respected when it comes to running stable server software. The system boots up with just a couple of processes running in `top`. The fantastic [FreeBSD handbook](https://docs.freebsd.org/en/books/handbook/) and supplementary reading by [Michael W. Lucas](https://mwl.io/nonfiction/os) directly teach what each file under `/etc/` does. + +I did some more reading as well as some experimentation with a FreeBSD virtual machine. It quickly became clear that this should be the way forward. I am excited to try out new, but at the same time respected and old, technology. + +FreeBSD is different. A familiar, but still strange system is fascinating. I have only worked with Linux systems before, so many things are as I expect them to be, while similarly many surprise me. + +## To be continued + +In the next part we'll dive into technical good stuff: setting up the machine and installing FreeBSD. diff --git a/content/posts/home-server-part-2.md b/content/posts/home-server-part-2.md new file mode 100644 index 0000000..937a8df --- /dev/null +++ b/content/posts/home-server-part-2.md @@ -0,0 +1,91 @@ +--- +title: "A home server journey, part 2: Hardware" +date: 2025-10-04 +description: | + This is the second post in a blog series chronicling my progress with designing and setting up a new, physical FreeBSD home server. +extra: + kind: note +--- + +<blockquote>“My first impulse, when presented with any spanking-new piece of computer hardware, is to imagine how it will look in ten years’ time, gathering dust under a card table in a thrift shop.” +<cite style="float: right">― William Gibson, Distrust That Particular Flavor</cite> +</blockquote> +<br> + +This is the second post in a blog series chronicling my progress with designing and setting up a new, physical FreeBSD home server. Last time we went over my motivations, and this time we're looking at the physical hardware. + +Check out episode [1](../archive/home-server-part-1). + +{{ toc() }} + +## Budget specs + +My yearly cost in the public cloud is around 420€. It feels reasonable for a project of this caliber that a year’s expenses could buy a capable minicomputer. + +What is a capable minicomputer then, you may ask. Well, my needs are pretty straightforward: + +<table> +<tr><td colspan=2><strong>Qualitative requirements</strong></td></tr> +<tr><td>Usefulness</td><td>Can run a range of small to medium load web services without a hitch</td></tr> +<tr><td>Trustworthiness</td><td>Data is durable, disk failures are not catastrophic, I can sleep at night</td></tr> +<tr><td>Expandability</td><td>Can add more terabytes in the future</td></tr> +<tr><td colspan=2><strong>Quantitative requirements</strong></td></tr> +<tr><td>CPU</td><td>Low power, economical, can run FreeBSD.</td></tr> +<tr><td>Memory</td><td>Won’t OOM swap in day-to-day operation, ZFS is happy</td></tr> +<tr><td>Disk I/O</td><td>I don’t really care, no high-throughput required. As long as it’s not especially bad (i.e. HDD-level bad).</td></tr> +<tr><td>Disk space</td><td>Slots for multiple SSDs (RAID). Enough space to hold important files and some media with redundancy. No need for data hoarding.</td></tr> +<tr><td>Network</td><td>2 interfaces (private and public networks), Gigabit ethernet. No need for wireless.</td></tr> +<tr><td>Annoyance</td><td>Low noise, low temps, sleek form factor</td></tr> +</table> + +With this list of requirements, I handwaved some models and numbers: + +- CPU: Intel N series ⭐️ +- Memory: 16GB +- Disk space: 2TB (usable) + +> ⭐️ Intel N series processors, formally known as [Alder Lake-N](https://en.wikipedia.org/wiki/Alder_Lake#Alder_Lake-N) and [Twin Lake-N](https://en.wikipedia.org/wiki/Alder_Lake#Twin_lake-N), are affordable processors designed to be primarily used in mobile devices, lower end laptops (think Chromebooks) and small workstations. Some models, such as the N100, N150, N200 and N250, have thermal power ratings as low as 6W, making them popular in the passively-cooled, quasi-embedded design space: routers, NAS's, and the like. + +I got the idea for an Intel N series chip from a friend who has succesfully set up computing cluster from a bunch of small machines with those chips. They seemed to be just what I was looking for. + +With these, I sail off towards the east (AliExpress). + +By the way, I also looked at ARM and RISC-V chips for this. I figured that if Apple Silicon can work as well as it does while being based on ARM, what's stopping me from reaping the same benefits, at least in part? ARM has T1 support from FreeBSD. + +Turns out that there simply aren't any reasonably priced machines based on chips comparable to e.g. the Intel N150. Everything's either designed for mobile devices or just prohibitively expensive. Maybe there is no market yet for small non-x64 computers? + +## Sourcing the silicon + +AliExpress, an online retailer based in China, is nice since they have a huge variety of devices at prices that would never be profitable if sold in a western shop. I would prefer to buy European in this current political atmosphere, but I'm working off a budget, and limiting my options to reputable and ethical vendors would force me to relax my requirements a lot or stop the project in its tracks altogether. + + + +I'm happy to notice that there is a varied selection of devices at the ~200€ price point. Some are designed to be network devices, with a bunch of physical interfaces and limited storage options, while some are clearly NAS oriented, with SATA 3.5" drive bays capable of housing two or four disks, most commonly. + +I'm looking for something in the middle ground: I need two physical interfaces (a property of router-likes) but I also need storage, preferably of the solid state variety (a property of NAS-likes). + +**Conclusion**: I'm going with this one (possibly stale link warning): [Topton Pocket Mini PC](https://www.aliexpress.com/item/1005007905020917.html). With some mystery promotions applied, I had to only pay 162.27€, with free shipping. I also purchased 16GB of RAM (Crucial DDR5 SODIMM 4800MHz) separately, also from AliExpress. + +> AliExpress tends to have multiple layers of ‼️ LIMITED SALE ‼️ and 🤑 TOP DEAL COUPON 🤑 going on at the same time. In the mobile app they also have a bunch of connect-four-type games that can score you big AliExpress Coins©. Please continue consuming. + + + +Regarding disks, I think I want to buy them locally, just in case there are issues with them. It's way less stressful to handle returns and such with a local vendor than with an overseas megacorp. I feel that stuff like RAM is pretty often OK even if bought from less reputable sources, but with disks I don't want to test my luck. + +**Conclusion**: I'm going to get 4 x 1 TB (double the required usable space because of mirroring) of basic NVMe drives from a computer parts shop (Gigantti) nearby. + +## Setting up hardware + +The box arrives after a bit of a wait (par for the course for the cheapest delivery option, China Airmail). Everything seems to be in order content-wise, but there is no installation manual whatsoever included. I would like one, since I need to figure out how to install the RAM stick. It seems to somehow go below the NVMe sticks, but there is no obvious mechanism for detaching the NVMe bay. + +However, with a bit of trial and error I find the correct screw to remove. The top layer comes off, revealing the RAM stick slot underneath. Very carefully I slot in the memory and push down the clamp that fixes it in place. + +With similar care I put the drive bay back in, and connect the four NVMe drives and stick on their respective thermal material stickers that conduct heat for the drives to the heatsink on top of the machine. I screw everything in place with the tiny screws that came with the machine. A magnetic screwdriver would be a godsend now! + +After a bit, everything's put together. After connecting the power cable, the machine powers on without me even touching the power switch, which is a bit surprising, but ok. I hear the POST beep. + +Success! + +## To be continued + +In the next part(s) we will be looking at installing FreeBSD for the first (and second) time, and networking. I had some – let's say curious – issues with DHCP leases from the ISP. We might also take a look at my jail setup. diff --git a/content/posts/home-server-part-3.md b/content/posts/home-server-part-3.md new file mode 100644 index 0000000..7aadbba --- /dev/null +++ b/content/posts/home-server-part-3.md @@ -0,0 +1,77 @@ +--- +title: "A home server journey, part 3: Installation" +date: 2025-12-23 +description: | + This is the third episode in a series where I set up a FreeBSD home server, explaining all the steps, problems and solutions along the way. This time we're installing the OS for the first time. +extra: + kind: note +--- + +This is the third episode in a series where I set up a FreeBSD home server, explaining all the steps, problems and solutions along the way. This time we're installing the OS for the first time. + +Check out episodes [1](../archive/home-server-part-1) and [2](../archive/home-server-part-2). + +{{ toc() }} + +## Installing FreeBSD + +Installation should be pretty straightforward. I'm going to go with the latest stable release installer (`14.3-RELEASE`) in the `amd64` + `memstick.iso` variety. Let's burn the installer onto a memory stick, plug it in, and was enter the install wizard. + +> Note from the future! `15.0-RELEASE` is already out, and I have recently updated to it. This post is already outdated! + +The initial idea is this: + +- ZFS, for all the automatic durability gains and easy-to-setup RAID1+0 (stripe of mirrors) +- encrypted ZFS datasets (kinda similar to partitions) for **jails**, which would contain all the actual services +- unencrypted root, so that I can reboot the machine remotely, physically unattended + +> [Jails](https://docs.freebsd.org/en/books/handbook/jails/) are a lightweight way to containerize applications. They can get a separate process space, network stack, file system, root user, among others. They use the same kernel as the host, so they are more similar to LXC or OCI containers (think Docker, but more mature and flexible) than to virtual machines, which in FreeBSD are managed with [bhyve](https://docs.freebsd.org/en/books/handbook/virtualization/#virtualization-host-bhyve). +> +> Teaser: we will be installing `bhyve` later too. + +Using the install wizard, I set up the system following my rough plan. And here we go, all up and running: + + + +### RAIDZ + +You might notice that I bought 4 x 1 TB of NVMe disks, but `fastfetch` is only showing ~2TB. That's because I set up the disks into a "RAID 1+0" configuration: + + + +RAID 1+0 is a combination of two RAID levels. RAID 1, or _mirror_, consumes two disks to produce one virtual disk that has the capacity of only one physical disk but can withstand the loss of either one. ZFS can automatically heal missing data in a member of a mirror by copying it over from the healthy member. RAID 0, or _stripe_, consumes two disks to produce a disk that has the capacity of the sum of its member capacities as well as double I/O speed, but fails if either member disk fails. The combination of these is a useful way to get increased speed and increased durability at the price of half your raw capacity. + +## Threat profile and encryption + +To figure out what level and what kind of at-rest encryption I need, I need to stop and think about the threat model. + +Scenario A, _online intruder_. An attacker that gains shell access can exfiltrate any data that the user has access to, since the decryption key has been activated at boot. At-rest encryption won't help here. + +Scenario B, _burglar_. An intruder grabs the machine and brings it to a place where they can inspect the disks, possibly with sophisticated recovery tools. + +- In a non-encrypted-root install, everything outside the encrypted jails is instantly accessible. Stored API keys and similar secrets leak. The administrator must take great care not to keep _anything_ of value on the unencrypted partition(s), and store everything in encrypted datasets. +- An encrypted root would be safe from this attack, assuming the cryptography used in the encryption is bulletproof. + +Scenario C, _evil maid attack_. A friend, spouse or similar tampers with the unattended physical system. Illegitimate access is blocked by requiring login and keeping the credentials in my personal vault. If the whole disk is encrypted, the worst thing the attacker can do is cause temporary harm and possibly data loss by powering off the system. If not, the attacker can do all kinds of nasty things, like booting a live environment and copying the disk contents, replacing the kernel/bootloader, installing malware etc. + +## Conclusion + +It makes sense to encrypt the whole thing. + +⚠️ But wait a minute! This is against my initial idea: + +> unencrypted root, so that I can reboot the machine remotely, physically unattended + +Encrypting the whole disk loses the ability to do unattended reboots. A bit inconvenient, but it is what it is. I'll just make sure to only reboot when I'm physically near the machine, which should be often, considering I work from home and the server is, uh, right there. + +Because of this architectural change, I ended up installing FreeBSD for a second time, now with full disk encryption. + +## System disk? + +Sometimes a separate small system disk is used in addition to a redundant array of "data disks". I decided to not pursue this setup, since then the system would not benefit from the RAIDZ redundancy and self-healing capabilities. In fact, I'd imagine that the ability to repair the OS and packages is even more important to me than repairing bulk data. + +I decided to keep the system next to the data on the disks (a "traditional" install). + +## To be continued + +In the next part(s) we will be looking at networking. I had some – let's say curious – issues with DHCP leases from the ISP. We'll also take a look at my jail setup. diff --git a/content/posts/home-server-part-4.md b/content/posts/home-server-part-4.md new file mode 100644 index 0000000..5baf038 --- /dev/null +++ b/content/posts/home-server-part-4.md @@ -0,0 +1,124 @@ +--- +title: "A home server journey, part 4: Network" +date: 2025-12-25 +description: | + This is the fourth episode in a series where I set up a FreeBSD home server, explaining all the steps, problems and solutions along the way. This time we're setting up the networking. +extra: + kind: note +--- + +This is the fourth episode in a series where I set up a FreeBSD home server, explaining all the steps, problems and solutions along the way. This time we're setting up the networking. + +Check out episodes [1](../archive/home-server-part-1), [2](../archive/home-server-part-2), [3](../archive/home-server-part-3). + +{{ toc() }} + +## Dual interfaces + +I already have a private network (`192.168.0.0/16`) that uses my pfSense router as its gateway. The FreeBSD server will connect to this network through ethernet interface number one (`lan0`). Packets to and from local devices go through this interface. + +The host will also be connected directly to the ISP's network using its second interface (`wan0`). This interface will get its IP using DHCP. Packets to the internet, as well as packets to public services running on the FreeBSD host go through this interface. + + + +A dual interface set up like this is simple in theory, but there are important nuances, e.g. regarding return traffic routing and DNS. + +## Routing + +If we send a packet out through `eth0`, we should expect return traffic to arrive on `eth0` (and vice versa). However, it is rather easy to misconfigure the system. + +Assume we have a public service, such as an `nginx` web server, which listens on the public interface. If we set up `rc.conf` naively like this: + +```sh +defaultrouter=192.168.0.1 +``` + +...the request might arrive through the public interface, but the response from the web server leaves through the private interface since we have configured the private gateway as the default. This is called _asymmetric routing_. + +This might work! Both routes lead to the internet. From the perspective of the ISP, it doesn't really matter whether the packet went through the home router or not, in this case. + +Sometimes however, this setup leads to the most vexing of networking issues. Figuring out what's wrong taught me a bunch about DHCP and layer 3 routing. + +### Routing issue \#1: ISP DHCP server and MAC addresses + +A DHCP server can keep track of clients using their MAC address. Separate physical interfaces have separate MAC addresses, so there is an obvious problem here. If an initial DHCP request broadcast leaves through one interface, and following DHCP requests (non-broadcast) leave through another interface, the DHCP server might see this as a forged packet. + +This happened to me because of the `defaultrouter` setting: + +1. The initial broadcast leaves through the `wan0` interface correctly. An IP is received. +2. When the lease is about to expire, a targeted request is sent to the now known DHCP server address. This is routed through `lan0` and `192.168.0.1`, since it's the `defaultrouter`. +3. The DHCP server sees the FreeBSD WAN MAC on first contact, and the home router MAC address on second contact. The lease is not renewed. +4. The lease expires, repeat from step 1. + +My solution? Just don't use `defaultrouter`. As part of the routine DHCP song and dance, `dhclient` adds a valid default route to the system routing table. Just use that for all internet-bound traffic. You can see the routes with `netstat -rn`: + +```sh +# netstat -rn +Routing tables + +Internet: +Destination Gateway Flags Netif Expire +default 87.92.64.1 UGS wan0 +87.92.64.0/18 link#36 U wan0 +192.168.0.0/16 link#25 U lan0 +127.0.0.1 link#57 UH lo0 +... +``` + +The `default` route was added by DHCP. + +### Routing issue \#2: DNS-level ad blocker in my router + +I run [pfBlockerNG](https://docs.netgate.com/pfsense/en/latest/packages/pfblocker.html) on my router to block ads on a DNS level (think Pi-hole). If I route traffic through the router, the return traffic is processed by this extra firewall. Some packets got dropped by pfBlockerNG and never arrived at the destination. + +This was very annoying to debug! At least I had logging on, so I could confirm what's happening pretty quickly. + +## DNS + +Nameservers are defined on a whole system basis in `/etc/resolv.conf`, and not per interface or per IP. I have a basic DNS resolver running on my home server, so I simply configured my `resolv.conf` as: + +```sh +nameserver 192.168.0.1 +``` + +### Split horizon + +Which IP should be returned from the DNS server if I query `some-public-service.jan.systems`? The private address from `192.168.0.0/16`, or a public IP reachable from the internet? + +This is called **split horizon** DNS. One approach is to configure the DNS server to respond differently based on the address of the querying host. A private network host gets the private IP, etc. + +I decided to not worry about this, and just use a separate domain, `local.jan.systems` for local addresses. This is a bit inconvenient, but I don't mind. The fewer DNS problems the better. + +### DHCP and `resolv.conf` + +By default, `dhclient` has a hook that overwrites `resolv.conf` with the DHCP-provided nameserver information. I want to always use my router as the DNS server, so I had to disable this functionality. + +On FreeBSD, there are two simple solutions: + +1. `resolvconf.conf`: a meta-config that configures the `resolvconf` tool, which is used internally by `dhclient` + +To disable `resolvconf` altogether, add this to `/etc/resolvconf.conf`: + +```sh +resolvconf=NO +``` + +Now, `resolvconf`, and transitively `dhclient`, won't overwrite your changes anymore. + +2. making `/etc/resolv.conf` immutable with a flag + +As root, you can set the file as immutable with `chflags`. Then, nothing can edit the file before the flag is removed. + +```sh +chflags schg /etc/resolv.conf +``` + +Note that if you have a non-default [`securelevel`](https://man.freebsd.org/cgi/man.cgi?securelevel), you might not be able to remove this flag. + +## Dual stack? + +For now, I'm building everything on IPv4. My ISP has no support for SLAAC which makes IPv6 somewhat non-trivial. I will most likely add IPv6 at some point, but it's not a priority. + +## To be continued + +In the next part(s) we will be looking at provisioning with Ansible. diff --git a/static/files/ansible_task_postgres.png b/content/posts/home-server-part-5/ansible_task_postgres.png Binary files differindex 47f8584..47f8584 100644 --- a/static/files/ansible_task_postgres.png +++ b/content/posts/home-server-part-5/ansible_task_postgres.png diff --git a/content/posts/home-server-part-5/index.md b/content/posts/home-server-part-5/index.md new file mode 100644 index 0000000..16d60f0 --- /dev/null +++ b/content/posts/home-server-part-5/index.md @@ -0,0 +1,102 @@ +--- +title: "A home server journey, part 5: Ansible" +date: 2025-12-26 +description: | + This is the fifth episode in a series where I set up a FreeBSD home server, explaining all the steps, problems and solutions along the way. This time we're provisioning the server with Ansible. +extra: + kind: note +--- + +This is the fifth episode in a series where I set up a FreeBSD home server, explaining all the steps, problems and solutions along the way. This time we're provisioning the server with Ansible. + +Check out episodes [1](../archive/home-server-part-1), [2](../archive/home-server-part-2), [3](../archive/home-server-part-3), [4](../archive/home-server-part-4). + +{{ toc() }} + +## Automation + +[Ansible](https://docs.ansible.com/projects/ansible/latest/index.html) is a tool for running a bunch of Python scripts, or "tasks", in a kind of declarative way. It's commonly used for provisioning servers. + +My ansible configuration is publicly available on my GitHub. I update it regularly. +🌐 [jantuomi/ansible-freebsd-home-server](https://github.com/jantuomi/ansible-freebsd-home-server) + +I have only one server, and I'm planning to have it for a long time. Why would I need automated provisioning? It seems like an extra step when I could just provision everything manually. There is no need to synchronise multiple hosts or anything like that. + +I view this kind of automation as executable documentation, or readable instructions for both the computer and the human. If I define the server in terms of these Ansible tasks, I can (in theory) reconstruct what's been done with the server just by reading the Ansible playbook. + +To be reliable, this necessitates that everything is defined in Ansible. Which happens to be very difficult to actually do. + +## Project structure + +There are many ways to structure an Ansible project. I picked one that I consider to be simple: + +- a single main playbook, with tags for selective execution of sub-playbooks +- a non-tracked `secrets.yml` that contains all secret values + - reasoning: this way I don't need to parse a separate configuration format and I get access to all features of YAML, including its data structures (lists, objects), which I would be lacking if I used a flat file format such as `.env`. +- a flat file hierarchy with names mirroring the target path, e.g. `templates/root_ssh_config.j2` goes to `/root/ssh/config`. +- no handlers and notify, because I want complete control over the execution order + +This is my first non-trivial Ansible project, so I wouldn't be surprised if I have some anti-patterns in there. + +The playbooks are designed to be fully _idempotent_: I can run the playbook again after the first run and expect the system to be in the same state. This is very important, since I want to be able to fearlessly run any playbook at any time and expect a success, or at least a helpful failure that doesn't compromise basic operation of the host. + +The only host in the Ansible inventory is my server. I run the playbook like this from my development laptop: + +```sh +$ ansible-playbook -i inventory playbook.yml +``` + +If I want to only update the `ingress` jail, I run: + +```sh +$ ansible-playbook -i inventory playbook.yml -t jail_ingress +``` + +## Separation of responsibilities + +Let's borrow and tweak the standard cloud infrastructure stack diagram that shows what parts of the stack are handled by the cloud provider and what parts are handled by the customer. + +My current approach is to use Ansible for the "core" parts of server administration, installing packages, defining service jails, etc: + +{{ fig(src="pursotin_stack.svg", alt="A stack model depicting the software layers in my server") }} + +Some notes about this setup: + +- The top "application layer" is currently a chaotic no-man's-land with varying levels of automation. Optimally I could rebuild all service jails completely with Ansible. +- The host OS release is managed manually, i.e. release upgrades are run manually with `freebsd-update`. This is fine IMHO. I will be supervising all the few and in between OS upgrades anyway, so automation does not help much there. + +## Why not Chef, Salt, etc? + +Ansible is very simple. It's pretty much a framework to write idempotent shell scripts without having to write shell scripts. Or Python scripts, whichever way you want to look at it. + +It's agentless. I don't have to run a service on the FreeBSD host to use Ansible. It uses SSH as transport and runs the tasks with Python, both of which are software that I expect to be present on any \*nix server. + +{{ fig(src="ansible_task_postgres.png", alt='A snippet of Ansible playbook installing PostgreSQL in the "postgres" jail') }} + +## How about containers? + +I'd image that most new server setups in 2025 are designed with container workloads in mind. This one is too, kinda. + +I isolate most services with FreeBSD jails, which are similar to OCI containers. Processes are isolated from other jails and the host automatically. Virtual networking (`vnet`) isolates the network. ZFS snapshots and clones provide thin jail filesystems that have a small storage footprint. Jails share the host kernel. + +Many OCI container concepts translate well to jails, but jails are somewhat more _persistent_. Conventionally, jail filesystems are not ephemeral and survive jail restarts. + +Only `ssh`, `bash`, `vim` and some other packages are installed on the host directly. Everything else is 🔐 _incarcerated_ 🔐. + +{{ fig(src="pursotin_jls.png", alt='A list of running jails on the server') }} + +### FreeBSD OCI container support + +FreeBSD was [recently welcomed](https://forums.freebsd.org/threads/an-introduction-to-oci-containers-on-freebsd.99907/) into the prestigious club of platforms that can run OCI containers. This was achieved by using jails as the container runtime and `podman` as container manager. + +This pretty much means that we can use Dockerfiles/Containerfiles to define our jails. Existing Containerfiles cannot be used directly, since they are built on Linux base images, but they can be used as a starting point for building new, FreeBSD-based images. + +While this sounds very cool and promising, I want to let the implementation mature a bit before building everything on it. + +> I tried installing the `podman-suite` package and following the instructions, and promptly broke all of my networking, so yeah 😑 + +I'm sure OCI containers will be a valid and respectable approach to containerization on FreeBSD, after the kinks have been ironed out. + +## To be continued + +In the next part(s) we will be looking at the specifics of the jail setup, e.g. how to create jails using `jail.conf.d` scripts. diff --git a/static/files/pursotin_jls.png b/content/posts/home-server-part-5/pursotin_jls.png Binary files differindex 2f2aa94..2f2aa94 100644 --- a/static/files/pursotin_jls.png +++ b/content/posts/home-server-part-5/pursotin_jls.png diff --git a/static/files/pursotin_stack.svg b/content/posts/home-server-part-5/pursotin_stack.svg index 6d57c70..6d57c70 100644 --- a/static/files/pursotin_stack.svg +++ b/content/posts/home-server-part-5/pursotin_stack.svg diff --git a/content/posts/initial.md b/content/posts/initial.md deleted file mode 100644 index 2c59d79..0000000 --- a/content/posts/initial.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Initial Post -date: 2025-12-31 -description: Or, how to write a dummy blog post for testing the static site generation. -extra: - kind: note - keywords: - - initial post - - dummy blog post - - static site generation ---- - -## Foobar - -jea - -## benis - -asdasdas asdasdasd asd asd asd diff --git a/content/posts/it-doesnt-mean-that.md b/content/posts/it-doesnt-mean-that.md new file mode 100644 index 0000000..adf171f --- /dev/null +++ b/content/posts/it-doesnt-mean-that.md @@ -0,0 +1,42 @@ +--- +title: It doesn't mean that +date: 2024-03-31 +extra: + kind: note +--- + +<img src="/files/wikimedia_monad.svg" alt="A plain circle with a small dot in the center" style="max-height: 200px; margin-bottom: 0px" /> + +## When I learned my first programming language + +I learned about functions. + +A _function_ is a thing you can call. The function might then _return_ a value. + +## Then I learned about functional programming + +And learned that those words did not mean that at all. Or at least that was not their full, universal meaning. + +Those _functions_ were actually _procedures_. Or _subroutines_. A function should be mathematical and pure. A mapping from a type to another. + +_Returning_ was not optional. All functions return, explicitly or implicitly, since function application must be assigned a value. + +I learned new words too. + +_Functors_ are containers. _Monads_ are powered-up functors with useful computational context. + +## Now that I’m learning about category theory… + +…I’m learning that those words do not mean what I thought they meant, either. + +_Functions_ are apparently morphisms, or arrows, in the category of types. _Functors_ are not containers, they are morphisms between categories. They are also their own category. + +_Monads_ are — I haven’t gotten to monads yet. + +## I read that they have a Monad in philosophy + +And it means _god_. [¹](it-doesnt-mean-that#fn1) + +--- + +1. <a href="https://en.wikipedia.org/wiki/Monad_(philosophy)" id="fn1">More or less.</a> diff --git a/content/posts/macos-opening-git-commit-prompt-in-active-gui-editor.md b/content/posts/macos-opening-git-commit-prompt-in-active-gui-editor.md new file mode 100644 index 0000000..2d71dd4 --- /dev/null +++ b/content/posts/macos-opening-git-commit-prompt-in-active-gui-editor.md @@ -0,0 +1,38 @@ +--- +title: "MacOS: Opening Git commit prompt in active GUI editor" +date: 2025-01-29 +extra: + kind: note +--- + +This is a quick, neat thing I happened to find on accident. As a result of the application bundle system that MacOS uses ubiquitously, you can figure out the currently open application with the `__CFBundleIdentifier` environment variable. + +```bash +$ echo $__CFBundleIdentifier +dev.zed.Zed +``` + +This bundle identifier uniquely identifies an installed application. + +Many editors or IDEs (Zed in my example) allow you to open an integrated terminal. It can be convenient to use that same graphical editor as the `$EDITOR` for commands like `git commit` or `git rebase -i` instead of using `vim` or `nano` or some other TUI editor inside the integrated terminal. + +Use it like this: + +```bash +# $HOME/.zshrc + +if [ "$__CFBundleIdentifier" = "dev.zed.Zed" ]; then + export EDITOR="zed -w" +elif [ "$__CFBundleIdentifier" = "com.jetbrains.intellij" ]; then + export EDITOR="idea --wait" +fi +``` + +Alternatively, if your editor supports setting custom environment variables for the integrated terminal, you can do that and avoid having the if-else in your shell rc. I prefer to have all my environment shenanigans in one place so I chose not to follow that route. + +Note that your editor must support a flag like `-w` or `--wait` that blocks the editor command from exiting before you close the file. If you don't do that, you will see stuff like: + +```bash +$ git commit +Aborting commit due to empty commit message +``` diff --git a/content/posts/managing-dotfiles-in-git.md b/content/posts/managing-dotfiles-in-git.md new file mode 100644 index 0000000..0d4681a --- /dev/null +++ b/content/posts/managing-dotfiles-in-git.md @@ -0,0 +1,38 @@ +--- +title: Managing dotfiles in `git` +date: 2024-07-08 +extra: + kind: note +--- + +Dealing with dotfiles can be a nuisance. I use a method that I read about in [this Git tutorial](https://www.atlassian.com/git/tutorials/dotfiles) (which is in turn based on a Hacker News post). + +It is the most elegant approach I've come across. It does not need to clutter your home directory, it does not cause your shell prompt to show Git status info whenever you're in a subdirectory of `$HOME`. It stays out of the way, but is still instantly accessible whenever you need it and allows you to use the full power of Git. + +Check out the linked tutorial for the full rundown. Here's the core idea, with my personal improvements added on top. + +1. Use `git init --bare` to initialize a bare `dotfiles` Git repository somewhere on your file system. I used `$HOME/.dotfiles/`. This repository will track all of your dotfiles while hiding away in a convenient location, which is not your home directory root. +2. Add a shell alias that uses this newly created bare dotfiles repository: + +```bash +alias dot='/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME' +``` + +3. Configure the `dotfiles` repo to not show untracked files in status output: + +```bash +dot config --local status.showUntrackedFiles no +``` + +4. Add some prints to your shell `rc` file in order to be reminded when you have forgotten to sync your stuff: + +```bash +dotfiles_status="$(dot status)" +if ! echo "$dotfiles_status" | grep -q "nothing to commit"; then + echo 'There are dotfiles changes to commit. Run "dotfiles status" to see them.' +fi + +if echo "$dotfiles_status" | grep -q "Your branch is ahead"; then + echo 'There are dotfiles commits to push. Run "dotfiles log" to see recent commits.' +fi +``` diff --git a/content/posts/now-2023-winter.md b/content/posts/now-2023-winter.md new file mode 100644 index 0000000..f2e38f1 --- /dev/null +++ b/content/posts/now-2023-winter.md @@ -0,0 +1,33 @@ +--- +title: ❄️ Now, winter 2023 +date: 2023-01-30 +extra: + kind: now +--- + +## Currently + +- Working on this site +- Doing small renovations and fixes to the house +- Learning Chicken Scheme and using it to build [desmoctl](/projects#desmoctl) and [this website](/archive/deploying-the-garden) +- Reading _Category Theory for Programmers_ by Bartosz Milewski + +## Recently + +- Purchased a 100 m2 terraced house +- Upstreamed some fixes to [Spliit](https://spliit.app/) +- Started a project of reading more books. I never read much growing up so this is something I have to learn to do. Lately I've been into learning about programming languages with "big ideas", i.e. Lisp (programs as data), Forth (stack focus), Smalltalk (OOP). + +## Reading + +### 2024 + +- _A Retargetable C Compiler: Design And Implementation_ by David Hanson, Christopher Fraser. The book defines the `lcc` ANSI C compiler, which I have considered writing a custom backend for in the [ATK16](/projects#ATK16) project. +- [Beej's Guide to Networking Concepts](https://beej.us/guide/bgnet0/) +- _How to Win Friends and Influence People in the Digital Age_ by Dale Carnegie, a 2012 adaptation of the classic +- _Gödel, Escher, Bach: An Eternal Golden Braid_ by Douglas Hofstaedter + +### 2023 + +- _Essentials of Programming Languages_ by Daniel Friedman +- _Structure and Interpretation of Computer Programs_ by Abelson and Sussman diff --git a/content/posts/now-2024-fall.md b/content/posts/now-2024-fall.md new file mode 100644 index 0000000..95e846a --- /dev/null +++ b/content/posts/now-2024-fall.md @@ -0,0 +1,85 @@ +--- +title: 🍁 Now, fall 2024 +date: 2024-12-10 +extra: + kind: now +--- + +## Home + +- Dog dug up the yard and the new grass. Multiple times even. Planted some clovers to hide the bald spot. Fixed up a vine that fell down from the wall. +- Painted some rooms. The previous owner left them in a sorry state. +- Built a little bathroom shelf, a little nightstand and a sound diffuser. Getting the hang of woodworking. + +<style> +.gallery { + display: flex; + flex-flow: row wrap; + margin: -10px; +} +.gallery a { + flex: 1; + min-width: 250px; + margin: 10px; +} +.gallery img { + margin-bottom: 0; +} +.gallery label { + text-align: center; + margin: auto; + font-style: italic; +} +</style> +<p class="gallery"> +<a href="/files/diffuser_1.jpg"><img src="/files/diffuser_1.jpg" alt="Sawed and varnished wooden pegs waiting for assembly."></a> +<a href="/files/diffuser_2.jpg"><img src="/files/diffuser_2.jpg" alt="A plywood board with a single row and column of pegs assembled on top."></a> +<a href="/files/diffuser_3.jpg"><img src="/files/diffuser_3.jpg" alt="A complete sound diffuser, comprising a sizeable grid of differently angled pegs."></a> +<label>The sound diffuser being built.</label> +</p> + +## Relationship + +- Got engaged! + +## Quests + +I remember seeing some blog post somewhere that advocated, possibly a bit ironically, the concept of using **main quests**, **side quests** and **daily quests** in tracking your to-dos. I decided to give them a try. I now use main and side quests to keep track of my higher level objectives. These objectives function at the week or month level. I do not use daily quests. + +I’ve always found it hard to stick to to-do tracking schemes. They are often too complex and unable to track priorities or time spans in a way that would feel natural. Maybe this approach is simple and video-gamey enough to stick. + +## Live music 🤘🏻 + +- Valkeat, Ethereal Sin @ Lepakkomies +- Queendyster, Liturgy @ Kuudes Linja +- Havukruunu, Satanic North @ Tavastia + +## Video games + +Played: + +- Outer Wilds (this was a good game) and the DLC (not entirely my vibe) +- Plucky Squire +- Neon White + +Enjoyed watching: + +- Metal Gear Solids 1–4 in [Giant Bomb longplay format](https://www.youtube.com/playlist?list=PLXlhzeWIuTHLfGvMnnWLJK3DRLQwcTpwd) +- Ultrakill + +## Career + +- Finished some certifications: _GCP-ACE_, _AZ-900_, _AI-900_. + +## Reading + +- Still reading _Designing Data-intensive Applications_. Got sidetracked by a database project that was inspired by the book. + +## Top tunes + +- Djennaration – Liturgy +- The Ultrakill OST + +## Previous update + +- [🌞 Now, summer 2024](../archive/now-2024-summer) diff --git a/content/posts/now-2024-spring.md b/content/posts/now-2024-spring.md new file mode 100644 index 0000000..93b1e94 --- /dev/null +++ b/content/posts/now-2024-spring.md @@ -0,0 +1,83 @@ +--- +title: 🌱 Now, spring 2024 +date: 2024-04-22 +extra: + kind: now +--- + +Inches of snow are falling on the yard, covering any new green growths. The dog likes it, I don't. + +<video width="100%" controls> + <source src="/files/jojo-in-snow.webm" type="video/webm"> +Your browser does not support the video tag. +</video> + +## Travel + +- Attended the [QCon London 2024](../archive/qcon-london-2024-takeaways) in London +- Went to [Tahko](https://www.tahko.com/en/) for a company leisure trip to downhill ski and snowmobile + +## Music + +[BLEK](../projects#blek) was live for 3 hours on student radio Radiodiodi with a dungeon synth genre deepdive program. According to metrics, 20 people (!) were listening to our show, which was also our grand EP premiere. Big leagues. + +I bought a [cassette player](../archive/cassettes) and a bunch of metal and dungeon synth tapes, as well some empty ones from the Bezos shop. I'm planning to record BLEK's music on some of them. + +## Games + +We've been on a proper Ratchet and Clank binge, together with Marika. + +- Ratchet and Clank: Rift Apart +- Ratchet and Clank: Tools of Destruction +- Ratchet and Clank: Gladiator (Deadlocked) +- Also we played the original trilogy in 2023 + +Non-Ratchet: + +- Rollerdrome + +Tabletop games: + +- Started playing [Miru](https://hinokodo.itch.io/miru-an-analog-adventure-game) + +## Reading + +- _Category Theory for Programmers_ by Bartosz Milewski + - Turned out to be quite a bit more difficult than I expected. The first half was completely doable with my functional programming and limited math background, but the second half is a piece of work. I'll push through though. + +## Movies + +I went to see Dune 2. It was as ok as the first part. + +## Projects + +### ATK16 + +Picked up [ATK16](../projects#atk16) again, adding some major stuff: + +- A calling convention for functions: arguments in registers `RA`–`RG`, return value in register `RG` +- An emulator & step debugger in Python + Pygame for easier debugging +- A bump allocator for using the heap +- Ergonomics features in the assembler: + - A `@data` directive for storing data in a data segment + - A string datatype that is encoded as a length and data bytes in memory +- Some "standard library" stuff, like `memset` and `memcopy` +- A hardware monitor / shell-like program for interacting with the system in text mode +- An MMIO register for setting the interrupt flag in order to define critical sections for primitive interrupt concurrency + +I feel like a proper introduction post / documentation is long overdue 😅 I should fix that. + +### Garden (jan.systems) + +Rewrote the site generator flow. Previously, each file in the content directory was accessed and processed from start to finish separately. This meant that there was no knowledge of other notes available when rendering a note. This made stuff like a [_dynamic now page_](https://derekkedziora.com/blog/dynamic-now-page) impossible. + +Now, the flow starts by traversing the entire content directory and indexing it into an in-memory database. The database contains all the metadata and filesystem info of all the notes, as well as static files. This makes dynamic pages possible since the database can be queried at render time. This approach also improved performance, since I could reduce the number of filesystem reads and writes. + +Things I still want to add: + +- A bookmarking system, maybe somehow connected to the [liked](../liked) page? +- A project page structure (just a content thing, no code required) + +## Previous update + +[❄️ Now, winter 2023](../archive/now-2023-winter) diff --git a/content/posts/now-2024-summer.md b/content/posts/now-2024-summer.md new file mode 100644 index 0000000..e4d06c4 --- /dev/null +++ b/content/posts/now-2024-summer.md @@ -0,0 +1,54 @@ +--- +title: 🌞 Now, summer 2024 +date: 2024-07-18 +extra: + kind: now +--- + +## Home + +- Yard work: some small landscaping, changed the topsoil to something less dry, and planted grass and flowers. +- Purchased a **wine fridge** for some reason. This led directly to... + +## A new hobby + +- Started learning about wines. Organized a wine tasting. My top pick: Domaine Laroche Chablis 1er Cru Les Vaudevey 2022. + + + +### Career + +- I passed the **AWS Certified Developer Associate** certificate exam! A celebratory espresso martini was in order. Planning to do **Solutions Architect Associate** next. + + + +## Played games + +- Animal well +- Ratchet & Clank: A Crack in Time +- Snufkin: Melody of Moominvalley +- Cult of the Lamb +- [Miru](https://hinokodo.itch.io/miru-an-analog-adventure-game) +- Celeste + +## Finished reading + +- Finished _Category Theory for Programmers_ by Bartosz Milewski. Had to work through it twice, taking notes on the second pass. I wish I started taking notes from the get go. +- _Wine Simple_ by Aldo Sohm, wine fundamentals for beginners. +- Some CS papers: + - _Do Be Do Be Do_ by Sam Lindley et al. The paper defines a language called Frank that uses a novel algebraic effects based typed effect system instead of monads. Also has types for suspended calculations (think Haskell thunks). Similar to Koka I believe. + - _Cons should not eval its arguments_, a classic that paves the way for lazily evaluated languages. +- Japanese short stories (Yomu Yomu app) such as _人間椅子 (Human chair)_, a story about a carpenter that decides to live inside a chair that he’s made. Adapted to a vocabulary of 1500 words. + +## Started reading + +- _Designing Data-intensive Applications_ by Martin Kleppmann. + +## Top tunes + +- [maudlin of the Well - Gleam in Ranks](https://open.spotify.com/track/5cyKheZd5E3jBtjcflE7Gz?si=y-mOWdUVT36XOo0zl1kK_w&context=spotify%3Aalbum%3A2BwbUYJeuUsv6LUA6GZHB4) +- [predawka - erynias](https://youtu.be/wyOG7ZiVLLo) + +## Previous update + +- [🌱 Now, spring 2024](../archive/now-2024-spring) diff --git a/content/posts/now-2025-fall.md b/content/posts/now-2025-fall.md new file mode 100644 index 0000000..220e6ea --- /dev/null +++ b/content/posts/now-2025-fall.md @@ -0,0 +1,87 @@ +--- +title: 🍁 Now, fall 2025 +date: 2025-10-05 +extra: + kind: now +--- + +Late summer and early autumn have been a time of ups and downs. + +I lost my father to illness. The passing was expected, in a way, so I have had time to process things in advance, but when it actually happened it turned everything on its head for a while. + +Sorting out things after the death has taken a lot of time and effort, but things are now more or less in order. I have got a lot of support and help from my peers, and I feel I have let myself process the grief well. I try to focus on positive things. + +We've also been planning the wedding that's taking place in 2026. In many ways I feel out of my element, but things are moving forward. + +## Home + +A lot of renovations this summer. One of the biggest ones was the translucent roofing installation: + + + +After around six hours (including breaks), it's all done! Just in time for rain. + + + +You can see the gnarly reddish wall in the picture. That and a bunch of other grime and residue was cleaned off as part of a neighborhood exterior painting project. The place is looking a lot nicer now from the outside (no pictures of this I'm afraid). + +We also got some minor things done, like installing curtains to my fiancé's office room. I got some new toolboxes that I use to keep everything nice and organized, and hidden away when not needed. + +With all the materials and tools lying around our home has felt like a job site, but now that everything's done and cleaned up, I'm happy that everything got done as well as it did. + +## FreeBSD server + +I have been setting up a home server and writing a blog series about it. Check out parts [one](home-server-part-1) and [two](home-server-part-2)! My plan is to move things back from the cloud & other people's computers back to my home and self-host as many things as possible. + +## Books + +**Finished** + +- Robert Nystrom, _Crafting Interpreters_ +- Michael W. Lucas, _Absolute FreeBSD_ +- Michael W. Lucas, _FreeBSD Mastery: ZFS_ +- Michael W. Lucas, _FreeBSD Mastery: Jails_ +- Leo Brodie, _Thinking Forth_ + +**Started** + +- Benjamin C. Pierce, _Types and Programming Languages_ +- Adam Aleksic, _Algospeak_ + +## Games + +**Played** + +- Bomb Rush Cyberfunk +- Hollow Knight: Silksong + +**Started** + +- God of War: Ragnarök + +**Enjoyed as a VOD/live stream** + +- Watched CDawgVA's playthroughs of MGS 1–3 and Revengeance +- A lot of Silksong VODs and speedruns + +## Travel + +I late August I went on a company trip to Krakow, Poland for one weekend. A nice city with beautiful old town buildings, but sadly the weather was a bit dull. My favorite food spot was the [Black Duck](https://czarnakaczka.pl/) 😋. + +In September we had booked a trip to Italy with a group of friends. We stayed in Rome for a couple of days, enjoying the 30C heat and sights such as the Villa Borghese. We had already visited the Colosseum and the Vatican on earlier trips so we decided to avoid the queues and visit less popular places. It was the Jubileum year so many sights were jam-packed. After that we got a rental and drove ~500km south to Calabria, where we spent the next week at a really nice villa overlooking the sea. + + + +Later in September I attended EuroBSDCon 2025 in Zagreb, Croatia. A lot of great talks and lab sessions! I might write a conference notes post about the trip. + +So, a lot of traveling in a short timespan, so I've been feeling quite exhausted. The rest of the autumn is looking less hectic, so I might be able to work on projects more 🤔 + +## Anime + +- Puella Magi Madoka Magica +- Started Kill la Kill +- Chainsaw Man (still a couple of eps to go) + +## Previous update + +- [☀️ Now, summer 2025](../archive/now-2025-summer) diff --git a/content/posts/now-2025-summer.md b/content/posts/now-2025-summer.md new file mode 100644 index 0000000..2f999dc --- /dev/null +++ b/content/posts/now-2025-summer.md @@ -0,0 +1,110 @@ +--- +title: ☀️ Now, summer 2025 +date: 2025-06-15 +description: | + Here's three seasons of updates in one package. +extra: + kind: now +--- + + + +## AutereDB project + +I was reading [Designing Data-intensive Applications](https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/) and I came across a chapter where the author goes over log-structured database architecture. I knew about B-tree based systems, but this seemingly simpler approach was new to me. I was intrigued, but I also felt like I didn't fully grok how every cog in the theoretical database machine worked together. + +Therefore, I decided to write a quick MVP thing in Rust during a conference flight. The result is **AutereDB** ([Github](https://github.com/jantuomi/autere_db)). + +AutereDB is more like a database engine. It doesn't have a query language, authorization mechanisms, multi-table support or other higher-level features. It simply makes sure that records are written to disk as expected, and queries return the correct data. + +You can read more about the project in the `README.md`. + +I have no plans to really further this project. I feel it was a one-off to better grok how these things work. I consider this goal met; I am now more confident about reasoning about data systems. Many things that I learned the hard way during this project have already proved to be beneficial in my day job, so mission success. + +Help yourself to the `ARCHITECTURE.md` file in the repository, and have a laugh at how I made multiple major miscalculations early on in the project, and then scrambled to fix these as I realized my folly. I tried to document most of these in the architecture decision log, for posterity. + +## FemtoQueue project + +Another data-centric project I conjured up: a Python task queue library that uses files in the filesystem for task persistence, and `rename()` to atomically move tasks from one state to another. + +FemtoQueue is on [Github](https://github.com/jantuomi/femtoqueue), as well as on [PyPI](https://pypi.org/project/femtoqueue/) as a pip-installable package. + +I'm pretty proud of this one. The core idea fits in under 100 lines of no-dependency Python. The current version with QoL stuff and docstrings clocks in at around 350 LoC. I could see it being very useful for small projects where you need some kind of a durable queue for sending emails etc., but don't want to add a whole separate networked Redis-like thing, or Celery or a home-cooked SQLite-based solution. + +## Japan trip + +Me and my fiancée made a long-awaited trip to Japan in the spring. We landed in the KIX airport near Ōsaka right in the middle of the cherry blossom season, hanami. + +We traveled the central island, Honshū, for three weeks. Mostly by train, but also by bus. We visited Ōsaka, Nara, Kyōto, Gifu-Takayama, Kanazawa and finally Tōkyō. This route skips the popular south-coast bullet train connection, instead going north through the mountains. + + + +I got to finally test my Japanese. Speaking was very laborious at first, but it got way easier the more I got to practise. My vocabulary didn't improve that much, but what improved were things like _cadence_, _[aizuchi](https://en.wikipedia.org/wiki/Aizuchi)_, and ability to respond with more natural word choices. Big up to active listening and copying native speakers. + +We ate and drank well. We mostly gravitated towards traditional-style, or _washoku_, establishments. Our favorite meals were prepared by a sweet grandma in a hot springs guesthouse in the mountains. Simple and clean ingredients. + +Something to note: pickled veggies go great with meat-centered, hearty meals. We were eager to implement this in our home cooking as soon as we got home. + + + +We visited the obligatory tourist spots, like the _Thousand torī gates_ in Kyōto, and the _Dōtonbori shopping street_ in Ōsaka. We like seeing sights, but we know that our most memorable experiences have always come naturally from exploration and keeping an open mind. That's why we spent only a fraction of our time visiting Tripadvisor locations and most of the time wandering around, following local recommendations and keeping our eyes, ears and nostrils peeled. + +We are planning to travel there again, maybe on honeymoon. + +## FreeBSD home server + +I have recently started a project of setting up a small publicly accessible home server that will replace my DigitalOcean VPS in some time. The system will be very FreeBSD jail focused. I have just received the hardware and verified that the everything is up to par. Very hyped about this one. + +I am planning to write some posts about the progress. + +## Home + +A lot of small things need renovated. I guess this is what owning a house is. We have done a lot, and even more is still on the backlog. + +しょうがないね。 + +Going to install some patio roofing during the next couple of weeks, first time doing that. + +## Books + +Mostly FreeBSD stuff by [Michael W. Lucas](https://mwl.io/nonfiction/os). + +**Finished** + +- [Designing Data-intensive Applications](https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/) (took a long time to work through this one) +- FreeBSD Mastery: Storage Essentials + +**Started** + +- Absolute FreeBSD +- FreeBSD Mastery: ZFS +- FreeBSD Mastery: Jails + +## Games + +**Played** + +- Gato Roboto +- A Short Hike +- Cozy Grove (watched my fiancée play this one) +- Warhammer 40k: Boltgun + +**Enjoyed as a VOD/live stream** + +- Blue Prince +- Rainworld + +## Anime + +- Jujutsu Kaisen 0, S1, S2 +- FLCL Progressive +- Demon Slayer: Mugen Train +- Evangelion rebuild movies +- Blame! (movie) +- Puella Magi Madoka Magica +- Dandadan +- Chainsaw Man (still a couple of eps to go) + +## Previous update + +- [🍁 Now, fall 2024](../archive/now-2024-fall) diff --git a/content/posts/now-2025-winter.md b/content/posts/now-2025-winter.md new file mode 100644 index 0000000..9e6aa85 --- /dev/null +++ b/content/posts/now-2025-winter.md @@ -0,0 +1,122 @@ +--- +title: ❄️ Now, winter 2025 +date: 2025-12-16 +description: | + <p>Fall has gone by, fast. Mostly rain and darkness.</p> + <p>Managing the estate of my late father has taken up a lot of time I would've otherwise spent on hobbies.</p> + <p>Some decor and reno at home, some game programming, a lot of games. Game design has been on my mind a lot lately.</p> + <p>I also attended my first ever Finnish defence forces reserves repetition exercise. I had a good time!</p> +extra: + kind: now +--- + +## Metroidvania game dev + +🚨 Original game idea alert 🚨 + +I decided to make a 2D platformer with ability-gated progression. It's gonna have pixel art too. + +It's been a very long time since I made a complete game project. I don't care about how many players I'm gonna get. I just want to create a game, for me. + +I figured I should do some proper design upfront instead of doing the easy thing, i.e. jumping straight into programming mechanics. That's why I started writing a Game Design Document ASAP. With that, maybe I can come back to this project easier, after the inevitable plateau and loss of interest. + +Maybe some screenshots later. + +## Future of ATK16 + +An idea came to me in my sleep. I want to change the course of my computing project ATK16 and turn it into a stack-based, Forth-like system. + +Clearly I've been subconsciously processing ideas from Forth and [Uxntal](https://wiki.xxiivv.com/site/uxntal.html), since my current vision is a hodgepodge of features from those two: + +- an interactive REPL-compiler, able to compile native code on the fly +- a system-wide dictionary (map of symbols to code or data), giving structure to the linear memory +- reverse Polish notation, which falls naturally out of the stack machine design + +A basic session could look like this: + +```sh +ATK16 v0.1 +Dictionary usage: 10 KB + +: average3 (a b c -- avg) + + + 3 / ; +``` + +This would define a new symbol, `average3`, that computes the average of the top 3 elements on the stack and pushes the result on the stack. The stack could be implemented in hardware with a top-of-stack optimization: the top three slots live in fast registers, while the rest is in dedicated SRAM. This would allow fast access to the most important, i.e. topmost, elements of the stack. + +In addition, I'm thinking of doing some things a bit unorthodox: + +- The system dictionary could live in FRAM, which is non-volatile. All symbol definitions would then be automatically persistent, like files on a disk. This could be huge. +- A programmer board could then allow me to directly update this dictionary from my dev laptop, possibly by emulating the same REPL software that runs on the hardware. + +In fact, it might be possible to design this in a way that completely does away with assemblers and cross-compilers. I would just need an emulator and a bit of bootstrap assembly that implements a minimal REPL that can self-compile new symbols into the dictionary. Like this, I wouldn't need to worry about ensuring that the assembler and self-compiler work exactly the same: there is only one compiler implementation! I just emulate it on my laptop. Everything is implementation defined! + +## Home + +### ”Dopamine office” decor + +Office room number one is now pretty much complete. We tried to combine some of pastel colors and fun patterns, while keeping it smart and tidy. + + + +### Backdoor clothes rack and wall feature + +We use our apartment's back door a lot, more than the front door really. It's easier to take the dog out that way. + +Until now, there has been nowhere to really hang our outdoor clothes nicely. The dining table and its chairs have doubled as a clothing rack (not ideal). + +So we got the idea to repurpose the empty space in the corner, adding some visual interest with acoustic paneling as well as utility with some colorful "knobs" that you can hang coats and scarves on. + + + +And the final product (in completely different lighting): + + + +The white box is an IKEA shoebox. We keep some dog gear in there. + +### Cool dog print + + + +## Games + +**Played** + +- God of War: Ragnarök +- Cocoon +- Another Crab’s Treasure +- Super Metroid +- Metroid Dread +- Prince of Persia: The Lost Crown +- Ori and the Blind Forest + +## Anime + +**Finished** + +- Bocchi the Rock S1 +- Spirited Away (movie) +- Ghost in the Shell (movie) +- Kill la Kill + +**Started** + +- Dandadan S2 +- Spy x Family S1 + +## Tunes + +Mostly [Moomin music](https://youtu.be/Bf1pR4rkPK8). I went to see the _Muumimusiikkia_ show in Musiikkitalo (Helsinki) in November, and after that I haven't been able to get Shiratori's melodies out of my head. This is a positive problem. + + + +### Spotify Wrapped + +I feel like I'm using Spotify less and less. Might drop the subscription in the near future. I really don't like the policies they are driving. + + + +## Previous update + +- [🍁 Now, fall 2025](../archive/now-2025-fall) diff --git a/content/posts/opening-multiple-db-tunnels-with-aws-ssm.md b/content/posts/opening-multiple-db-tunnels-with-aws-ssm.md new file mode 100644 index 0000000..14d5508 --- /dev/null +++ b/content/posts/opening-multiple-db-tunnels-with-aws-ssm.md @@ -0,0 +1,67 @@ +--- +title: Opening multiple DB tunnels with AWS SSM +date: 2024-06-26 +extra: + kind: note +--- + +To open a tunneled connection to an AWS managed database, such as RDS or DocumentDB, the commonly recommended way is to use a bastion host and `aws ssm start-session`. The bastion host is an EC2 instance that is in the same VPC as your database. The AWS CLI command allows you to connect to SSM-enabled EC2 instances from your development machine using IAM authorization, without having to manage SSH keys. + +The script below connects to the specified PostgreSQL RDS and DocumentDB instances using a bastion host. Both databases will be accessible on a `localhost` port. Modify as necessary. + +```bash +#!/bin/bash + +# Configure to match your environment +BASTION_NAME="<YOUR_BASTION_INSTANCE_NAME>" +DOCDB_CLUSTER_NAME="<YOUR_DOCDB_CLUSTER_NAME>" +PG_CLUSTER_NAME="<YOUR_RDS_CLUSTER_NAME>" +DOCDB_PORT="27017" +PG_PORT="5432" + +# Fail on errors, do not allow use of unset variables +set -eu + +# Colors for nicer output +BB="\\033[34m" +RST="\\033[0m" + +# Fetch the endpoints URIs and bastion instance ID +DOCDB_ENDPOINT=$(aws docdb describe-db-clusters --db-cluster-identifier "${DOCDB_CLUSTER_NAME}" --query "DBClusters[0].Endpoint" --output text) +PG_ENDPOINT=$(aws rds describe-db-clusters --db-cluster-identifier "${PG_CLUSTER_NAME}" --query "DBClusters[0].Endpoint" --output text) +BASTION_INSTANCE=$(aws ec2 describe-instances --filter "Name=tag:Name,Values=${BASTION_NAME}" --query "Reservations[].Instances[?State.Name == 'running'].InstanceId[]" --output text) + +echo "DocumentDB endpoint: ${DOCDB_ENDPOINT}" +echo "PostgreSQL endpoint: ${PG_ENDPOINT}" +echo "Bastion host instance ID: ${BASTION_INSTANCE}" +echo "" + +echo -e "${BB}Starting SSM sessions with DocumentDB and PostgreSQL port forwarding in localhost in this terminal...${RST}" + +# DocumentDB port forwarding +aws ssm start-session \ + --target "${BASTION_INSTANCE}" \ + --document-name AWS-StartPortForwardingSessionToRemoteHost \ + --parameters '{"portNumber":["'${DOCDB_PORT}'"],"localPortNumber":["'${DOCDB_PORT}'"],"host":["'"${DOCDB_ENDPOINT}"'"]}' & +PID1=$! + +# PostgreSQL port forwarding +aws ssm start-session \ + --target "${BASTION_INSTANCE}" \ + --document-name AWS-StartPortForwardingSessionToRemoteHost \ + --parameters '{"portNumber":["'${PG_PORT}'"],"localPortNumber":["'${PG_PORT}'"],"host":["'"${PG_ENDPOINT}"'"]}' & +PID2=$! + +cleanup() { + echo "Caught CTRL-C, stopping both tunnels..." + kill $PID1 $PID2 + exit +} + +# Catch the interrupt signal (CTRL-C) +trap cleanup INT + +# Wait for both processes to exit +wait $PID1 +wait $PID2 +``` diff --git a/content/posts/pile-of-incomplete-projects.md b/content/posts/pile-of-incomplete-projects.md new file mode 100644 index 0000000..0d07ab1 --- /dev/null +++ b/content/posts/pile-of-incomplete-projects.md @@ -0,0 +1,18 @@ +--- +title: My pile of incomplete projects +date: 2025-04-28 +extra: + kind: note +--- + +The number of started projects and/or empty checkboxes in my to do list is getting out of hand. I have: + +- \> 7 hobby tech projects in a state where I need to put ”just a bit” more effort to them to get them to the finish line, +- 2 or 3 hobby tech projects that have an incalculable number of man-hours left, and +- a nebulous cloud of non-tech stuff that should be done (and preferably prioritized). + +Starting is easy (and fun), finishing is hard (and dull). Despite having internalized this, it is very tempting to step into the trap willingly and start another one. + +### And now? + +I’ll make the conscious effort to finish incomplete work before starting new projects. I’m going to be a lot stricter in the scoping of my DIY software: things that are supposed to be small should be very small indeed. No extra features. diff --git a/content/posts/qcon-london-2024-takeaways.md b/content/posts/qcon-london-2024-takeaways.md new file mode 100644 index 0000000..fa4638d --- /dev/null +++ b/content/posts/qcon-london-2024-takeaways.md @@ -0,0 +1,112 @@ +--- +title: QCon London 2024 takeaways +date: 2024-04-20 +extra: + kind: post +--- + +QCon London 2024 was a multidisciplinary software engineering conference in the London QEII Centre in April 2024. I attended with the intention to learn about what's going on in the software-sphere. Especially in terms of software architecture. + +This was also my first time in the UK so I had to do some touristing. The weather was a bit chilly, half-cloudy with some intermittent drizzle. + + + +## Common themes + +### Data products + +A term from data mesh architecture. Think of the data you produce as a product. A product has an audience and marketing. + +### Platform thinking, platform teams + +Enable your software teams by building a platform of supported programming languages/runtimes, CI/CD pipeline configurations, deployment clusters, etc. so that each team does not have to spend resources reinventing the wheel. + +Provide a _golden path_ (tried and tested technologies supported by the platform team) for new projects but do not smother your engineers' freedom of choice. + +### “GenAI/LLMs are not necessarily that useful” + +Probably comes as a surprise but the latest wave of AI hype isn't very substantiated. Issues of security, trust, quality, ownership and licensing. Keep a human in the loop. + +### Safe, performant and ecological + +Running software causes carbon emissions. Choose runtimes, programming languages and hosting options that minimize idle and active compute use. + +Avoid entire categories of vulnerabilities by picking safe languages such as Rust. + +## Most inspiring talks + +### **The Home Computer That Roared: How the BBC Micro Shaped Our World** by _Jeremy Ruston_ + +A deep history dive to the advent of home computing in the UK. Jeremy was involved in building the educational BBC Micro computer as well as TV children's show animations and Doctor Who games. Super inspiring stuff, especially since I can relate to many of the constraints Jeremy et al. faced since I'm working on my very constrained "ATK16" hobby CPU project myself. + +### **Thinking like an architect** by _Gregor Hohpe (AWS)_ + +Architects find connections. Between perspectives, between technologies, between people. + +The director's budgets, risks and customer success are connected to the engineer's refactorings, scalability and backups. + +Architects are an IQ multiplier, not the smartest person in the room. Architects make other people smarter. + +### **Architecting for Data Products** by _Danilo Sato (Thoughtworks)_ + +Move from _left-to-right_ architectures (operational ↣ analytics) to a data mesh. + +Make your data discoverable and usable via different protocols and formats. Make it self-serve (platform thinking). + +> 🙋🏼 Note: I didn't know much about data mesh literature before this talk. The most useful things I got from it were the words and terms to look up, such as the _DATSIS principles_, and the books to read (see [Reading list](#reading-list)). + +### **Building Your First Platform Team** in a Fast Growing Startup by _Jessica Andersson (Kognic)_ + +The spirit of DevOps has in part transmogrified into the idea of _software platforms_. DevOps as a term has degraded to just mean Ops in many places. +Empower product teams with an explicit platform. Each company has a platform, implicit or explicit. +A _base plaform_ should provide: + +- **CI/CD**: pipeline templates, runner images, best practices +- **Runtime**: programming languages and runtimes, but also container runtimes, orchestration +- **Observability**: distributed tracing, logging, monitoring, alerting + +## Reading list {#reading-list} + +- ⭐️ [Team topologies](https://teamtopologies.com/book) by _Matthew Skelton and Manuel Pais_ +- [Domain-Driven Design](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) by _Eric Evans_ +- [Data Mesh](https://www.oreilly.com/library/view/data-mesh/9781492092384/) by _Zhamak Dehghani_ +- [Architect Elevator](https://architectelevator.com/) by _Gregor Hohpe_ + - all the other books by Hohpe also + +## Word dump + +I did not recognize these words or concepts. + +- attestation testing, SLSA +- server-driven UI +- DATSIS principles +- data vault architecture +- source-aligned, consumer-aligned data products +- galaxy apps +- 2-step commit +- wardley map +- BPMN (business process model and notation) +- eBPF +- expand-contract pattern +- SCI (software carbon intensity) +- WASI (WebAssembly System Interface) +- Gartner hype cycle +- trust boundary (in context of LLMs) +- LLM agent +- shift left (in context of software platforms) + +## Food and drink + +### Fantastic Indian cuisine: [Dishoom Covent Garden](https://www.dishoom.com/covent-garden/) + +You don't get Indian food like this in Finland. Super flavourful, suitably spicy. Was handed a gratis chai to drink while queuing up in the drizzling rain too. Excellent customer service. + +### Real Ale Pub: [The Harp](https://www.harpcoventgarden.com/) + +Nothing mindblowing, just good ale and a comfy vibe. A good place to sit out the rain. + +### Bloody lovely: [Duke of Argyll](https://maps.app.goo.gl/21YHi1ZBrXohhczj9) + +I had to eat fish & chips at least once just to check it off my bucket list. I don't know if theirs is the best or even proper, but I enjoyed it a lot. + + diff --git a/content/posts/second.md b/content/posts/second.md deleted file mode 100644 index c7a8127..0000000 --- a/content/posts/second.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Second Post -date: 2025-12-31 -description: Another post -extra: - kind: note - keywords: - - second post ---- - -jabadabaduu diff --git a/content/posts/subjective-review-miru.md b/content/posts/subjective-review-miru.md new file mode 100644 index 0000000..ec3220c --- /dev/null +++ b/content/posts/subjective-review-miru.md @@ -0,0 +1,73 @@ +--- +title: "Subjective review: Miru" +date: 2024-07-14 +extra: + kind: note +--- + + + +## TL;DR + +It’s my favorite solo game, but it has some pacing problems. 8/10. + +## Miru, the solo analog adventure + +> 🎲 **Intended audience**: people who are familiar with basic tabletop role-playing game concepts. + +[**Miru**](https://hinokodo.itch.io/miru-an-analog-adventure-game) is a journaling solo RPG adventure by _Hinokodo_. + +Solo RPGs are pen-and-paper games that require only one player to play. Whereas most tabletop games require one player to take up the role of the game master or referee, solo RPGs replace this role with various mechanisms, such as an _oracle_ or a system of _random event tables_, paired with an increased player responsibility over the progression of the game. What would be ruled by the referee in a non-solo TTRPG, is resolved instead with a roll of the dice, or some other, potentially more mechanically curious procedure. + +Many solo games, Miru included, tell a single curated story. The focus can be very narrow: the game might take place in a specific location at a specific time, for instance. Miru does not allow you to create or customize your character either, which is par for the course. Solo systems are usually not generic enough to support arbitrary settings or adventure prompts. That is one of their strengths; the system can be tightly intertwined with the story in a way that would be inconvenient or even impossible in non-solo systems. + +Solo games are mechanically diverse and often involve innovative approaches to both visuals and game design. They also tend to be rather lightweight, including only the necessary rules or content in the often somewhat slim zine. Players often have to massage their brains a bit and deduce how some properties of the game interact. This is part of the appeal, for me at least. + +> I enjoy [The Soloist](https://soloist.substack.com/) and [Dan's News](https://danews.substack.com/) for my solo RPG news. + +_Journaling_ solo games are solo games that involve writing a journal of some kind as you progress. Miru asks you to write down a short rundown of what happened on each day of your adventure. You also draw the adventure map, hex by hex, on a blank map template. + +With definitions out of the way, let me start by saying that _Miru is the best solo adventure I have played_. The creative setting, the mechanics and distinctive visual identity come together to create a cohesive, well designed experience that I can wholeheartedly recommend. However, it is not perfect. There is a bit of a pacing issue during mid-game as well as some confusing or contradictory rules that are either explained by looking up the author's comments on the [itch.io](https://hinokodo.itch.io/miru-an-analog-adventure-game) page or not explained at all. These are minor issues, though, and can be circumvented with some _GM's fiat_ (which is of course also _player's fiat_ when you're flying solo). + +## First look + + + +Miru paints a picture of a dystopian, post-metropolitan society future where humans begrudgingly coexist with humanoid and animal-like robots. The story is kicked off in a scene where a farm worker robot kills a relative of the main character in an act of self defense when it evaluates their drunken hooliganism to be a threat. Stricken, the main character vows revenge and sets off to find out the truth about the machines’ origins. + +The game is a procedurally generated hex crawl with apocalyptic gear-scavenging mechanisms and straightforward combat with a dash of god-slaying thrown in for good measure. + +Hexes are generated by combining 3 oracle tables: + +> **_Hex type_** (mountain, grassland, swamp...) → **_Event type_** → **_Particular event_** + +Visiting a single hex takes one in-game day. The main gameplay loop is about generating a new hex, surviving the generated event, and planning your next move such that you don't lose too many resources, such as food or sleep. Near the endgame, this survival aspect turns into loadout optimization when you prepare for the final boss. + +There is also currency management. Money, or **bitliths** (a snarky reference to cryptocurrency), are used to purchase survival resources as well as equipment from villages. The available stock improves as you discover more villages: the first village has level 1 stock, the second one level 2, and all the following ones level 3. + +## My experience + +The beginning was excellent. I quickly [grokked](https://www.merriam-webster.com/dictionary/grok) the rules and the objective of the game. The first hexes were significant; you could never know if you're going to get important gear, or nearly die to a robot-wolf. The plot was nicely propelled forwards by special events that happened on predetermined dates, and character progression felt good. These events, as well as random events dropped little bits of lore here and there. As a big fan of From Software games and figuring out the world by reading item descriptions, this kind of soft worldbuilding hit the nail on the head. + +This well thought-out and balanced part of the game lasted maybe 10 to 20 in-game days. + +My entire playthrough lasted 41 days. In wall clock time, it lasted me over three of months. There wasn’t actually enough much gameplay to warrant multiple months of play; I just had an extended break near the near-endgame part. I felt that the pacing there was a bit off, every hex felt very same-y. I had obtained enough resources and gear for exploring to be very safe and so the random events began to mean less and less. Little by little, the grinding-adjacent preparation for the final boss fight started to feel repetitive, and so I inadvertently took a break from the game. + +I returned to the game just recently and decided that I'm just going to have a try at the boss. Screw getting the best gear, I will finish the game! This was a good call, since the battle was intense. I threw everything at my disposal at my opponent and was _just_ strong enough to defeat it with one (1) HP remaining! Talk about a close call. + +I will not spoil the final boss or the ending here, but I want to say a bit about it nevertheless. Depending on whether or not you picked up on the [solarpunky](https://en.wikipedia.org/wiki/Solarpunk) undertones of the game during your playthrough, the ending may come either as a bit of a surprise. It surprised me at least. + +There's also something that seems to be a hook for the next game in the series, [Miru 2](https://hinokodo.itch.io/miru-ii-an-analog-horror-game), subtitled "Analog Horror Game". The third installation, [Miru 3](https://www.kickstarter.com/projects/mimicpublishing/miru-3-an-analog-defense-game), is an "Analog Defense Game". I am left wondering what happens in the plot between parts two and three. + +I give Miru a solid 8 out of 10. The visuals, the theme, the mechanics, and the overall vibe are all excellent. The grindy bit in the middle was really the only thing that reduced my enjoyment somewhat. Note however that your mileage may vary. Everything is determined by a roll of the dice. + +## Mixed media addendum + +I love mixed media art. Combining disciplines and breaking traditional boundaries makes great art. + +With that in mind, the designer has released an official soundtrack called [Static Abyss](https://hinokodo.itch.io/static-abyss) for the Miru game series. The physical minidisk release of the soundtrack comes with some kind of game rules too, based on photos that I've seen. Sadly, it has sold out, possibly soon after its release in April 2023. The digital version is available on the itch.io page. + +The concept of a soundtrack for a physical game is badass. I own one physical release of such a thing, the Mörk Borg adventure [Putrescence Regnant](https://jnohr.itch.io/putrescence-regnant), which takes the form of a highly stylized LP disc and sleeve. Very cool. + + +Photo attribution: [Hinokodo](https://hinokodo.itch.io/static-abyss) diff --git a/content/posts/tech-stack-retro-prospective.md b/content/posts/tech-stack-retro-prospective.md new file mode 100644 index 0000000..af16cf6 --- /dev/null +++ b/content/posts/tech-stack-retro-prospective.md @@ -0,0 +1,41 @@ +--- +title: jan.systems tech stack retro/prospective +date: 2025-04-28 +extra: + kind: note +--- + +I have a website authoring system that involves Obsidian sync and a hand-crafted static site generator (SSG) [written in Scheme](../archive/deploying-the-garden) that: + +- syncs Markdown files to my VPS +- compiles Markdown source files into HTML pages using pandoc +- parses and processes Markdown frontmatter as metadata +- builds an RSS feed from the pages +- builds the blog archive page that shows a time-sorted list of blog posts +- builds the `/now` page based on the most recent post with frontmatter binding `type: now` +- copies the compilation artifacts and static files to the web server directory + +### I have a couple of qualms with this setup + +- Running Obsidian Sync requires a graphical X environment, as Obsidian is a GUI app + - I have successfully used [xpra](https://xpra.org/index.html) for this, since my VPS seems to have X installed (why, I'll never know). A properly headless server could use [xvfb](https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml) +- Refactoring scheme source code to weave new data through the compilation pipeline is kind of rough, and makes me long for static typing +- A full build is slow (several seconds), mainly because of pandoc, but also because of interpreted scheme +- A change in one file triggers a full build, since the build system does not understand incremental builds. +- Pandoc handles inline HTML in a very weird way when using specific tags. I have not looked into why. This makes writing HTML-infused Markdown very fragile since I have to constantly manually inspect the output HTML for quirks. +- Pandoc is very heavy in terms of disk space (I have a small VPS SSD) +- When making edits, CPU usage on the VPS easily jumps to over 50% + +### The good parts + +It works pretty well. There as some small issues here and there, but it works. + +### New feature: linklog + +I would like to add a new feature: rendering recent links from [my linkhut profile](https://ln.ht/~jant) on the `/liked` page (possibly to be renamed to `/linklog` or something). Adding this to the current setup is possible. Linkhut has a JSON API for this use case, so I would only have to 1) parse the JSON, and 2) render a HTML fragment. + +### Approach + +In the light of my [most recent post](../archive/pile-of-incomplete-projects) I should fight the urge to rewrite the thing in Rust. However, I really want to get rid of pandoc. + +As such, I will probably RiiR at some point, using a [markdown library](https://github.com/wooorm/markdown-rs) instead of pandoc. diff --git a/content/posts/theres-an-rss-feed-now.md b/content/posts/theres-an-rss-feed-now.md new file mode 100644 index 0000000..8ec5448 --- /dev/null +++ b/content/posts/theres-an-rss-feed-now.md @@ -0,0 +1,10 @@ +--- +title: There's an RSS feed now +date: 2024-02-15 +extra: + kind: note +--- + +As part of the site build process, I use [`pandoc-rss`](https://github.com/chambln/pandoc-rss) to automatically build an RSS feed of my posts. + + diff --git a/content/posts/unfold.md b/content/posts/unfold.md new file mode 100644 index 0000000..593f6bf --- /dev/null +++ b/content/posts/unfold.md @@ -0,0 +1,42 @@ +--- +title: Automatic slideshows with reveal-unfold.js +date: 2024-04-21 +extra: + kind: project + slideshow: defined +--- + +## Lightning talk + +<p data-unfold-hidden> +I recently presented a lightning talk about the IndieWeb and personal websites to my colleagues at the office. Even though I don't normally enjoy presenting, speaking about things I'm passionate about always turns out to be rather easy. The talk and the presentation that I used to support the talk were well received. +</p> + + + +--- + +## Tinkering + +<p data-unfold-hidden> +Later, when I shared my PowerPoint slides in the IndieWeb chat, I was asked if I had considered using HTML slides instead of pptx. The thought had crossed my mind, but I didn’t have time to put the thought into practice before my talk. It was a curious idea though. +</p> +<p data-unfold-hidden> +So curious in fact, that I decided to study up on the ”HTML slideshow landscape”: what kind of tools there are, how do they work on a static site, things like that. Turns out that a tool called `reveal.js` is very popular and often recommended. An idea entered my mind: what if I could turn an HTML note (like this one that you’re reading) into an HTML slideshow, in a somewhat automated fashion? +</p> + + + +--- + +## Literate slideshow + +- A portmanteau of _literate program_ and _slideshow_ +- Convert a semantically marked up post to a slideshow with a click +- This post is a literate slideshow + +--- + +## Direction + +I don't know if I'll even need this project. Maybe I'll use it in some posts in the future? diff --git a/static/files/.DS_Store b/static/files/.DS_Store Binary files differindex 5b714be..37a4f07 100644 --- a/static/files/.DS_Store +++ b/static/files/.DS_Store diff --git a/static/style.css b/static/style.css index 03578b1..838f1a9 100644 --- a/static/style.css +++ b/static/style.css @@ -26,8 +26,7 @@ body { --dark--link-color: #5da9e9; --dark--blockquote-color: #eff1f8; --dark--blockquote-bg-color: #ffffff10; - --dark--code-bg-color: #ffffff0d; - --dark--h2-bg-color: #6d326d40; + --dark--code-bg-color: #ffffff00; --dark--syntax-0: #eff1f8; --dark--syntax-1: #5da9e9; @@ -40,8 +39,7 @@ body { --light--link-color: #3980bc; --light--blockquote-color: #1a1a1a; --light--blockquote-bg-color: #eff1f866; - --light--code-bg-color: #eff1f8; - --light--h2-bg-color: #6d326d1a; + --light--code-bg-color: #282a36; --light--syntax-0: #1a1a1a; --light--syntax-1: #6d326d; @@ -58,12 +56,6 @@ body { --blockquote-color: var(--dark--blockquote-color); --blockquote-bg-color: var(--dark--blockquote-bg-color); --code-bg-color: var(--dark--code-bg-color); - --h2-bg-color: var(--dark--h2-bg-color); - - --syntax-0: var(--dark--syntax-0); - --syntax-1: var(--dark--syntax-1); - --syntax-2: var(--dark--syntax-2); - --syntax-3: var(--dark--syntax-3); } } @media (prefers-color-scheme: light) { @@ -75,12 +67,6 @@ body { --blockquote-color: var(--light--blockquote-color); --blockquote-bg-color: var(--light--blockquote-bg-color); --code-bg-color: var(--light--code-bg-color); - --h2-bg-color: var(--light--h2-bg-color); - - --syntax-0: var(--light--syntax-0); - --syntax-1: var(--light--syntax-1); - --syntax-2: var(--light--syntax-2); - --syntax-3: var(--light--syntax-3); } } @@ -92,12 +78,6 @@ html.dark { --blockquote-color: var(--dark--blockquote-color); --blockquote-bg-color: var(--dark--blockquote-bg-color); --code-bg-color: var(--dark--code-bg-color); - --h2-bg-color: var(--dark--h2-bg-color); - - --syntax-0: var(--dark--syntax-0); - --syntax-1: var(--dark--syntax-1); - --syntax-2: var(--dark--syntax-2); - --syntax-3: var(--dark--syntax-3); } html.light { @@ -108,12 +88,6 @@ html.light { --blockquote-color: var(--light--blockquote-color); --blockquote-bg-color: var(--light--blockquote-bg-color); --code-bg-color: var(--light--code-bg-color); - --h2-bg-color: var(--light--h2-bg-color); - - --syntax-0: var(--light--syntax-0); - --syntax-1: var(--light--syntax-1); - --syntax-2: var(--light--syntax-2); - --syntax-3: var(--light--syntax-3); } html { @@ -129,6 +103,18 @@ h5, h6 { color: var(--header-color); line-height: 1.2; + margin-top: 1.4em; +} + +h1 a, +h2 a, +h3 a, +h4 a, +h5 a, +h6 a { + color: inherit !important; + font-style: inherit !important; + font-weight: 600 !important; } a, @@ -174,17 +160,26 @@ a:focus { font-weight: 700; } +.latest-posts { + margin: 35px 0; +} + code { padding: 3px 5px; - white-space: nowrap; border: 1px solid var(--blockquote-color); + white-space: nowrap; } -pre, -div.sourceCode { +pre { padding: 8px 10px; overflow-x: scroll; color: inherit; + border: 1px solid var(--blockquote-color); +} + +pre code { + white-space: inherit; + border: none; } @media screen and (max-width: 600px) { @@ -368,109 +363,278 @@ p { line-height: 1.5; } -/* Syntax highlighting, overrides Pandoc generated styles */ -code.sourceCode span { - color: var(--syntax-0); +/* Highlighting with Dracula theme as the base */ +.z-code { + color: #f8f8f2; + background-color: var(--code-bg-color); } -code span.al { - color: var(--syntax-1); - font-weight: bold; -} /* Alert */ -code span.an { - color: var(--syntax-0); - font-weight: bold; + +.z-comment { + color: #6272a4; +} +.z-string { + color: #f1fa8c; +} +.z-constant.z-numeric { + color: #bd93f9; +} +.z-constant.z-language { + color: #bd93f9; +} +.z-constant.z-character, +.z-constant.z-other { + color: #bd93f9; +} +.z-variable { +} +.z-variable.z-other.z-readwrite.z-instance { + color: #ffb86c; +} +.z-constant.z-character.z-escaped, +.z-constant.z-character.z-escape, +.z-string .z-source, +.z-string .z-source.z-ruby { + color: #ff79c6; +} +.z-source.z-ruby .z-string.z-regexp.z-classic.z-ruby, +.z-source.z-ruby .z-string.z-regexp.z-mod-r.z-ruby { + color: #ff5555; +} +.z-keyword { + color: #ff79c6; +} +.z-storage { + color: #ff79c6; +} +.z-storage.z-type { + color: #8be9fd; font-style: italic; -} /* Annotation */ -code span.at { - color: var(--syntax-3); -} /* Attribute */ -code span.bn { - color: var(--syntax-3); -} /* BaseN */ -code span.bu { - color: var(--syntax-3); -} /* BuiltIn */ -code span.cf { - color: var(--syntax-3); - font-weight: bold; -} /* ControlFlow */ -code span.ch { - color: var(--syntax-2); -} /* Char */ -code span.cn { - color: var(--syntax-3); -} /* Constant */ -code span.co { - color: var(--syntax-0); +} +.z-storage.z-type.z-namespace { + color: #8be9fd; font-style: italic; - opacity: 0.7; -} /* Comment */ -code span.cv { - color: var(--syntax-2); - font-weight: bold; +} +.z-storage.z-type.z-class { + color: #ff79c6; font-style: italic; -} /* CommentVar */ -code span.do { - color: var(--syntax-0); +} +.z-entity.z-name.z-class { + color: #8be9fd; + text-decoration: underline; +} +.z-meta.z-path { + color: #66d9ef; + text-decoration: underline; +} +.z-entity.z-other.z-inherited-class { + color: #8be9fd; + text-decoration: underline; font-style: italic; -} /* Documentation */ -code span.dt { - color: var(--syntax-1); -} /* DataType */ -code span.dv { - color: var(--syntax-3); -} /* DecVal */ -code span.er { - color: var(--syntax-1); - font-weight: bold; -} /* Error */ -code span.ex { -} /* Extension */ -code span.fl { - color: var(--syntax-3); -} /* Float */ -code span.fu { - color: var(--syntax-1); -} /* Function */ -code span.im { - color: var(--syntax-3); - font-weight: bold; -} /* Import */ -code span.in { - color: var(--syntax-3); - font-weight: bold; +} +.z-entity.z-name.z-function { + color: #50fa7b; +} +.z-variable.z-parameter { + color: #ffb86c; font-style: italic; -} /* Information */ -code span.kw { - color: var(--syntax-3); - font-weight: bold; -} /* Keyword */ -code span.op { - color: var(--syntax-2); -} /* Operator */ -code span.ot { - color: var(--syntax-0); -} /* Other */ -code span.pp { - color: var(--syntax-1); -} /* Preprocessor */ -code span.sc { - color: var(--syntax-2); -} /* SpecialChar */ -code span.ss { - color: var(--syntax-2); -} /* SpecialString */ -code span.st { - color: var(--syntax-2); -} /* String */ -code span.va { - color: var(--syntax-1); -} /* Variable */ -code span.vs { - color: var(--syntax-2); -} /* VerbatimString */ -code span.wa { - color: var(--syntax-3); +} +.z-entity.z-name.z-tag { + color: #ff79c6; +} +.z-entity.z-other.z-attribute-name { + color: #50fa7b; +} +.z-support.z-function { + color: #8be9fd; +} +.z-support.z-constant { + color: #6be5fd; +} +.z-support.z-type, +.z-support.z-class { + color: #66d9ef; + font-style: italic; +} +.z-support.z-other.z-variable { +} +.z-support.z-other.z-namespace { + color: #66d9ef; + font-style: italic; +} +.z-invalid { + color: #f8f8f0; + background-color: #ff79c6; +} +.z-invalid.z-deprecated { + color: #f8f8f0; + background-color: #bd93f9; +} +.z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { + color: #cfcfc2; +} +.z-meta.z-diff, +.z-meta.z-diff.z-header { + color: #6272a4; +} +.z-markup.z-deleted { + color: #ff79c6; +} +.z-markup.z-inserted { + color: #50fa7b; +} +.z-markup.z-changed { + color: #e6db74; +} +.z-constant.z-numeric.z-line-number.z-find-in-files { + color: #bd93f9; +} +.z-entity.z-name.z-filename { + color: #e6db74; +} +.z-message.z-error { + color: #f83333; +} +.z-punctuation.z-definition.z-string.z-begin.z-json, +.z-punctuation.z-definition.z-string.z-end.z-json { + color: #eeeeee; +} +.z-meta.z-structure.z-dictionary.z-json .z-string.z-quoted.z-double.z-json { + color: #8be9fd; +} +.z-meta.z-structure.z-dictionary.z-value.z-json + .z-string.z-quoted.z-double.z-json { + color: #f1fa8c; +} +.z-meta + .z-meta + .z-meta + .z-meta + .z-meta + .z-meta + .z-meta.z-structure.z-dictionary.z-value + .z-string { + color: #50fa7b; +} +.z-meta + .z-meta + .z-meta + .z-meta + .z-meta + .z-meta.z-structure.z-dictionary.z-value + .z-string { + color: #ffb86c; +} +.z-meta + .z-meta + .z-meta + .z-meta + .z-meta.z-structure.z-dictionary.z-value + .z-string { + color: #ff79c6; +} +.z-meta .z-meta .z-meta .z-meta.z-structure.z-dictionary.z-value .z-string { + color: #bd93f9; +} +.z-meta .z-meta .z-meta.z-structure.z-dictionary.z-value .z-string { + color: #50fa7b; +} +.z-meta .z-meta.z-structure.z-dictionary.z-value .z-string { + color: #ffb86c; +} +.z-markup.z-strike { + color: #ffb86c; + font-style: italic; +} +.z-markup.z-bold { + color: #ffb86c; font-weight: bold; +} +.z-markup.z-italic { + color: #ffb86c; + font-style: italic; +} +.z-markup.z-heading { + color: #8be9fd; +} +.z-punctuation.z-definition.z-list_item.z-markdown { + color: #ff79c6; +} +.z-markup.z-quote { + color: #6272a4; font-style: italic; } +.z-punctuation.z-definition.z-blockquote.z-markdown { + color: #6272a4; + background-color: #6272a4; + font-style: italic; +} +.z-meta.z-separator { + color: #6272a4; +} +.z-text.z-html.z-markdown .z-markup.z-raw.z-inline { + color: #50fa7b; +} +.z-markup.z-underline { + color: #bd93f9; + text-decoration: underline; +} +.z-markup.z-raw.z-block { + color: #cfcfc2; +} +.z-markup.z-raw.z-block.z-fenced.z-markdown .z-source { + color: #f8f8f2; +} +.z-punctuation.z-definition.z-fenced.z-markdown, +.z-variable.z-language.z-fenced.z-markdown { + color: #6272a4; + font-style: italic; +} +.z-variable.z-language.z-fenced.z-markdown { + color: #6272a4; + font-style: italic; +} +.z-punctuation.z-accessor { + color: #ff79c6; +} +.z-meta.z-function.z-return-type { + color: #ff79c6; +} +.z-punctuation.z-section.z-block.z-begin { + color: #ffffff; +} +.z-punctuation.z-section.z-block.z-end { + color: #ffffff; +} +.z-punctuation.z-section.z-embedded.z-begin { + color: #ff79c6; +} +.z-punctuation.z-section.z-embedded.z-end { + color: #ff79c6; +} +.z-punctuation.z-separator.z-namespace { + color: #ff79c6; +} +.z-variable.z-function { + color: #50fa7b; +} +.z-variable.z-other { + color: #ffffff; +} +.z-variable.z-language { + color: #bd93f9; +} +.z-entity.z-name.z-module.z-ruby { + color: #8be9fd; +} +.z-entity.z-name.z-constant.z-ruby { + color: #bd93f9; +} +.z-support.z-function.z-builtin.z-ruby { + color: #ffffff; +} +.z-storage.z-type.z-namespace.z-cs { + color: #ff79c6; +} +.z-entity.z-name.z-namespace.z-cs { + color: #8be9fd; +} diff --git a/templates/atom.xml b/templates/atom.xml new file mode 100644 index 0000000..b1721ce --- /dev/null +++ b/templates/atom.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}"> + <title>{{ config.title }} + {%- if term %} - {{ term.name }} + {%- elif section.title %} - {{ section.title }} + {%- endif -%} + </title> + {%- if config.description %} + <subtitle>{{ config.description }}</subtitle> + {%- endif %} + <link rel="self" type="application/atom+xml" href="{{ feed_url | safe }}" /> + <link + rel="alternate" + type="text/html" + href=" + {%- if section -%} + {{ section.permalink | escape_xml | safe }} + {%- else -%} + {{ config.base_url | escape_xml | safe }} + {%- endif -%} + " + /> + <generator uri="https://www.getzola.org/">Zola</generator> + {% if last_updated is defined %}<updated + >{{ last_updated | date(format="%+") }}</updated>{% endif %} + <id>{{ feed_url | safe }}</id> + {%- for page in pages %} + <entry xml:lang="{{ page.lang }}"> + <title>{{ page.title }}</title> + <published>{{ page.date | date(format="%+") }}</published> + <updated + >{{ page.updated | default(value=page.date) | date(format="%+") }}</updated> + {% for author in page.authors %} + <author> + <name> + {{ author }} + </name> + </author> + {% else %} + <author> + <name> + {%- if config.author -%} + {{ config.author }} + {%- else -%} + Unknown + {%- endif -%} + </name> + </author> + {% endfor %} + <link + rel="alternate" + type="text/html" + href="{{ page.permalink | safe }}" + /> + <id>{{ page.permalink | safe }}</id> + {% if page.summary %} + <summary type="html">{{ page.summary }}</summary> + {% else %} + <content + type="html" + xml:base="{{ page.permalink | escape_xml | safe }}" + > + {{ page.content }} + </content> + {% endif %} + </entry> + {%- endfor %} +</feed> diff --git a/templates/base.html b/templates/base.html index c000c1b..de01e9c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -61,8 +61,8 @@ font-style: italic; } </style> - <link rel="stylesheet" href="/style.css" /> + <link rel="alternate" type="application/atom+xml" diff --git a/templates/feed.xml b/templates/feed.xml new file mode 100644 index 0000000..4eaed63 --- /dev/null +++ b/templates/feed.xml @@ -0,0 +1 @@ +{% include "atom.xml" %} diff --git a/templates/index.html b/templates/index.html index fdcd433..31d0a90 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,7 +13,7 @@ <section class="latest-posts"> <h2>Latest posts</h2> <ul class="archive-list"> -{% for page in posts.pages %} +{% for page in posts.pages | slice(end=3) %} <li class="archive-entry"> <a href="{{ page.path }}">{{ page.title }}</a> <small>({{ page.date | date(format="%Y-%m-%d") }})</small> diff --git a/templates/macros.html b/templates/macros.html new file mode 100644 index 0000000..cb5efdc --- /dev/null +++ b/templates/macros.html @@ -0,0 +1,16 @@ +{% macro toc(page) %} +<nav class="toc"> + <ul> + {% for h in page.toc %} + <li><a href="{{ h.permalink }}">{{ h.title }}</a></li> + {% if h.children %} + <ul> + {% for c in h.children %} + <li><a href="{{ c.permalink }}">{{ c.title }}</a></li> + {% endfor %} + </ul> + {% endif %} + {% endfor %} + </ul> +</nav> +{% endmacro %} diff --git a/templates/page.html b/templates/page.html index 19f42a7..7120590 100644 --- a/templates/page.html +++ b/templates/page.html @@ -1,41 +1,21 @@ -{% extends "base.html" %} {% block content %} +{% extends "base.html" %} +{% import "macros.html" as macros %} +{% block content %} <h1> {{ page.title }}<br /> - <small>{{ page.date }}</small> + <small>({{ page.date }})</small> <small>[{{ page.extra.kind }}]</small> </h1> -{% block lede %} -<p>{{ page.description }}</p> -{% endblock %} - -{% block toc %} -<ul> - {% for h1 in page.toc %} - <li><a href="#{{ h1.id }}">{{ h1.title }}</a></li> - <ul> - {% for h2 in h1.children %} - <li><a href="#{{ h2.id }}">{{ h2.title }}</a></li> - <ul> - {% for h3 in h2.children %} - <li><a href="#{{ h3.id }}">{{ h3.title }}</a></li> - {% endfor %} - </ul> - {% endfor %} - </ul> - {% endfor %} -</ul> -{% endblock %} - -{{ page.content | safe }} +{{ page.content | replace(from="<!-- toc -->", to=macros::toc(page=page)) | safe }} <nav class="post-nav" aria-label="Post navigation"> - {% if page.lower %} - <div class="post-nav-left"><a class="post-nav-link" href="{{ page.lower.permalink }}">← {{ page.lower.title }}</a></div> - {% endif %} {% if page.higher %} - <div class="post-nav-right"><a class="post-nav-link" href="{{ page.higher.permalink }}">{{ page.higher.title }} →</a></div> + <div class="post-nav-left"><a class="post-nav-link" href="{{ page.higher.permalink }}">← {{ page.higher.title }}</a></div> + {% endif %} + {% if page.lower %} + <div class="post-nav-right"><a class="post-nav-link" href="{{ page.lower.permalink }}">{{ page.lower.title }} →</a></div> {% endif %} </nav> diff --git a/templates/section.html b/templates/section.html index c9ed8b1..b72f536 100644 --- a/templates/section.html +++ b/templates/section.html @@ -1,8 +1,18 @@ -{% extends "base.html" %} {% block content %} +{% extends "base.html" %} {% block content %} {% set posts = +get_section(path="posts/_index.md") %} <h1>{{ section.title }}</h1> -{% for page in section.pages %} -<ul> - <li><a href="{{ page.path }}">{{ page.title }}</a></li> +<p> + Follow via <a href="/atom.xml">Atom/RSS</a> (<a + href="https://aboutfeeds.com/" + >Huh?</a + >) +</p> +<ul class="archive-list"> + {% for page in posts.pages %} + <li class="archive-entry"> + <a href="{{ page.path }}">{{ page.title }}</a> + <small>({{ page.date | date(format="%Y-%m-%d") }})</small> + </li> + {% endfor %} {% endblock %} </ul> -{% endfor %} {% endblock %} diff --git a/templates/shortcodes/fig.html b/templates/shortcodes/fig.html new file mode 100644 index 0000000..4ac99b4 --- /dev/null +++ b/templates/shortcodes/fig.html @@ -0,0 +1,4 @@ +<figure> + <img src="{{ src }}" alt="{{ alt }}" /> + <figcaption aria-hidden="true">{{ alt }}</figcaption> +</figure> diff --git a/templates/shortcodes/toc.html b/templates/shortcodes/toc.html new file mode 100644 index 0000000..479abec --- /dev/null +++ b/templates/shortcodes/toc.html @@ -0,0 +1 @@ +<!-- toc --> |
