From 31969b417dfd40a4e4a2a8b3c896408a8df54209 Mon Sep 17 00:00:00 2001
From: Tobias Leinss <7684178+leinss@users.noreply.github.com>
Date: Wed, 26 Aug 2026 16:02:23 +0200
Subject: [PATCH 1/2] a11y: give every page an h1, and name the one unnamed
link
35 of 82 pages had no h1 at all. PageLayout takes a `title` but only puts it in
`
`, so four templates rendered their page title in a styled `div` and the
document had no top-level heading: the two dynamic routes, education and
social. A screen reader user landing on a project page got a document whose
first heading was an h2 belonging to the newsletter widget.
Promotes the existing title element from `div` to `h1` rather than adding a
second heading. Zero visual change, because Tailwind preflight resets heading
font-size and margin and every class is carried over. Verified in the browser
against the built site: the promoted h1 computes to font-size 18px, weight 600,
margin-top 0, which is what the div rendered.
Also adds alt text to the one image that had none, on the Farcaster project
page. That image is the only content of a link to a YouTube video, so with no
alt the link had no accessible name either.
Not changed, having checked: twelve other image-only links were flagged by a
first pass and are fine. An img with alt text names its wrapping link, and they
all carry `alt="demo image"`. That alt is uninformative and worth improving as
content, but it is not a missing name.
Test plan:
- [x] `pnpm run lint` and `pnpm run build` pass, 82 pages
- [x] internal link check still 1760 links, none broken
- [x] re-audit: pages with no h1 went 35 -> 0, images without alt 1 -> 0,
genuinely unnamed links 1 -> 0
- [x] computed style of a promoted h1 read from the rendered page, not assumed
---
src/content/projects/farcaster-hackathons/index.md | 2 +-
src/pages/[...slug].astro | 4 ++--
src/pages/education/index.astro | 4 ++--
src/pages/projects/[...slug].astro | 4 ++--
src/pages/social.astro | 4 ++--
5 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/content/projects/farcaster-hackathons/index.md b/src/content/projects/farcaster-hackathons/index.md
index 6cc0461b0..363086ae5 100644
--- a/src/content/projects/farcaster-hackathons/index.md
+++ b/src/content/projects/farcaster-hackathons/index.md
@@ -11,7 +11,7 @@ repoURL: ""
July 2024
-
+
Datalatte is a DataDAO managing a community knowledge graph that integrates public and private personal knowledge graphs as its core asset.
diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro
index 0ed981e4e..3e040f292 100644
--- a/src/pages/[...slug].astro
+++ b/src/pages/[...slug].astro
@@ -32,9 +32,9 @@ const { Content } = await render(post);
{readingTime(post.body ?? "")}
-
+
{post.data.title}
-
+
diff --git a/src/pages/education/index.astro b/src/pages/education/index.astro
index 43ecc824a..611e4c556 100644
--- a/src/pages/education/index.astro
+++ b/src/pages/education/index.astro
@@ -21,9 +21,9 @@ const education = await Promise.all(
-
+
🎓 Education
-
+
{
education.map((entry) => (
diff --git a/src/pages/projects/[...slug].astro b/src/pages/projects/[...slug].astro
index 4bcdbe7eb..dd481f6b9 100644
--- a/src/pages/projects/[...slug].astro
+++ b/src/pages/projects/[...slug].astro
@@ -44,9 +44,9 @@ const appSchema = softwareApplicationSchema(project, Astro.site);
{readingTime(project.body ?? "")}
-
+
{project.data.title}
-
+
{(project.data.demoURL || project.data.repoURL) && (
{project.data.demoURL && (
diff --git a/src/pages/social.astro b/src/pages/social.astro
index f94238aa4..643916666 100644
--- a/src/pages/social.astro
+++ b/src/pages/social.astro
@@ -12,9 +12,9 @@ const BSKY_PROFILE_URL = `https://bsky.app/profile/${BSKY_HANDLE}`
>
-
+
Social
-
+
Date: Wed, 26 Aug 2026 16:09:34 +0200
Subject: [PATCH 2/2] content: put the day job on /now, make /uses real, fix
the nav separator
Three things, all visible on the live site.
The nav rendered "now uses / work / projects". The separator was hardcoded
between the fixed entries only, while the static pages were mapped with
nothing between them, so any two of them ran together. Emitting it before
every mapped entry except the first is correct for one page or five.
/now did not mention the day job at all. It described the apps and the
consultancy and stopped, so the single most substantial thing on the page was
missing: senior backend engineer at Gnosis working on Circles, owning the
backend services and the platform under them. It now leads with that and
carries the figures already published on the CV, then the apps, then the
consulting. Also dated June and now two and a half months stale; refreshed,
and TobiBoard's F-Droid repository is linked because it is a thing a reader
can install.
/uses carried three `TODO(Tobias)` comments shipped in the content and
described a stack far smaller than the real one. It listed a Mac, zsh, fnm and
Astro, and omitted the entire infrastructure side, which is the professionally
interesting half. Now covers the terminal tooling, the mobile and distribution
stack including the self-hosted F-Droid repository, and the self-hosted
infrastructure: Ansible, Docker and Traefik, the Prometheus and Grafana and
Loki and Alertmanager stack, Tailscale, Borg, PostgreSQL with replication.
Deliberately not listed there, and the page says so: the identity,
DNS-filtering and intrusion-detection components. Naming those is a small
favour to somebody scanning and no favour to a reader.
The editor line is gone rather than guessed. One of the TODOs asked to confirm
it and I would only have invented an answer.
Test plan:
- [x] `pnpm run lint` and `pnpm run build` pass, 82 pages
- [x] internal link check passes, none broken
- [x] nav renders "now / uses / work / projects" in the built HTML
- [x] both pages served and screenshotted, headings h1 then h2 throughout
- [x] no TODO comments left in staticPages
---
src/components/Header.astro | 16 ++++++---
src/content/staticPages/now.md | 26 ++++++++++----
src/content/staticPages/uses.md | 62 +++++++++++++++++++++------------
3 files changed, 71 insertions(+), 33 deletions(-)
diff --git a/src/components/Header.astro b/src/components/Header.astro
index 5d4d7ae6d..00f41fb43 100644
--- a/src/components/Header.astro
+++ b/src/components/Header.astro
@@ -22,10 +22,18 @@ const staticPages = (await getCollection("staticPages"))
{
- staticPages.map((page) => (
-
- {page.data.title.toLocaleLowerCase()}
-
+ /* The separator belongs between the mapped links too. The hardcoded
+ spans below only ever separated the fixed entries, so with two
+ static pages the nav read "now uses / work / projects" and the
+ first two ran together. Emitting it before every entry except the
+ first keeps it correct for one page or five. */
+ staticPages.map((page, i) => (
+ <>
+ {i > 0 && {`/`} }
+
+ {page.data.title.toLocaleLowerCase()}
+
+ >
))
}
diff --git a/src/content/staticPages/now.md b/src/content/staticPages/now.md
index da0eef426..30a3935fd 100644
--- a/src/content/staticPages/now.md
+++ b/src/content/staticPages/now.md
@@ -2,15 +2,29 @@
title: "Now"
slug: "/now"
description: "What I'm working on right now."
-date: "2026-06-13"
+date: "2026-08-26"
---
-I build **privacy-first apps**: tools that run on your own device, work without accounts or a backend, and only reach the cloud when *you* bring your own AI key. No telemetry, no data collection, no lock-in.
+Two things have my attention: the infrastructure under a blockchain protocol, and a set of privacy-first apps that run on your own device.
-Most of my time right now goes into [TobiBoard](/projects/TobiBoard), an on-device Android keyboard with optional bring-your-own-key AI voice-to-text and text fixing, and [TobiReader](/projects/TobiReader), a local-first RSS reader with on-device read-aloud and optional AI summaries. Around those I keep a small portfolio of shipped apps: a cross-platform screen recorder, macOS voice-to-text and menu-bar utilities, and a few AI-assisted side projects. The full list is on the [projects page](/projects).
+## The day job
-The thread through all of it: see how far useful, AI-flavoured software can go while keeping user data on the user's own hardware. In practice that's two patterns: run models locally where it's feasible (on-device speech and text-to-speech, local LLMs via Ollama on the desktop), and bring-your-own-key for the rest, so anything cloud-bound uses *your* API key and endpoint, never mine.
+I'm a senior backend engineer at [Gnosis](https://www.gnosis.io), working on [Circles](https://aboutcircles.com). I own the backend services and the whole platform under them, from service code through deployment to monitoring: six hosts serving over 1.5 million requests a day, seven public services sitting at 99.99% uptime.
-I also do some consulting through [Leinss Consulting](https://consulting.leinss.xyz), helping service companies put generative AI to work, but building my own apps is the main focus these days.
+Most of what I've built there falls into three buckets. Four backend services written from the first commit, in TypeScript on PostgreSQL. Most of the protocol indexer, a C# plugin inside the execution client, whose query layer I rebuilt: p95 response times came down by between 5x and 141x depending on the call. And the platform itself, which moved off managed cloud onto bare metal, replacing Terraform and Kubernetes with thirty-eight Ansible roles that deploy blockchain nodes and PostgreSQL with automated failover, replication and pooling, host by host without downtime. Monthly cost fell by two thirds, and a merged change now reaches production in 10 to 20 minutes.
-Based in Germany. This is a [now page](https://nownownow.com/about); last updated June 2026.
+I care about the observability more than is probably normal. Two-tier health checks pull an unhealthy node out of DNS within 90 seconds, and a year of metrics means a regression is a question with an answer rather than an argument.
+
+## The apps
+
+Alongside that I build **privacy-first apps**: tools that run on your own device, work without accounts or a backend, and only reach the cloud when *you* bring your own AI key. No telemetry, no data collection, no lock-in.
+
+Most of that time goes into [TobiBoard](/projects/TobiBoard), an on-device Android keyboard with optional bring-your-own-key AI voice-to-text and text fixing, and [TobiReader](/projects/TobiReader), a local-first RSS reader with on-device read-aloud and optional AI summaries. TobiBoard ships from my own [F-Droid repository](https://leinss.xyz/TobiBoard/repo). Around those sits a small portfolio: a cross-platform screen recorder, macOS voice-to-text and menu-bar utilities, and a few AI-assisted side projects. The full list is on the [projects page](/projects).
+
+The thread through all of it is seeing how far useful, AI-flavoured software can go while keeping user data on the user's own hardware. In practice that's two patterns: run models locally where it's feasible, and bring-your-own-key for the rest, so anything cloud-bound uses *your* API key and endpoint, never mine.
+
+## And some consulting
+
+I also take on work through [Leinss Consulting](https://consulting.leinss.xyz), helping service companies put generative AI to work. Currently an e-commerce storefront with a back-office product management app, and dispatch and route planning for a fuel delivery business.
+
+Based in Germany, relocating to Asia. This is a [now page](https://nownownow.com/about); last updated August 2026.
diff --git a/src/content/staticPages/uses.md b/src/content/staticPages/uses.md
index 6bfa9f647..c970abf9f 100644
--- a/src/content/staticPages/uses.md
+++ b/src/content/staticPages/uses.md
@@ -1,46 +1,62 @@
---
title: "Uses"
slug: "/uses"
-description: "The hardware, tools and services I use to build my apps."
-date: "2026-06-12"
+description: "The hardware, tools and services I use to build my apps and run my infrastructure."
+date: "2026-08-26"
---
-A snapshot of what I build with. Inspired by [uses.tech](https://uses.tech). Everything here leans toward working offline and keeping data local, the same principle behind [my apps](/projects).
+A snapshot of what I build with. Inspired by [uses.tech](https://uses.tech). The bias throughout is toward things that work offline, keep data local, and can be rebuilt from a file in a repository rather than a click in a dashboard.
## Hardware
-- **Apple Silicon Mac**: main development machine, and where I test on-device LLMs locally.
-- Android devices for testing [TobiBoard](/projects/TobiBoard) and [TobiReader](/projects/TobiReader).
-
+- **Apple Silicon Mac**: main development machine, and where I run local LLMs.
+- **Android devices** for testing [TobiBoard](/projects/TobiBoard) and [TobiReader](/projects/TobiReader), driven over `adb` when a test needs to be repeatable.
+- **A small homelab**: a NAS, a mini PC and a Raspberry Pi, which between them run everything in the self-hosted section below. All of it is configured by Ansible, none of it by hand.
-## Editor & terminal
+## Terminal
-
-- **zsh** as the shell.
-- **fnm** for Node version management (a single `.node-version` keeps local and CI in sync).
+I live in the terminal, and most of the tooling is a replacement for something older that I got tired of.
+
+- **zsh**, with `eza` for `ls`, `bat` for `cat`, `fd` for `find`, `ripgrep` for `grep`, `delta` for `diff`, `dust` for `du`, `duf` for `df` and `btop` for `htop`.
+- **`jq` and `yq`** for anything shaped like JSON or YAML, **httpie** for HTTP by hand.
+- **direnv** for per-project environment, with secrets resolved from the macOS Keychain rather than sitting in a `.envrc`.
+- **chezmoi** for dotfiles, with **age** encrypting the parts that need it.
+- **fnm** for Node versions, a single `.node-version` keeping local and CI in sync.
+- **Claude Code** for day-to-day development.
## Web stack
-This site, and most of my web work, runs on:
+This site, and most of my web work:
-- **Astro** + **MDX** for content-driven sites
+- **Astro** and **MDX** for content-driven sites
- **Tailwind CSS** for styling
- **TypeScript**, linted with **ESLint**
- **pnpm** for packages, **Node 24**
- Deployed to **GitHub Pages**
-## On-device & AI tooling
+## Mobile and desktop
-- **Ollama** for running local LLMs on the desktop (e.g. the auto-tagging in [taggr](/projects/taggr)).
-- **On-device speech**: local transcription and text-to-speech for the mobile apps, so audio never leaves the device.
-- **Bring-your-own-key** cloud models for the optional AI features in my apps: the key stays on the user's device, and calls go to the provider *you* choose.
-- **Claude Code** for day-to-day development.
-
+- **Kotlin** for Android, **Swift** for macOS, **TypeScript** where a thing should run everywhere.
+- **A self-hosted F-Droid repository** plus GitHub Releases for distributing Android builds, so an install does not require a store account.
+
+## On-device and AI tooling
+
+- **Ollama** for local models on the desktop, including the auto-tagging in [taggr](/projects/taggr).
+- **On-device speech**: transcription and text-to-speech that run on the phone, so audio never leaves it.
+- **Bring-your-own-key** cloud models for the optional AI features in my apps. The key stays on the user's device and calls go to the provider *you* choose.
+
+## Infrastructure and self-hosting
-## Self-hosted services
+The professional half of this list, and the part I would defend hardest in an interview.
-I prefer to host the supporting infrastructure myself rather than hand data to third parties:
+- **Ansible** for everything. If a box needs a change, it happens in a role and gets applied, not typed into a shell.
+- **Docker** and **Traefik** for services and routing.
+- **Prometheus, Grafana, Loki and Alertmanager** for metrics, logs and paging, with **Blackbox** for external probes and **cAdvisor** for container-level detail. An alert that cannot fire is worse than no alert, so the rules have their own unit tests that run in CI.
+- **Tailscale** so nothing that does not need a public port has one.
+- **Borg** for backups, on a tiered schedule, with restores actually exercised.
+- **PostgreSQL** with replication, failover and pooling.
+- **Gitea**, **FreshRSS**, **SearXNG**, **Navidrome**, **ArchiveBox** and **ntfy**, all self-hosted, because I would rather run a thing than be a product inside it.
+- **Frigate** for camera detection and **ioBroker** for home automation, wired together so a detection reaches my phone.
+- **umami** for privacy-friendly, cookie-free analytics, and **Listmonk** for the newsletter.
-- **umami** for privacy-friendly, cookie-free analytics
-- **Listmonk** for the newsletter
-- A self-hosted **F-Droid** repository for distributing Android builds
+Deliberately not listed: the identity, DNS-filtering and intrusion-detection pieces. They are the parts of a homelab where naming the software is a small favour to somebody scanning, and no favour at all to a reader.