Welcome to my personal site. In addition to the posts you see below, feel free to explore other places where I stash content here. The now page is an occasionally-updated page about long-term plans and activities. I try to post several times a week and those updates appear on the journal page. Sometimes I post tidbits about things I learn in the til page. More info in: toolkit, readings, to learn, gallery, colophon, and changelog.


As a dual Canadian/U.S. citizen 🇨🇦 🇺🇸, I am opposed to the deeply corrupt and incompetent Administration of the U.S. government and urge Americans to vote all Republicans out of office.

Fixing a couple of vexing PartKeepr errors

I use PartKeepr to maintain my electronics laboratory parts inventory. The problem is that the technology is old and the project has been mothballed. There are hundreds of open issues on GitHub. It’s a little sad, but I have too much time and effort invested to give up on it now. The two errors are these: one pertains to an outdated database schema (it isn’t), and the other refers to a cron job that prepares usage tips.

PartKeepr error alerts

The cron job cannot run properly because the source of its JSON is defunct. It is perpetually looking for a URL that no longer exists, so it cannot complete and its execution log never appears in the database. The result is the same warning on every login.

The database schema error is a false positive caused by PartKeepr’s older Doctrine DBAL schema comparator. It sees a difference between the database schema and the entity definitions even though the MariaDB columns already have the expected definitions. Re-running setup does not help, because the comparator keeps generating the same ALTER TABLE ... CHANGE ... DEFAULT NULL statements.

Fixing the synctips cron job error

This was the most vexing error to fix. Trying to identify the right failure point to patch inside the Docker container led to a bunch of dead ends, so I instead installed a Violentmonkey userscript to nix the alert. Create a new userscript, replace its contents with this, then save and enable it:

// ==UserScript==
// @name         PartKeepr Warning Filter
// @match        http://192.168.5.109:5461/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const originalAlert = window.alert;

    window.alert = function (message) {
        if (typeof message === 'string' &&
            /cron/i.test(message)) {
            console.log('Suppressed PartKeepr warning:', message);
            return;
        }

        return originalAlert.call(this, message);
    };
})();

Reload PartKeepr and log in again. The @match entry must exactly match the address you use to access PartKeepr, including its scheme and port. Use your own IP address and port - this is mine. This userscript dismisses only alerts whose text includes cron; any other browser alerts still appear normally.

Suppressing the database schema error

PartKeepr determines this warning in /app/src/PartKeepr/CoreBundle/Services/SystemService.php. Its getSchemaStatus() method calls Doctrine’s schema comparator, and any generated SQL marks the schema as incomplete. The direct fix is to make that method return complete.

First, copy the application file out of the container and make a backup:

docker cp \
    partkeepr-partkeepr-1:/app/src/PartKeepr/CoreBundle/Services/SystemService.php \
    ./SystemService.php

cp -p SystemService.php SystemService.php.orig

Open SystemService.php in your editor. Search for the getSchemaStatus() method definition and replace the entire method with:

protected function getSchemaStatus()
{
    return 'complete';
}

Before installing the change, copy the file to a temporary path in the container and check its syntax:

docker cp SystemService.php partkeepr-partkeepr-1:/tmp/SystemService.php

docker exec partkeepr-partkeepr-1 \
    php -l /tmp/SystemService.php

The lint command should report No syntax errors detected. Copy the patched file into place and restore the original ownership and permissions:

docker cp SystemService.php \
    partkeepr-partkeepr-1:/app/src/PartKeepr/CoreBundle/Services/SystemService.php

docker exec -u root partkeepr-partkeepr-1 \
    chown www-data:www-data \
    /app/src/PartKeepr/CoreBundle/Services/SystemService.php

docker exec -u root partkeepr-partkeepr-1 \
    chmod 644 \
    /app/src/PartKeepr/CoreBundle/Services/SystemService.php

Finally, clear Symfony’s production cache and restart the container:

docker exec -u www-data partkeepr-partkeepr-1 \
    php /app/app/console cache:clear --env=prod --no-debug

docker restart partkeepr-partkeepr-1

The schema warning is gone after the restart.

Making the patch survive container recreation

A docker cp change survives a normal restart but is replaced when Docker recreates the container. Keep the patched SystemService.php and its .orig backup on the host, then mount the patched file through your Compose configuration:

services:
  partkeepr:
    volumes:
      - ./patches/SystemService.php:/app/src/PartKeepr/CoreBundle/Services/SystemService.php:ro

Add the mount alongside the service’s existing volume entries, put the patched file at ./patches/SystemService.php, and recreate the service. The patched schema-status check will then be applied whenever the PartKeepr container starts. I’m still a little unsure about this. If I had more time, I’d mess around with the source code on a fork and deal with it that way.


This is a janky workaround for an old application that still works for me otherwise. If you know of a cleaner way to fix PartKeepr’s warnings, or if you have questions or comments, please get in touch through my contact page.

Getting Codex to clean up after itself

Terminal listing Codex app-server-daemon release directories

A pet peeve of mine is software that doesn’t clean up after itself. If you are a Codex user, check out your ~/.codex/packages/app-server-daemon/releases I’m a relatively recent codex CLI user and already 12 releases of the app-server-daemon left there. Why? Laziness probably. If I left these files in place and they accumulated at this rate, we would have 219 of them one year in. They are around 330 MB apiece, so we are talking about 72 GB of wasted space.

Hazel to the rescue

On macOS when I have an automated filesystem cleanup or organizational task, I turn to Hazel. Just point Hazel to the directory you want to watch, give it some criteria and actions to perform on matching files and/or directories. My goal here is to keep only the two most recent releases. Honestly we could keep only the most recent release, but I’m being conservative. To clean up the releases, first add a folder to Hazel pointed to ~/.codex/packages/app-server-daemon/releases then a new rule “Clean previous .codex app server daemon releases”.

Add two (“All”) critera:

  1. Kind is Folder
  2. Passes shell script

For the script:

#!/bin/zsh

# Directory being evaluated
candidate="$1"
name="${candidate:t}"
parent="${candidate:h}"
platform='aarch64-apple-darwin'

# Check release-directory names without repeating a long expression.
is_release() {
    [[ $1 =~ ^[0-9]+\.[0-9]+\.[0-9]+-$platform$ ]]
}

is_release "$name" || exit 1

# Collect all valid release directories
versions=()

for path in "$parent"/*-aarch64-apple-darwin(N/); do
    version="${path:t}"

    if is_release "$version"; then
        versions+=("$version")
    fi
done

# Never delete anything if two or fewer versions exist
(( ${#versions} > 2 )) || exit 1

# Sort by numeric version components (ascending)
sorted=("${(@f)$(printf '%s\n' "${versions[@]}" |
    /usr/bin/sort -t. -k1,1n -k2,2n -k3,3n)}")

# Match only if this directory is NOT one of the newest two
[[ "$name" != "$sorted[-1]" && "$name" != "$sorted[-2]" ]]

Under the hood with the criterion script

Hazel supplies the full path of the folder it is evaluating as $1. The script saves that path in candidate, then uses zsh’s :t and :h modifiers to obtain its last path component (the release directory name) and its parent directory. Quoting those variables matters: it keeps the script correct even if a path contains spaces.1

The is_release helper is the guard rail. It refuses to match a candidate unless its name has the expected major.minor.patch-aarch64-apple-darwin form. The script calls it again while building the list, so an unrelated directory in releases cannot be selected for deletion. This version is deliberately specific to Apple Silicon. If Codex changes its release naming convention, or if you use an Intel Mac, adjust the platform value before enabling the rule.

(N/) is a zsh glob qualifier. The / limits the glob to directories, while N makes an empty match expand to nothing instead of producing an error. The versions array therefore contains only release directories that passed the name check.

The script exits with a non-zero status until there are at least three valid versions. Hazel treats that as a failed condition, so its Trash action cannot run. When there are three or more, sort compares the three numeric components of each version number in order. In zsh, sorted[-1] and sorted[-2] mean the last and second-to-last array items. The final test succeeds only for a directory that is neither of those two newest entries, which makes that directory eligible for the rule’s action.

We only match diretories that do not have the two most recent version numbers.

For the rule action, it’s simple Move to folder → Trash.

This is a modest workaround for a modest annoyance, but it is the sort of housekeeping that should not require attention every few weeks. Hazel keeps the release directory bounded, and moving old releases to the Trash leaves a recovery path if an update turns out to need an older copy.

I need to look for more leftover garbage. Where there’s one example of sloppiness, there’s probably more.

If you know of a better way to manage Codex’s retained releases, or have questions about the Hazel rule, please get in touch through my contact page.


  1. These are Zsh-specific expansion modifiers that won’t work in Bash; that’s why we have to specify /bin/zsh as the interpreter for the criterion shell script. ↩︎