2025-11-02 03:19:11
My domain will be 30 years old in February. I have no idea how much I’ve spent on hosting and domain renewals in the past three decades. Certainly hundreds, if not over a thousand dollars, I imagine. For the past five years all the sites I have and support, have been hosted at Pair Networks.
Their price slowly crept up each year. I think it was initially around $102 per year (discounted since I paid a year at a time). Last year it was $136.20. A couple months ago I got an email from Pair touting their new “Platinum Mail” service. Email is no longer free. By setting up some forwarding rules, and eliminating one email address, I got the account down to two mailboxes. Which combined are $65.88 per year. Apparently the amount of storage used by our email exceeds some limit, as we paid an extra $1.50 for “additional mailbox storage.” The kicker is the charge for web site hosting.
Ready? $227.88. AFTER a $12 discount for paying a year at a time. This gets us 60 GB of disk space, 1000 GB monthly transfer, and 30 MySQL databases. That’s their “Shared Hosting 2” plan which is $19.99 per month. They have a “Shared Hosting 1” plan for $13.99 per month with 30 GB storage, 500 GB monthly transfer, and 15 MySQL databases.
Combining the new email charge, with the hosting fee, and you get a total of $295.26. A mere 116% increase over the bill from last year. To say I was shocked would be an understatement. I couldn’t believe that I had missed an email announcing the hosting cost. Searching back through emails from Pair I finally found one from last December that had a paragraph at the bottom about new rates. The top of the email looked like advertisement for features and plans. Apparently I archived the email without reading to the bottom.
Using Claude AI and ChatGPT I did some queries as to current popular webhosting providers. I also asked about their reputations. When I moved to Pair in 2020 I was leaving Bluehost as their service had become painful. One of Claude’s recommendations was initially Bluehost. I also considered something like an unmanaged VPS at Hetzner, or a droplet at Digital Ocean.
The roll-your-own approach could potential be much less expensive in dollars, but it would be far more time consuming in setup and maintenance. And, it would not solve the email problem as I am not crazy enough to host my own email server.
The managed hosting providers are heavily geared toward the WordPress crowd. Three of the sites I’d be migrating are WordPress-based, but the rest are static sites, or a PHP/JavaScript visitor tracking application and database. I could go crazy and host the static sites on AWS S3, but that doesn’t make hosting the WordPress sites any cheaper. May as well have all the eggs in one basket.
Using Dreamhost as an example, the annual hosting cost would be $3.99 per month the first year, and then $12.99 per month after that. That makes the full price $155.88 per year. They also charge for email, $1.67 per mailbox paid annually. Two mailboxes would be $40.08. So the annual bill from Dreamhost would be $195.96. Only $37.80 less that Pair’s Shared Hosting 1 plan, and $97.80 less that my current Shared Hosting 2 plan.
There is a non-zero cost in time and effort to migrate from one hosting provider to another. I have my detailed notes from the last two host migrations I’ve done. Really not looking forward to doing all that again.
I think I need to contact Pair to see if my current usage fits inside the limits of the less expensive plan. If it doesn’t, I have to decide if $98 a year is worth the time, effort, and stress of migrating or not.
2025-10-23 03:36:59
Like any configuration obsessed nerd I have a collection of “dot files”; configurations for the variety of tools, command line and otherwise, that I use. Initially having these stored in a Git repository was convenient for experimentation, but over time I have come to appreciate being able to quickly setup my preferred tools with my configuration on a new computer.
When the new computer is one of my own, having a single source for these configurations was fine. As soon as I started using these tools, and their configurations, on work computers, I started running into minor edge cases. Maybe I wanted a different font size for the desktop at work than on my personal laptop. Or, the user specified in the Git configuration needed to be different between work and home.
For a long time I tried having a branch called work to isolate changes that
were work specific. But this was tedious at times and so I stopped using it.
Finally I decided to have two sets of dot files. I created a new GitHub
account, expressly for work. And in that account I made a fork of my dot files
repository. Not only did this simplify the management of of my dot files, it
also allowed me to eliminate configurations from the work side that I only used
in my personal computing.
With the new repository I needed to update the git remote on the local repository. Running this command updates the remote:
git remote set-url origin [email protected]:<work-account>/dotfiles.git
Furthermore, I could add an upstream remote to the original repository:
git remote add upstream [email protected]:<personal-account>/dotfiles.git
Changing the original remote via the set-url command has some advantages:
Symlinks stay intact. The directory hasn’t moved, so ~/.config symlinks
continue to work
History preserved. All the local commits and branches remain.
No re-setup required. Everything stays in place.
Cleaner workflow. It is easy to sync between personal and work repositories if needed.
As a final step after updating the remote, do a git fetch to update
remote-tracking branches.
2025-10-21 10:20:57
For the second time this year, I’ve had joint replacement surgery. In January my right knee was replaced, and at the end of September I had my left knee replaced.
My right knee had been deteriorating for some time. After a year plus on daily prescription pain killers, I was having to take Tylenol in addition to the Mobic, in order to keep the pain to a minimum.
The first surgery was a success, but was not without its challenges. Originally scheduled for Monday, January 6, it had to be delayed until Wednesday, January 8, as a major snow storm dumped some 15 inches of snow on Sunday, effectively paralyzing the city.
In the run up to joint replacement you are given a lot of information. Never having experienced this before you don’t always get the full meaning or import of some things you are told. Staying ahead of the pain with your prescription medications, for example. We did not do a good job initially of keeping up with my pain. As a result I had sleepless nights, and I think my recovery took longer.
The second time around we quickly settled on a 6 am - 12 pm - 6 pm - 12 am schedule for the primary prescription, Hydrocodone. And a 9 am - 3 pm - 9 pm - 3 am schedule for the secondary prescription, Tramadol. I also wasn’t shy about taking two Hydrocondone, especially at bed time. The result has been an almost pain-free recovery. Yes, there is some muscle pain, and stiffness, but I’ve had none of the awake in the night in pain experiences this time around.
Recovery from any major surgery is not a linear progressing. Some days you make progress and feel pretty good, other days you feel like crap and wonder if this will ever end. It helps to have a good support system. I am fortunate to have my wife, Sibylle, here to help me. Not only has she helped with physical things like bathing, or bringing cold packs for my knee, she has been the clear headed, as opposed to drugged, half of this adventure. I’m grateful beyond words for everything she’s done for me throughout both of these surgeries and recovery periods.
I am also lucky to have an outstanding physical therapist. We won the PT lottery when the first therapist my wife had for her first hip replacement was a complete disaster. Through see luck of the draw we found someone who worked with us, and made recovery possible and, if not enjoyable, at least bearable. She is able to hold you to an expectation without being the prototypical physical therapist everyone loves to hate.
My progress that past two weeks has been good. This past weekend was the first setback, and that was only a minor one. I managed to upset the calf in my surgery leg, and it was painfully tight for two days. Today’s PT session was harder as a result, but ultimately good. I managed to gain a degree of straightness by the end of the session.
My first knee is perhaps 95% recovered. I still feel a little pull in my quads going down stairs. And, after two weeks of relying on my right left to lift me up from a seated position, it is a bit sore. When they tell you it can take up to a year for a knee replacement to fully heal, they aren’t kidding.
On paper I have another four weeks of physical therapy. The goal is getting my leg 100% straight, i.e., 0 degree bend, and at least 120 degrees of flex. Currently I’m at about 2 degrees and 104 degrees respectively.
I am pleased with my progress so far. I hope that there aren’t any major setbacks or unexpected challenges in the coming weeks. It will be nice to be back to normal activity in time for the Christmas and New Year’s holidays, if not for Thanksgiving.
2025-10-18 08:46:34
These notes are a result of having both of my knees replaced, one in January and the other in September 2025. I am not a medical professional, I can’t even spell arthrop… orthop…. total knee replacement. While I am trying to be humorous, there is some valuable information here.
Most likely you will have been given a spinal block, and probably had some injections directly in your knee to deaden nerve pain, prior to the surgery. These blocks will last for a while after surgery. Some effect will still be in place up to 24 hours later. This means you’ll be feeling little or no pain. Do not be lulled into thinking you can wait to take your prescription narcotics. Once you fall behind the pain it is very hard to catch up.
This is not the time to be frugal or skimpy. Inexpensive walkers are cumbersome and, I felt, actually impeded my walking. For about $130 you can get a good, folding, walker with wheels that turn, and a handy pouch to carry things with. Worth every penny. I recommend the Stander Wonder Walker Plus
Get a cane that is adjustable and has a four legged foot. It will stand by itself, and provides a better grip on the floor. Using a cane takes some getting used to, maybe practice a little ahead of time. I recommend the BeneCane
Really. Pick a schedule that works for you and stick to it. Use sticky notes as reminders. Get family members to help remind you. What worked for me was a 6 - 12 - 6 - 12 schedule. Pain medication at 6 am, noon, 6 pm, and midnight. That was for the primary narcotic (hydrocodone in my case). I had a secondary pain mediation, tramadal, that I took in between the hydrocodone doses. Roughly 9 am, 3 pm, and 9 pm. If I was up in the night (bathroom) around 3 or 4 am, I’d take a tramadal then too.
You will not be stable, or able to stand comfortably on your surgery leg for a few days. Get a shower seat, preferably one with handles/arm rests. You will not want to climb into and out of a tub. A walk-in shower is best. Ideally you’ll have a care giver who is willing to help you bathe.
Take them. Gradually ease back on the amount and frequency. Don’t be surprised if you have a set back or bad day that leaves you hurting more. When that happens, take more pain medication.
It will be a while before lowering yourself to the normal height of a toilet seat will be easy. Even with a raised seat that adds 3 inches to the height you’ll struggle at first to sit and to get up again. And, once you are used to the higher seat, you’ll likely keep it after your recovery is over.
They’re good drugs. The slightly fuzzy, floating feeling will honestly help you fall asleep.
As a prevention against blood clots, you’ll get to wear TED hose, or Thrombo-Embolic Deterrent stockings, for four weeks following your surgery. Getting one onto your non-surgery leg will be a nice workout. Getting one onto your surgery leg will require more effort. It is possible to have someone help, but pulling the sock up your leg and over your knee will be better controlled by you.
When the prescription says 1-2 every 4-6 hours, believe them.
Your leg is going to be one large bruise from mid-thigh down to your foot. At first there won’t be much, but within a day or two of getting home you’ll be astonished at the amount and size of your bruising. Any bruised spot that is warm to the touch needs to be iced.
Narcotic pain meds can’t be renewed without an okay from the doctor. If you are running low and will need more before the weekend is over, call your doctor by Thursday to ask for more. Insurance is sticky about paying for more than a 7 day supply.
Get some good reusable cold packs to put on your leg and knee. An old fashioned ice bag works well too. Fill it partially with crushed ice and a little water. It’ll mold to your knee or leg nicely that way. These Rester’s Choice flexible ice packs are superb.
Use pillows to elevate your leg as much as possible. Getting the knee above your heart several times a day for 45 minutes is the goal. Every minute will help.
Narcotics can make you nauseous which is miserable on top of being in pain. Doesn’t need to be a whole meal, but something in your stomach will help.
Take pain meds before PT. Plan on being tired afterwards. The goal is regaining flexibility and strength. Some of the quads are cut during the surgery and those muscles need to be strengthened. Your hamstring will be sore and tight. At first you won’t be able to get your leg 100% straight and it’ll only bend about 80-85°. The goal is 0° - I.e., completely straight, and about 120° bent.
Eventually you’ll be able to get by without the prescription pain medication, and can manage with over-the-counter meds. Be prepared to take a prescription one if you have a bad day.
My first knee surgery was nine months ago and I can still feel a slight pull in the quads going downstairs. I was told it can take a year to completely recover from knee replacement surgery.
Lean on your medical team. They want you to succeed, they want your quality of life to be improved following the surgery. They have answers for your questions.
2025-10-16 05:06:10
For work I have both a desktop (Mac Mini) and a laptop (MacBook Pro). As one would expect I use the laptop when I need to be away from my desk and the desktop computer. A considerable amount of my work happens in the terminal. By using tmux I’m able to create a session on the desktop and then remotely access it from my laptop.
This work fantastically, until the desktop gets a new IP address and I can’t remote in. If I’m at home I can walk to the other computer, and check its IP address. Annoying but no big deal. When I’m not at home, I’m stuck.
I created a bash script that checks the current IP address against a saved one. If they match nothing happens. If they are different the script creates a draft email that has the new IP address in it. And it posts a notification that the IP address has changed.
I used a draft email since I can access my mailbox from either computer; no need to send it, just open the draft and copy the new IP address.
The script is managed through a Launch Agent. It runs every five minutes. Worst case scenario is I’d have to wait 5 minutes to get the updated IP address. I think the only time I lose the current IP address is when my Aruba RAP (remote access point) loses power and reboots, so I’m not expecting to get IP address changes email drafts too often.
Here’s the bash script. It needs to be on the PATH and executable for this to work.
1#!/usr/bin/env bash
2# vim: ft=bash
3
4# File to store previous IP
5IP_FILE="$HOME/.current_ip"
6
7# Get current local IP (adjust interface if needed)
8CURRENT_IP=$(ipconfig getifaddr en0)
9
10# Fallback if en0 doesn’t work (e.g., on Ethernet it might be en1 or enX)
11if [ -z "$CURRENT_IP" ]; then
12 CURRENT_IP=$(ipconfig getifaddr en1)
13fi
14
15# Read stored IP
16if [ -f "$IP_FILE" ]; then
17 STORED_IP=$(cat "$IP_FILE")
18else
19 STORED_IP=""
20fi
21
22# Compare and notify
23if [ "$CURRENT_IP" != "$STORED_IP" ]; then
24 echo "$CURRENT_IP" > "$IP_FILE"
25
26 # Create and save a draft email with the IP address
27 osascript <<EOF
28tell application "Mail"
29 set newMessage to make new outgoing message with properties {subject:"Desktop IP Changed", content:"The new IP address is $CURRENT_IP", visible:true}
30 tell newMessage
31 make new to recipient at end of to recipients with properties {address:"[email protected]"}
32 end tell
33end tell
34EOF
35
36 osascript -e "display notification \"IP changed to $CURRENT_IP\" with title \"IP Monitor\""
37fi
Instead of using a cron job to manage the running of the script, the MacOS
preferred way is to use a Launch Agent.
The Launch Agent requires a plist file to define it.
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertiesList-1.0.dtd">
3<plist version="1.0">
4<dict>
5 <key>Label</key>
6 <string>com.user.ipmonitor</string>
7
8 <key>ProgramArguments</key>
9 <array>
10 <string>/Users/USERID/bin/bash/ip_monitor</string>
11 </array>
12
13 <key>StartInterval</key>
14 <integer>300</integer>
15
16 <key>RunAtLoad</key>
17 <true/>
18
19 <key>StandardOutPath</key>
20 <string>/tmp/ipmonitor.log</string>
21
22 <key>StandardErrorPath</key>
23 <string>/tmp/ipmonitor.err</string>
24</dict>
25</plist>
You’ll need to substitute your account ID for USERID on line 10.
The plist defines the script location, sets the interval for how often to execute it (5 minutes (300 seconds)), and defines locations for logging and error messages.
The plist needs to be copied to the user’s Library/LaunchAgent directory. I
had to create this directory on my computer.
cp com.user.ipmonitor.plist ~/Library/LaunchAgents/com.user.ipmonitor.plist
To launch the agent:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.ipmonitor.plist
The start/enable the service:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.ipmonitor.plist
To check if the service is running:
launchctl list | grep ipmonitor
To kick the service, if it is already loaded:
launchctl kickstart gui/$(id -u)/com.user.ipmonitor
Logs can be viewed:
tail -f /tmp/ipmonitor.log /tmp/ipmonitor.err
Make sure the plist file path is correct. Verify with:
ls -la ~/Library/LaunchAgents/com.user.ipmonitor.plist
Verify you updated the user name in the plist.
Check plist syntax
plutil -lint ~/Library/LaunchAgents/com.user.ipmonitor.plist
Ensure the script is executable:
chmod +x ip_monitor
If you still get errors after bootstrap, run:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.ipmonitor.plist 2>&1
This will show the actual error message, which will help troubleshoot the issue.
I have a Git repository with the script, plist, and a detailed README: ip_monitor
2025-09-02 20:10:49
I am using both GitHub and Codeberg for my Git repositories. It is possible to setup multiple push URLs for a given repository. This allows you to interact with both remotes with a single command. Here’s how.
# Set up origin at GitHub
git remote add origin [email protected]:zanshin/repo.git
# Add Codeberg as a second push URL to origin
git remote set-url --add --push origin ssh://[email protected]/zanshin/repo.git
git remote set-url --add --push origin [email protected]:zanshin/repo.git
Once the push URLs are set, running git remote -v will show one remote as the
fetch remote and both as push remotes.
origin [email protected]:zanshin/repo.git (fetch)
origin [email protected]:zanshin/repo.git (push)
origin [email protected]/zanshin/repo.git (push)
With this configuration, git pull will only pull from the fetch remote, in
the example above, GitHub. git push will push to both remotes.
Using Jujutsu, jj, you can push to all URLs using
jj git push --all-remotes
Using git for repository interactions there may be drift between the remotes,
since pulls are only from the fetch remote. Periodic pulls from the other
remote will be necessary to stay in sync.
Using Jujutsu, jj, for repository interactions allows you to use
jj git fetch --all-remotes
which will fetch from multiple remotes simultaneously.
The key point is that git pull only uses the fetch URL, while git push uses
all push URLs.