(((•)))
tech.interfluidity.com
a notebook by steve randy waldman
→ See also top, archive, subscribe, drafts blog, interfluidity main
2026-06-11 ⇒

# Feedletter dockerified

TL; DR: You can try `feedletter` dockerified here.

One thing you can say about software or "tech" is it's always changing. A relatively small fraction of the change, in my view, counts as progress, but there is so much change that even a small percentage accumulates pretty fast.

One of many, many trends in software that I never much mastered or paid attention to is "containerization". I am very accustomed to setting up old-school UNIX services (or kind of new school, since I tend to manage them under `systemd`). I haven't worked in the kind of high volume, dynamically scaling settings under which gadgets like Kubernetes makes sense. So, I've mostly ignored Docker and related technologies.

But, in my long and lonesome life, one sorrow I have is that software I write, good software I think, often ends up with a single lonesome user, me, even though most of what I write I publish as free software.

One thing I wrote that I think is good is `feedletter`. If you are seeing this note, it could very well be because feedletter pushed it to you, by e-mail, on Mastodon, or on Bluesky. Plus, I have ideas for feedletter. I mean to extend it to do a few more things pretty soon.

When I first published `feedletter`, I wrote a long tutorial for it on this website. In fact, there are three instructional posts regarding `feedletter` here:

- Feedletter tutorial
- Style-by-mail in feedletter
- Syndicating RSS to Mastodon and BlueSky with feedletter

Nevertheless, I can't pretend `feedletter` isn't a bit intimidating to set up and use. A person accustomed to hosting UNIX services should find it easy work, but it is some hassle.

So, I thought, why not try to learn a bit about Docker by Docker-ifying `feedletter`. Maybe it'd be an easier, less intimidating way for people to give the service a chance.

I've been meaning to try this for a while, but the fixed cost of all I'd have to learn dissuaded me.

But, now in the age of mysterious coding creatures, I thought why not see if I can't collaborate with Claude code and get it done quickly?

So that's what I did. I don't think what I've done counts as "vibe coding". I inspected all the artifacts Claude generated, asked a lot of questions, learned some stuff about Linux namespaces. Where my actual application code was touched, I mostly touched it myself, although Claude has been a very helpful reviewer and bug spotter.

But Claude did generate almost all of what's in the `feedletter-docker` repository, including its README.md.

I've worked through Claude's instructions there, and at this point everything seems to work fine, though we did have a few bumps along the way.

("We." It's a fascinating question whether that's the right pronoun.)

This mini project was, nevertheless, more time consuming than I expected. The coding — both the minor modifications I made to `feedletter` itself, and the artifacts Claude produced and edited with remarkable speed and accuracy — was a very small part of the time spent.

Testing was the pain point. I don't know how to automate spinning up a server, installing Docker, editing environment and config files, running dockerified admin commands, etc. Most of the time I spent running and rerunning the service, hitting hiccups (more my errors than Claude's!), and restarting the cycle. (I didn't spin up new servers and reinstall Docker every time, but I did try to start with a fresh clone of my repository, imagining I was one of you folk out there giving it a try.)

Still, the set up is much quicker than the native UNIX server. But some of the functionality is quirkily arranged to manage frictions presented by the boundary between a container and outside file systems and networks.

The default setup circumvents the hassle of configuring a web front end like `nginx` and getting SSL certificates, but at cost of letting `caddy` take exclusive control over ports 80 and 443, which many users will not abide.

Claude generated an alternative path to run the Dockerized service behind a reverse-proxy front end, so that your feedletter installation can be one virtual host among many. (That's how I run my real feedletter service, though in un-Dockerized form.) I have not tested Claude's scheme for Dockerized feedletter with external TLS yet, though.

Anyway, if this appeals to you, plase give it a try! I hope to have more to say about extensions to `feedletter` pretty soon.

p.s. "Claude" here was Opus 4.8.

# Updated: Feedletter tutorial

A significant update of Feedletter tutorial was made on 2026-06-11 @ 12:30 PM EDT.

> → Modify the tutorial to encourage use of new './feedletter generate-starter-untemplates', and document by default in terms of those.

The post was originally published 2024-01-29 @ 10:30 AM EST.

2025-11-08 ⇒

# Sysadminning in Scala, documenting in Claude

TL; DR: Me and Claude code collaborated on some documentation for my Scala sysadmin library. Is it any good? Maybe other people would use the library?

***

I don't like to do anything very complicated in a shell script. It feels loosey-goosey. I don't quite know what's going to happen, how things will behave if anything goes wrong.

I love doing just about anything in Scala, a programming language as tight and neurotic as I am. When I write Scala, I feel like I have pretty complete command over what happens. I find it easy to precisely check important prerequisites and provide detailed information about whatever goes down.

So I love automating sysadmin tasks in Scala.

Sysadmin tasks are high stakes. Often you are letting some piece of code run as root on production servers, doing something that really does need to be done. So I want to write my "scripts" neurotically. I want to have pretty complete and convenient information about what goes down when they run.

A few years ago I wrote a library in Scala to structure my automation of self-reporting sysadmin tasks. I get colorful HTML e-mails when they run, with a big `SUCCEEDED` or `FAILED` in the subject line.

Like most of the software I write, I made my library available as open-source, but had little hope or expectation that anyone else would actually use it.

I certainly didn't put in the time to write documentation that would make it easy for other people to use it!

This week I was messing with the library a bit, and it occurred to me that if I didn't have time to write documentation, maybe Claude would. So I asked Claude code to do just that.

I started with a pretty dumb prompt:

    > Could you write a comprehensive piece of documentation, in markdown, for developers interested in using TaskRunner?

Like so much in LLM-land, the result was incredibly impressive but also deflating. Claude code seemed to understand my code very well, and did a good job of explaining it. But there were errors, often small surprising things. Overall, I felt like Claude got all the hard-stuff right, but occasionally whiffed trivialities. Discerning and addressing those small mistakes meant the exercise was somewhat time-consuming.

It was not, to be clear, nearly as time-consuming or stressful as writing documentation by hand would have been. But we did go around with a bunch of revisions, and ultimately I saved less time than I'd initially imagined that I would.

At first I played manager to Claude the copy writer. I offered my observations and corrections in dialogue. Claude dutifully updated the text and fixed the errors.

(At one point, Claude was so attentive to my feedback that it offered to misstate the API in order to conform to my simplifying suggestion! I said no to that change.)

Eventually I went in and did some hand-editing.

The documentation that Claude produced is more long-winded than what I'd have written. But it is also more comprehensive.

Looking at Claude's examples, and how cumbersome they were, inspired me to change the API, significantly and for the better. That process seems like a contribution, even if you can't see it in the final product. (Claude cheerfully revised the code to the new, less cumbersome API, so that's all you see.)

Claude was quite ambitious. It created a vast "Complete Examples" section, just inventing common sysadmin tasks and writing scripts for them. I took that out because they were scripts I couldn't run to verify — they relied on file systems and resources that weren't mine — and just inspecting them I pretty quickly found some problems and bugs. I'd rather have fewer examples than mistaken examples.

Claude agreed removing the section was for the best. But Claude pretty much always agrees.

In any case, while I'm vain enough to think that if I'd written the documentation from scratch it would be better, I think what our collaboration produced is pretty good.

And I would not have taken the time to write the documentation from scratch!

It strikes me that a relatively good outcome for LLM usage would be that, rather than replacing labor, we just do more of things that might prove useful and valuable, but that would previously have been too time-consuming and speculative to justify the opportunity cost.

I'd love it if other people found my library useful. But I think the likelihood that even comprehensive developer docs will bring interest to this project is pretty low. Given that, without Claude, I just wouldn't have devoted the time.

But with Claude, (co)authoring decent if not fabulous documentation is faster and easier than it used to be. So maybe I'll document more of my projects this way. The likelihood that other people find something in my portfolio worth using, that library if not this one, might increase.

It wouldn't be the singularity, but I'd score it a win.

Anyway, here are pointers to the library and its Claude co-authored documentation, if you are interested.

2025-11-06 ⇒

# Zip-A-Dee-Doo-Dah

TL; DR: My two static sites (this site, `drafts.interfluidity.com`) can now be downloaded in full as zip files, for offline reading or archiving. Zip download links: tech, drafts

***

Last week (during "office hours"), I was asked why I was so fond of static sites and static-site generators for publishing writing. Aren't dynamic content management systems like WordPress or Drupal superior?

I really should have an answer to this! I now devote a large share of my time to an attempt to make static-site generation more functional and accessible for less technical authors.

There are a lot of reasons to prefer static sites. About a month ago, the venerable blogging service TypePad went down, and took with it the archives of some sites that are very important to me, like `economistsview.typepad.com` and `stublingandmumbling.typepad.com`.

There is no point making those names links. The sites are gone. Every link to them is broken.

(All is not entirely lost. The author of `stublingandmumbling.typepad.com` has moved to a substack.)

If these had been static sites, their authors could have simply downloaded the sites in full (as a zip file or something like that), and had them served from anywhere. Pretty much anything on the internet can serve a static site. Lots of services will host static sites essentially for free.

The infrastructure that serves static sites is the brainstem of the internet. WordPress constantly changes, is constantly under attack. It and the database that backs it require maintenance. A static site requires almost no maintenance beyond keeping the web server that hosts it on the internet.

If you (1) have a static site, and (2) own the domain name for the site, then the site is yours. You can move it whenever you want, without breaking links. In the age of the internet, it's so easy to "own" almost nothing. Everything is licensed, you are dependent on some vendor. Even when it's possible, it's often costly and inconvenient to extricate yourself and reconstruct what you've lost. Static sites, however, are freedom.

In addition to being movable, the fact that a static-site is basically a self-contained directory that can be wrapped into a zip file offers benefits to readers, not just to the site's owner. Readers can, at least in theory, download the whole site, for archiving purposes (including to keep the site publisher honest), or to read the work offline.

I've been embarrassed by that "at least in theory". My static sites — this one, and `drafts.interfluidity.com` — did not, in practice, make it easy for users to download the full site. Technical users have always been able to do so, since both are hosted as public git repositories. But there was nothing as simple as "download the site as a zip file".

So, now there is!

From the archive page of either site (tech, drafts), there are now download links, to a zip file updated nightly if there have been any changes. I've written scripts that maintain a zip directory for each site, that are hit every night by systemd timers, and then a service that provides the latest zip it finds in each of those directories. (As I like to do, this service is a script backed by a library, with the script effectively serving as configuration for the library.)

The zip files behind the links update themselves. The static sites need never change to stay current.

***

p.s. Sometime soon I'd like also to offer more-convenient-for-offline-reading epub files. The static sites now feature full-content, all-item RSS, which should be a straightforward source for building those.

2025-10-28 ⇒

# Turn your Bluesky archive into a readable, hostable static site with fossilphant

A couple of years ago, I wrote fossilphant, a utility for converting archives exported from Mastodon accounts into readable, self-hostable static sites. You can see what those look like here or here or here. (I've wandered across three “instances” on Mastodon!)

Now you can turn your Bluesky `repo.car` file into a static site as well. It supports embedded images, links, and quote-tweets too!

1. Install `scala-cli`. It's very easy!

2. Download two scripts, `download-bluesky-images` and `fossilphant-bluesky`. (To be sure you are using the latest version, you can download them directly from github.)

   Make the scripts executable:

       % chmod +x download-bluesky-images
       % chmod +x fossilphant-bluesky

   Try `./download-bluesky-images --help` and `./fossilphant-bluesky --help` to get a sense of the many options you won't need to use.

   Note: The first time you hit these scripts, they will download a bunch of stuff. That will only happen once.

3. Download your `repo.car` file. In Bluesky go to `Settings > Account > Export my data`.

4. Download your images into an images directory:

       % ./download-bluesky-images --repo repo.car --output images

   That may take a minute. When its done, you'll have a lot of images. Check out:

       % ls images

5. Now, build your static site!

       % ./fossilphant-bluesky --handle=interfluidity.com --images=images repo.car

   Note that you have to provide the DNS handle that you use for your Bluesky account. Your archive does not include this, just your AT-proto "did" ("decentralized identifier").

6. You'll find a directory called `public` has now appeared. Open its `index.html` file and behold!

You can take a peek at my archive here. That's it!

2025-08-30 ⇒

# Updated: The 'iffy' XML namespace

A significant update of The 'iffy' XML namespace was made on 2025-08-30 @ 06:50 PM EDT.

> → Revise definition of curation, so it has only three types: all, selection, and single. iffy:recent becomes a query under selection.

The post was originally published 2024-05-13 @ 04:10 AM EDT.

2025-07-27 ⇒

# Using Tailscale to proxy traffic to the US

I'm traveling, and found that some sites I needed to access forbid traffic from my location. In order to, like, pay a utility bill and renew some insurance, I needed my requests to appear to be coming from the United States.

Obviously, the easy thing would be just be to install one of a zillion VPNs that would let one do this. But they usually involve some subscription fee, this is a rare issue for me and I'm cheap, and I've been curious about Tailscale, which has a free personal-use tier.

Anyway, here's what I did:

1. Download the Tailscale app on my Mac (from the app store)

2. Run the app

3. Authenticate (I chose Apple as authentication provider) and give Tailscale VPN permissions

4. Install Tailscale on a US-based linux machine

   1. Check the linux version I'm running

      - I know it's a Debian, so run `cat /etc/*-release`

   2. I'm skittish about `curl -fsSL whatever | sh` style installations, which Tailscale first suggests for linux, so I ran (the not all that much safer) version-specific Tailscale installation.

   3. The `sudo apt-get` commands installed a new kernel. I rebooted after this part of the installation.

   4. Running `tailscale up` prints an authetication URL and then hangs. I copy the URL, then authenticate the Linux machine from my Mac laptop, by pasting it into my browser.

5. Enable IP forwarding for Tailscale, see the docs

   - For me this was the following, but do read the docs, it's system dependent:

         % echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
         % echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
         % sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

6. Allow my linux box to become an exit node with `tailscale up --advertise-exit-node`

   - Note there was no need to do a `tailscale down` before rerunning `tailscale up` with the `--advertise-exit-node` flag

7. On my Tailscale dashboard (link will only work when you're authenticated into Tailscale) my linux box is visitable and is marked as a potential exit node. Click "..." in the entry for the linux box, then "Edit route settings...", and check "Use as exit node."

8. In the Tailscale menu visible in the Mac menu bar when Tailscale is running, go to the "Exit Nodes" submenu, then select the linux box

9. Traffic is now being proxied through the US-located linux machine

10. Since I don't want (for now) to use Tailscale all the time, I turn Tailscale off:

    1. On the linux box `tailscale down`

    2. On the Mac, via the Tailscale menu, reset the exit node to "None"

    3. Turn the green "Connected" switch to gray "Not Connected"

    4. Via the Tailscale menu again, quit the Tailscale app

    Hopefully now I am back to ordinary networking.

I think (hope) that at any time, I can rerun the Tailscale app on my Mac, restore the "Not Connected" switch to "Connected", rerun `tailscale up --advertise-exit-node` on the linux box, then continue from Step 7 above.

On linux `tailscale status` will tell you whether `tailscale` is running. I reboot and check, to ensure Tailscale is not set to start-up on boot.

2025-05-26 ⇒

# Zero-ish overhead logging facade in Scala 3

The JVM has a lot of logging libraries. There's the built-in `java.util.logging` package, the venerable log4j project (now log4j2), logback, Scala-specific libraries like scribe, etc.

As a library author, I've long been partial to logging facades, where you write to one API, and can choose which among the multiple "backends" will log the messages.

The idea was pioneered by Apache Commons Logging. slf4j is I think the most popular logging facade these days. I long ago wrote my own facade, `mlog`, initially in support of the c3p0 library, but I've used it for many, many projects. I've also wrapped it beneath a concise Scala API that I really like.

JVM logging facades, if written with care, are quite performant in practice. They are not a meaningful bottleneck. Nevertheless, there is a bit of overhead. You call the facade API, bytecode is generated for those calls, which then transform inputs and forward them to a logging library.

I've been intrigued for a while by the possibility in Scala 3 of writing logging facades that eliminate this small bit of overhead. Scala 3 has a souped up inline functionality, by which facade API calls can be transfomed into back-end API calls at compile-time, eliminating any facade overhead at runtime.

I'm building a bunch of "daemon"-style services now, for which logging will be important, so it seemed like a good time to give this a try. The result is called `LogAdapter`.

The logging API style I developed for `mlog-scala`, which I am very happy with, is to call `log` methods directly on logging levels. The messages get routed to "loggers" determined by context (more specifically, by what Scala `given` or `implicit` values are in scope). For example, I might call:

    SEVERE.log("This is bad.")

`SEVERE` is a logging level defined in my `logadapter` package, but which is not defined in `log4j`, the backend I am using.

Although it looks like I'm calling a method on `SEVERE`, there is no sign of such a method in the generated bytecode:

         236: aload_0
         237: invokevirtual #103                // Method logAdapter:()Llogadapter/log4j2/LogAdapter;
         240: astore        22
         242: aload         22
         244: astore        23
         246: aload         23
         248: invokevirtual #107                // Method logadapter/log4j2/LogAdapter.logger:()Lorg/apache/logging/log4j/Logger;
         251: astore        24
         253: getstatic     #113                // Field org/apache/logging/log4j/Level.ERROR:Lorg/apache/logging/log4j/Level;
         256: astore        25
         258: aload         24
         260: aload         25
         262: invokeinterface #119,  2          // InterfaceMethod org/apache/logging/log4j/Logger.isEnabled:(Lorg/apache/logging/log4j/Level;)Z
         267: ifeq          284
         270: aload         24
         272: aload         25
         274: ldc           #138                // String This is bad.
         276: invokeinterface #125,  3          // InterfaceMethod org/apache/logging/log4j/Logger.log:(Lorg/apache/logging/log4j/Level;Ljava/lang/String;)V

The bit of bytecode referencing `logadapter` constructs is just a field lookup, which would be necessary even using the logging libraries directly. (Typically logging libraries reference a "logger" cached as a field of an object.) The field lookup is nested behind two accessors, it's like `this.logAdapter().logger()`.

While in Java, accessing a logger might mean accessing a `static` field directly, in Scala, even using the native libraries, the field lookup would be behind an accessor. So the only additional overhead is the second, inner accessor call. Using a back-end library directly might provoke only a single hit to an accessor. But the cost of the additional accessor call is negligible.

(And, a bit overzealously, the library actually economizes accessor calls on the backend by caching in ways typical library users do not. Note that `logger` and `Level.ERROR` are looked up once, rather than twice, in the bytecode above, although they are ultimately referred to twice, as in the code snippet below. These accessor calls are likely optimized away by modern JVMs at runtime anyway.)

Beyond looking up the logger, the bytecode refers only to construct of the backend library, `org.apache.logging.log4j` constructs in this case. There is no method call on a `logadapter.Level`. There are method calls on an `org.apache.logging.log4j.Logger` supplying an `org.apache.logging.log4j.Level` as arguments.

It's exactly as if we'd called the following methods directly on the `log4j2` library (referencing that library's `Level.ERROR`, since it does not support a level `SEVERE`):

    import org.apache.logging.log4j.*

    if logger.isEnabled( Level.ERROR ) then logger.log( Level.ERROR, "This is bad." )

Again, since logging facades are not really a performance bottleneck, there's not any spectacular improvement over using `mlog`. In informal testing of just the facade — that is, excluding the cost of actual logging, by sending messages at levels below the loggable threshold — I see a roughly 25% improvement using this resolved-at-compile-time facade over resolved-at-runtime `mlog`. In practice, the improvement would be even less, because many calls will hit logging IO (or add requests for logging IO to an asynchronous queue), rendering the facades, static and dynamic, a smaller part of the overhead.

Of course one of the benefits of facades — a reason why library authors often choose them! — is precisely because which backend gets hit can be resolved at runtime. If your library will be used as part of a larger application, it's nice if it can be configured to log to the same backend that the larger application will, without having to modify and recompile library code.

But the logadapter facade is so thin, there's very little cost to using it even if its "backend" will be a runtime-configurable facade like `slf4j` or `mlog`.

And for most applications, it's fine for the logging backend to be chosen at compile time. In which case the (small) runtime performance hit of a facade can be circumvented almost entirely.

`LogAdapter` is much simpler and smaller than traditional logging facades, and can be layered on top of them at low cost where that makes sense. I think it will be a pretty good foundation for logging.

2025-05-22 ⇒

# Scala 3 inline vs implicit ordering

I've been playing around with Scala 3's souped up `inline` construct, which is a very cool, relatively approachable, bit of metaprogramming.

(See "Rock the JVM" for a quick explainer.)

A thing that confused me, though, is the ordering of inlining vs implicit resolution. Consider...

    object InlineImplicitOrdering:
      given Int = 10

      inline def printIt(using i : Int) : Unit =
        println(summon[Int])

      object LocalContext:
        given Int = 22

        def printItLocally : Unit = printIt

      @main
      def go() =
        printIt
        LocalContext.printItLocally

It's straightforward that calling `printIt` prints `10`.

It's not so straightforward what `LocalContext.printItLocally` will do.

Implicit resolution is also a compile-time operation. If implicit resolution happens before the inlining, then `LocalContext.printItLocally` might print `10`. If implicit resolution happens after the inlining is resolved, then `LocalContext.printItLocally` should print `22`.

In reality, it prints `22`.

The output of this program is

    10
    22

I wondered whether this would always be the case, or whether adding modifiers might change this ordering. In particular I know that it's possible to declare `inline given`, rather than straight `given`, and I wondered whether this might cause the `printIt` function to collapse to `println(10)`.

The answer is no. As far as I can tell, there is no set of modifiers that would cause the implicit resolution to occur before the final inlining. Neither `inline given`, nor `transparent inline given`, nor marking the implicit argument `inline` has this effect.

As far as I can tell, the hard and fast rule is that inline resolution is completed prior to any implicit resolution. Implicits will be resolved at the ultimate, inlined call site, and never before.

Which is the behavior I find that I want! I am glad it does not seem to be fragile.

(I'd still like to understand what `inline given` is for, though.)

2025-04-03 ⇒

# Updated: Neonix

A significant update of Neonix was made on 2025-04-03 @ 09:10 PM EDT.

> → Add update regarding 'gum', ht Bill Mill

The post was originally published 2024-06-06 @ 03:35 PM EDT.