swan tron dot com

CurbSpec: A Cheat Sheet for Buying Used

Every F-150 thread eventually turns into cam phasers. I have a 2017 SuperCrew with the EcoBoost, so I’ve read most of them.

Buying used is the worst version of that. You’re standing next to somebody else’s truck with your phone out, the seller says “oh, that’s been taken care of,” and you’ve got about five minutes to figure out what you’re actually buying. Plenty of sites will sell you a title report. Nobody tells you what that engine is about to cost you.

So I built CurbSpec. Punch in a VIN (or paste the listing) and the mileage, and you get one page: what’s due or overdue on that powertrain, the recalls and campaigns that actually apply to that truck, and the known issues worth asking about. $12.99, no account, no ads. It’s the before-you-buy cousin of wrenchtron.

Paid 2017 F-150 SuperCrew sheet.. 89,555 mi, due/overdue $1,760–$4,110

Wrong on My Own Truck

I paid for a report on my own 2017 to make sure checkout actually worked. It did. Then I read the phaser line.

It basically said “ask whether the repair was done.” That’s the dealer shrug.

Known issues on that same snapshot.. 21N03 as a yes-or-no

Turns out 21N03 wasn’t a recall, and it wasn’t a yes or no. It was a Customer Satisfaction Program that paid out by the miles on the truck when it was fixed.. 100% under 70k, 66% up to 80k, 33% up to 90k, nothing after that. You only got in if the 21B10 PCM reflash happened before February 28, 2022, and the whole thing died January 1, 2023.

A stranger would have paid $12.99 for my shrug. So I fixed it.. the sourced version lives at curbspec.com/f-150.

There’s a Chrome extension too. It puts a button on Cars.com and Autotrader listings so you don’t have to copy a 17-character VIN across ten tabs.

Live: https://curbspec.com

Ask about the phasers.

utvtron: New Belt, Same Tips

Saturday the nine-year-old belt came apart on the last hill into Four Corners. I pulled the fiber out of the outlet duct and drove it home. Then I put a new one in.

The Swap

Cover off. Old fluff still clinging to the inner housing lip. New belt on the Polaris primary and the TEAM secondary, and blue paint on the primary nut so future me knows I actually torqued it.

New CVT belt on the Polaris primary and TEAM secondary, cover off

Inner clutch housing after cleanup.. ducting, loom, leftover belt fiber on the seal lip

Logged it in wrenchtron as a belt replacement. utvtron signs into the same Google account now, so the garage and the maintenance log are one machine again.

The Ride

ThermoPro in the cargo recess behind the seats, three tips live: Engine bay, Clutch, Ambient. Clutch tip in the outlet duct, where it belongs.

ThermoPro TP25 in the RZR cargo recess, three tips live before the break-in ride

Eighteen minutes, 7.4 miles, easy loop. Not River Road to Amsterdam, and definitely not 65 up the hill. The clutch outlet peaked at 130°, climbed and then just sat there. Last week’s shred hit 164° on that same channel. Boring is good.

utvtron share card: New belt ride, Clutch 130°F, 18 min, 7.4 mi

One hiccup: around nine minutes in the app dropped for a minute and a half. I hit Resume, reconnected the probe, and it kept it all as one ride.

New belt handles a normal evening loop just fine. More miles before anything spicy.

Live: utvtron.vercel.app

utvtron: Then the Belt Came Apart

Last time the hose I could see turned out to be the CVT intake, and the heat was on the probe I’d named Ambient. This time the tip was supposed to go in the hose that leaves the clutch.

Saturday I put it there. The belt died on the way home. The log has a pretty spike, but I didn’t need the log.. I heard it.

The Tip

ThermoPro on the bay plastic, three tips live before we rolled. The clutch tip went in above the slotted housing, next to the quilted heat shield.. outlet air this time, not cabin air. Names on the ride: Engine bay, Clutch, Ambient. In the right plugs.

ThermoPro TP25 in the RZR engine bay before the Saturday ride, three tips live, jack 4 empty

ThermoPro tip at the clutch outlet, quilted heat shield on the left

The Ride

Out of Four Corners, south on River Road, all the way down Axtell / Anceney to Amsterdam, and backroads back to pavement. Forty-five minutes of that. Then 84, and the last climb into Four Corners from the west. 65+ up a long hill. That’s where it gave.

GPS trace: Four Corners, River Road, Axtell / Anceney, Amsterdam, back

utvtron history: 58 min, 36.6 mi, Clutch 164°F peak, 54 impacts

58 minutes, 36.6 miles. The clutch outlet cruised around 115–134° on the loop. Then the hill: 126°, 140°, 151°, 164°. Engine was fine. I could hear the belt shredding.

The app held up.. session, GPS, even a Bluetooth drop early on. It just didn’t see it coming any sooner than my ears did.

What Came Apart

A CVT belt has cogs on both sides and a cord running through the middle. The cord went, and the cogs started peeling off. That fluff in the tunnel is the cord, blown into fiber. Same stuff packed the outlet duct.. the one my probe was sitting in.

Shredded belt packed around the probe braid in the tunnel

I pulled the fiber out of the duct and drove it home at 50.

Broken CVT belt fragment over the RZR center console

2017 RZR 4 900 in the yard after the ride

The belt was nine years old, and the machine lives outside in Montana.. twenty below, then a hundred. It was a timebomb. Saturday it went off.

I’m not going to run that loop again to see what a new belt looks like on the chart. I’m putting a new belt in so I can drive the thing.

Live: utvtron.vercel.app

Update, Aug 20: belt’s in. Short break-in ride, tips in the right ducts.

utvtron: The Duct Was the Intake

Last time I said the next ride was the duct. Sunday afternoon I took it.. 2017 RZR 4 900, three ThermoPro tips this time. The app handed back a share card that looked done.

utvtron share card for Aug 9 · Afternoon: 96°F max, 90°F avg, 18 min, 9.0 miles, 3 impacts, three probe series

96° max, 18 minutes, 9 miles. Green line flat, blue line doing all the work. I almost posted that and called it a day.

Then I opened the CSV.

The Hose I Could See

The clutch housing is in the left-rear wheel well, and a fat J-shaped hose clamps onto the round cover. That’s the hose I could see, so that’s where a tip went. One stayed in the cabin. One went in the engine bay, since that’s the only place that got hot last time.

CVT intake hose on the 2017 RZR 4 900, clamping onto the outer clutch cover

In the app I named them Belt and Ambient, and the third one just ended up as “Probe 3.”

What the CSV Said

708 rows, a reading every second and a half. Bluetooth held, the screen stayed awake, no timestamp pileup like the first ride.

“Belt” sat around 90°. Probe 3 sat around 90°. “Ambient” started at 106° and hit 163° fifteen minutes in. The card only showed the first probe, so the headline was 96° while the actual heat was on the channel I’d named Ambient. I had the plugs backwards.

That Hose Is the Intake

The J-tube is the CVT intake.. it pulls cool air in. A tip in there is just going to read cabin air, which is exactly what it did. The clutches are further inboard, not behind that rubber.

And on this generation there’s no clutch exhaust grill. Polaris dumps the hot air right into the engine bay, over the trans, at the block and header. So the bay probe is a mix of engine heat and whatever the clutch was blowing out. Still not a belt reading.

ThermoPro TP25 on the RZR floor, one cable routed through the floor toward the clutch

I wasn’t going to build GPS or multiple probes this early. Built them anyway, and they paid off.. three lines on one chart is what made the mixup obvious.

Next tip goes in the hose that leaves the clutch, and this time the names match the plugs.

Live: utvtron.vercel.app.. Chrome on Android.

Update, Aug 15: I got the tip in that hose. Then the belt came apart.

utvtron: Giving a $30 BLE Thermometer a Second Job

I do all my own maintenance on the fleet.. two trucks, an ATV, a RZR.. which is why wrenchtron exists. I’ve also spent a lot of years on the workbench with Arduinos, Raspberry Pis, and SparkFun parts bins. This one comes more from that side.

I’ve gotten into running lately, and the Peloton app’s outdoor mode does something I take for granted: my phone is the sensor. It starts the session, logs the whole thing, and hands me a history afterward.

Every belt-temp gauge I found for a UTV clutch is the opposite. A live number on an LED, and nothing else. Look away and that reading is gone forever.

The Build

Don’t build sensor hardware. Everybody already sells it.. BLE thermometers by the dozen, built for smokers. What’s missing is the part that turns a live number into a ride you can look at later.

utvtron is a Next.js static export on Vercel. No backend.. Web Bluetooth talks to the probe, localStorage holds the history, nothing leaves the phone. Connect, start, ride, stop, look at the chart, export a CSV. That’s it.

The probe is a ThermoPro TP25, a $30 meat thermometer. Getting it to talk to a browser meant figuring out a Bluetooth protocol nobody publishes. A community project (thermopro-cli) had decompiled the Android app and documented the handshake, plus a temperature format I’d never have guessed. First connection, the app read 66°F next to the ThermoPro reading 66°F. Huh.. it works.

utvtron session history: early bench tests reading 66°F next to the ThermoPro controller, before the first real ride

First Ride

I stuck the probe behind the back seat.. wrong spot, but it was a start.. and went for a ride.

ThermoPro TP25 controller between the RZR’s rear seats, probe cable routed down toward the storage compartment

36 minutes, live readout the whole time, saved cleanly afterward.

utvtron’s first ride: 146°F max, 129°F avg, 36 minutes, with the full temperature chart and an Export CSV button

The best part was the CSV. About 20 minutes in there’s a ~50 second gap, then 33 readings with the exact same timestamp even though the temps are all different. My phone screen locked mid-ride, the browser tab went to sleep, and Bluetooth kept stacking up readings until the tab woke up and swallowed them all at once. Fixed it by keeping the screen awake while recording, catching Bluetooth drops, and autosaving every 10 seconds so a killed tab doesn’t eat the ride.

At the end I moved the probe into the engine bay and the ThermoPro’s own screen read 142°F, right in line with what the app logged. Good data, dumb phone.

ThermoPro TP25 controller reading 142°F with the probe moved into the engine bay

The RZR dash only shows coolant temp.. nothing on the belt or clutch, which is the whole reason I’m doing this. A probe behind a seat doesn’t tell me anything about the belt. Next ride it goes in the clutch duct.

Live: utvtron.vercel.app.. Chrome on Android only, Web Bluetooth isn’t on iPhone yet.

Update, Aug 10: I took that ride. The duct was the intake.

Mount Baldy, Again

The girls are back in Glasgow with my parents for the week. Time for another relaxing aggressive hike.

Katie and I went back up Baldy.. published as 8 miles round trip, 4,300 feet of gain.. this time with a bit more tact: right amount of water, a wore a bougie sun shirt, we had protein bars.

summit selfie with Bozeman below

Still a contested name, as I mentioned last year.. but this time we kept going past the register box to the next bald spot along the ridge. A bit higher, closer to Ross Peak. From town the two of them look like a pair of bald camel humps.. meanwhile the “official” Baldy Mountain, a mile further north, is covered in trees with a rock scramble to its actual summit. Not bald at all, and you’d never even notice it. Looking back:

looking back down the ridge past the register box

Katie on the trail through wildflowers, register box hill behind her

Peloton tried to keep up. Not accurate, but it logged 13.88 miles, 5,297 feet of gain, and a little over five hours. GPS on a ridge does what GPS on a ridge does.

Peloton outdoor walking stats

Peloton route map

Fun trip.. looking forward to the next one.

Difftron: Only Grade the Diff

Every repo I’ve worked in has a coverage number. Ours says 84%. It’s said 84% for about a year.

Someone opens a PR, adds a hundred lines with no tests, and the number drifts from 84.1% to 83.6%. Nobody blocks a PR over half a percent.. you’d look insane. So the untested code walks right in, the number stays put, and six months later “84%” is sitting on a growing pile of stuff nobody ever tested.

Grade the Diff

Stop grading the whole repo. Grade the lines this PR changed. It’s called patch coverage, and Codecov and friends have done it for years. I wanted a version that was dead simple, didn’t care what language you write, and never sent anything off the runner.

difftron is a small Go CLI wrapped as a GitHub Action. Point it at an LCOV, Cobertura, or Go coverage file, and it diffs against the base branch and grades only the changed lines:

- uses: swantron/difftron@v1
  with:
    coverage: coverage/lcov.info
    threshold: '80'

You get a sticky PR comment with the uncovered line numbers, and a failed check if you’re under the threshold. Or it just comments, if you set fail-on-error: false while you ease into it.

Dogfooding

I dropped it onto six of my own repos in an afternoon.. Go tools, a couple of vitest apps, two that needed c8 to turn node --test into an lcov file. Same four lines of yaml every time.

On tronswan I planted a tested function next to an untested one and opened a throwaway PR. difftron graded the 15 changed lines.. 1 covered, 14 not. The project-wide number barely moved.

difftron sticky comment on a tronswan PR: 6.7% changed-line coverage, uncovered line numbers listed, test file skipped

v1 had a great bug: it failed a docs-only PR. Edit a README, there’s no coverage data for markdown, difftron reads that as 0%, PR fails. Instant uninstall. Now files with no coverage signal get skipped instead of failed, and the comment tells you which ones.

Marketplace: Difftron Delta Coverage Gate

Source: github.com/swantron/difftron

84% forever. Your diff, though.. that gets tested.

Watchtron: Did the Deploy Actually Work?

I sort of wound up with a formidable little fleet of sites.. tronswan, chomptron, swantron, mt, wrenchtron, jswan.dev. The DevX dawg in me naturally wanted to know if they were up.

So I wrote the dead simplest thing that could tell me. uptime-monitor is 234 lines in one file.. a GitHub Actions cron that pings every site every 5 minutes and writes uptime to a public gist. No VM, no terraform, $0. It only tips over when GitHub tips over. I’d build it the same way again.

But it can’t tell me whether a deploy worked. Every deploy hands me a green checkmark, and the checkmark just means the pipeline exited 0. A HEAD / coming back 200 can be a CDN handing you a cached page while the app behind it is face-down. A pinger falls for that too.

OTel, Backwards

Usually you instrument with OpenTelemetry so you can stare at dashboards after something breaks. I flipped it around. Right after a deploy, fire some synthetic traffic at the live URL, tag every request with a trace id, and check that the telemetry for that exact run shows up. If it does, the request made it all the way to the real app.. not a cache, not a stale instance. If it doesn’t, or it’s slow, the deploy fails.

watchtron is a prober, a tiny control plane on a free-tier GCE e2-micro, and a reusable workflow any repo can call after its deploy job:

verify:
  needs: deploy
  uses: swantron/watchtron/.github/workflows/verify.yml@main
  with:
    service: tronswan
    version: ${{ github.sha }}
  secrets:
    otlp_endpoint: ${{ secrets.WATCHTRON_OTLP_ENDPOINT }}
    token: ${{ secrets.WATCHTRON_TOKEN }}

The fleet is all over the place.. DigitalOcean, Cloud Run, GitHub Pages, Firebase, a self-hosted PDS.. and one workflow covers all of it. For the two Express apps I own, the app emits its own span too, so watchtron can match it to the probe and check that the git SHA it reports is the one that just shipped. Green means this build is serving, not just something.

Two Tools, One Strip

I started bolting history and dashboards onto watchtron and caught myself rebuilding the uptime monitor, worse. So I quit. The pinger is the heartbeat, watchtron is the deploy gate. Instead of competing, they share a chart: one strip per site, green up, red down, little triangles where deploys landed.

watchtron dashboard: per-service uptime strips with verified-deploy markers

It’s up at watch.swantron.com, with a cleaner cut on tronswan.com/status.

The dashboard paid for itself right away. GitHub quietly disables scheduled workflows after ~60 days without a commit. The uptime monitor has a dirty keepalive for exactly this. watchtron’s cron.. did not. The staleness flag caught it.

Source: github.com/swantron/watchtron

Now the green checkmark actually means something.

Shrinking third-party containers without touching a Dockerfile

Our ProdSec team at work recently leaned hard into Wiz and Slack-ops. Vulnerabilities started piling up across our various namespaces, a lot of them flagged critical on paper.. typical enterprise scenario where dashboards are red and every alert demands a response.

The catch: most of those images aren’t ours. We’re DevX, which sits inside CloudOps alongside Platform Engineering, SRE, and Cloud Cost.. a big part of what our org does is run the infrastructure that makes the clusters work. Wiz, Datadog agents, observability tooling, a bunch of GitOps stuff. Closed source, proprietary, not ours to rebuild. But they live in our namespaces, so we own the vuln count. Same for anything legacy or shared.. that comes as well.

Coworker bud and I started talking about what you’d actually do about this. You can’t patch a binary you don’t ship. You can’t swap the base image. The vendor updates when they update. The realistic options are: accept the noise, get exceptions granted, or get creative.

Moment of clarity: what if we just… axe the parts that aren’t running?

Trace What’s Actually Used

A typical third-party agent image is built for maximum compatibility. Shells, package managers, debug utilities, man pages, and a pile of shared libraries for features nobody in your environment uses. Hundreds of megabytes, and every one of those binaries is something for the scanner to yell about.

Rebuild the image from scratch with only the files the container actually touches at runtime, and the agent still pongs.. but the shell is gone. So is the package manager. So is most of what the scanner was complaining about.

So I Built Something

minifier-cli is a Go tool that does exactly this.

1. Trace. Run your container through the tool and it watches which files the process opens and maps into memory, polling /proc inside the container every second.

minifier-cli trace start --image datadog/agent:latest --name dd-prod
# Let it run in a production-like environment
# Ctrl+C to stop tracing

2. Repackage. It walks every binary in the trace for the shared libraries it needs, adds the handful of files everything quietly relies on (passwd, hosts, resolv.conf), copies it all out, and builds a FROM scratch image with the original ENV, CMD, and ENTRYPOINT.

minifier-cli repackage --name dd-prod --output datadog-minimal:prod

nginx:alpine goes from 91.7MB to 14.1MB. A startup-only trace of datadog/agent:latest takes it from 1.17GB to 59MB. The vuln scanner suddenly has a lot less to say.

minifier-cli terminal output: trace, repackage, and docker images before and after

How It Ended Up

We eventually landed on a different solution for the actual hardening problem.. we are basically buying pre-hardened images that someone else has already performed surgery on. With vendor backing and security buy-off, we are somewhat less culpable.

Still, it’s a handy way to find out what a black-box container actually needs at runtime.. for hardening, audits, or poking at a legacy app before you migrate it. Open-sourcing it on the theory that someone else has the same problem.

The catch: the image only has what got touched during the trace. Error paths, plugins, and config-flag-only drivers won’t be there unless you exercised them. Run your integration tests while tracing, trigger your error states, test it in staging, and keep the original around. Don’t ship it on the strength of a five-minute trace and a curl localhost.

Binaries on the releases page. The tracer needs Linux; the repackager runs fine on a Mac.

Source: github.com/swantron/minifier-cli

The Covid Bike: 3,500 Rides Later

I wasn’t a spin dude before Covid, and I still don’t really claim to be now. In 2020, the world sort of shut down, I went remote and lost my gym at Workiva. Katie bought us a Peloton early in the pandemic, and we’ve spent the last six years trying to break the thing.

We finally succeeded. I guess we’re spin dudes.

The resistance started jumping around and acting erratic a few weeks ago. Peloton support tried to tell me I needed a technician to install a new monitor cable. No on the tech, and sensor cable in the mail. I got ansty and contacted support the following day, and was able to talk them into selling me the sensor board.. again declining the tech after repeatedly acknowledging I would void my five year lapsed warranty.

Peloton resistance sensor board and cable assembly

Some Stats

I was curious what the “odometer” actually looked like after six years. Since this is an early bike, I had to manually pull the numbers from our profiles:

  • jswan: 1,825 rides + 209 Bootcamps
  • kt: 1,505 rides
  • Total: 3,539 sessions

I do enough 45-minute rides to offset the short 10-minute add-ons, so this hardware has easily seen 2,000+ hours of work. Honestly, one sensor board failure after that much volume is less of a defect and more of a badge of honor.

The Swapperoo

Two items..

Peloton order history showing monitor cable April 9 and sensor assembly April 10

The swap is dead simple. Remove the sweat guard, untuck wires, remove the sensor, replace the sensor, tuck wires, CALIBRATE, replace sweat guard. Phillips screwdriver and a hex set. Navigating to the calibration mode using the wonky Peloton UI arguably took me more time than the swap.

A Few Takeaways

If you have an old Original bike and you’re actually putting miles on it:

  • Don’t bite on the tier 1 support advice: If the resistance is jumpy but the screen works fine, it’s the sensor board. Skip the monitor cable.
  • Recalibrate: New sensors mean the bike might feel “heavy” or “light” compared to your old PRs. Run a calibration kit to level it out.
  • Check Pedals: We are pushing it with 3,500 rides, the bearings are still decent but on borrowed time.

The bike is a beat-to-shit but back in the rotation. It’s a tank..