<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://roundhere.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://roundhere.net/" rel="alternate" type="text/html" /><updated>2026-01-27T19:18:45+00:00</updated><id>https://roundhere.net/feed.xml</id><title type="html">Roundhere.net by Chris Martin</title><subtitle>A collection of things, by Chris James Martin</subtitle><icon>https://roundhere.net/assets/images/favicons/favicon-summer-64.png</icon><logo>https://roundhere.net/assets/images/favicons/favicon-summer-512.png</logo><author><name>Chris James Martin</name></author><entry><title type="html">SSH via Cloudflare tunnel using iSH on iPad</title><link href="https://roundhere.net/field-notes/2025/07/ssh-via-cloudflare-tunnel-from-ipad-using-ish/" rel="alternate" type="text/html" title="SSH via Cloudflare tunnel using iSH on iPad" /><published>2025-07-10T00:00:00+00:00</published><updated>2025-07-10T00:00:00+00:00</updated><id>https://roundhere.net/field-notes/2025/07/ssh-via-cloudflare-tunnel-from-ipad-using-ish</id><content type="html" xml:base="https://roundhere.net/field-notes/2025/07/ssh-via-cloudflare-tunnel-from-ipad-using-ish/"><![CDATA[<p>Quick post today, with relevance to virtually no one, but I googled it and came up empty so might as well write it down.</p>

<p>The onset of summer has meant much less regular time in front of the computer. Instead I’ve been carrying my iPad, and giving it the 20th try as a lightweight productivity device. I’m running the iPadOS 26 Developer Beta, which seems to be moving more in the MacOS direction, so maybe we’ll get there.</p>

<p>I have a Raspberry Pi that I plan to set up with <a href="https://code.visualstudio.com/docs/remote/vscode-server">VS Code Server</a> for remote code editing, and it’s set up with a <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/">Cloudflare Tunnel</a> for remote SSH access, which requires proxying through the <code class="language-plaintext highlighter-rouge">cloudflared</code> client.</p>

<p>The following assumes you already have a CF tunnel set up for SSH. If not you should probably look into <a href="https://tailscale.com/">Tailscale</a> instead. I would if I wasn’t already set up with Cloudflare.</p>

<p>Here’s how I went about setting up <a href="https://ish.app/">iSH</a> on my iPad to make everything work.</p>

<ol>
  <li>
    <p>Install iSH, which is a piece of wizardry that gives you an x86 Alpine Linux shell on your iPad.</p>
  </li>
  <li>
    <p>Install required packages:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> apk add openssh-client curl nano
</code></pre></div>    </div>

    <p>At this point <code class="language-plaintext highlighter-rouge">ssh user@your.cloudflare.tunnel</code> will fail with a timeout error.</p>

    <p>Since we’re in a full Linux shell, we can use the x86 cloudflared binary to proxy the connection, just as we would on any other Linux system.</p>
  </li>
  <li>
    <p>Create a user <code class="language-plaintext highlighter-rouge">bin</code> directory to hold the cloudflared binary:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> mkdir ~/bin &amp;&amp; cd ~/bin
</code></pre></div>    </div>
  </li>
  <li>
    <p>Grab the latest x86 (32 bit) binary from <a href="https://github.com/cloudflare/cloudflared/releases">GitHub</a>. The current release is 2025.7.0, you’ll want to update the curl command below to the latest <code class="language-plaintext highlighter-rouge">linux-386</code> version:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> curl -LO https://github.com/cloudflare/cloudflared/releases/download/2025.7.0/cloudflared-linux-386
</code></pre></div>    </div>
  </li>
  <li>
    <p>Tell SSH to proxy connections to <code class="language-plaintext highlighter-rouge">your.cloudflare.tunnel</code> through the cloudflared client:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> nano ~/.ssh/config
</code></pre></div>    </div>

    <p>Add the following:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Host your.cloudflare.tunnel
 ProxyCommand /root/bin/cloudflared-linux-386 access ssh --hostname %h
</code></pre></div>    </div>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ssh your.cloudflare.tunnel</code> should now work!</p>
  </li>
</ol>

<p>iSH is not the most practical way to work on a remote machine, but this is sufficient for logging in for the occasional need. Have fun!</p>]]></content><author><name>Chris James Martin</name></author><category term="field-notes" /><category term="SSH" /><category term="Cloudflare" /><category term="iPad" /><category term="iSH" /><summary type="html"><![CDATA[How to SSH from an iPad using iSH and a Cloudflare tunnel.]]></summary></entry><entry><title type="html">I love the Truckee Library</title><link href="https://roundhere.net/journal/2025/05/i-love-the-truckee-library/" rel="alternate" type="text/html" title="I love the Truckee Library" /><published>2025-05-23T00:00:00+00:00</published><updated>2025-05-23T00:00:00+00:00</updated><id>https://roundhere.net/journal/2025/05/i-love-the-truckee-library</id><content type="html" xml:base="https://roundhere.net/journal/2025/05/i-love-the-truckee-library/"><![CDATA[<p>I love our local library. My kids go multiple times a week after school to get homework help and read books for hours.</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54539300908/in/datetaken/" title="Public Libraries are Amazing">
  <img src="https://live.staticflickr.com/65535/54539300908_db1b077d7b_c.jpg" width="800" height="600" alt="Public Libraries are Amazing" />
</a></p>

<p>They recently upgraded their 3D printer, and yesterday I was able to take the intro class to be “certified” to use it. Now I can book time and print projects! First up is a <a href="https://www.thingiverse.com/thing:1601055">case/stand for my Raspberry Pi media server</a> (next week), but I plan to have a batch of projects lined up monthly. I’m excited to start designing some original stuff, the kids mainly want to print dragons.</p>]]></content><author><name>Chris James Martin</name></author><category term="Library" /><category term="Community" /><summary type="html"><![CDATA[The Library has a new 3D printer!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://live.staticflickr.com/65535/54539300908_db1b077d7b_c.jpg" /><media:content medium="image" url="https://live.staticflickr.com/65535/54539300908_db1b077d7b_c.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Fixed Feedburner RSS</title><link href="https://roundhere.net/field-notes/2025/05/fixed-feedburner-rss/" rel="alternate" type="text/html" title="Fixed Feedburner RSS" /><published>2025-05-08T00:00:00+00:00</published><updated>2025-05-08T00:00:00+00:00</updated><id>https://roundhere.net/field-notes/2025/05/fixed-feedburner-rss</id><content type="html" xml:base="https://roundhere.net/field-notes/2025/05/fixed-feedburner-rss/"><![CDATA[<blockquote class="mastodon-embed" data-embed-url="https://mas.to/@dokas/114469904427643118/embed" style="background: #FCF8FF; border-radius: 8px; border: 1px solid #C9C4DA; margin: 0; max-width: 540px; min-width: 270px; overflow: hidden; padding: 0;"> <a href="https://mas.to/@dokas/114469904427643118" target="_blank" style="align-items: center; color: #1C1A25; display: flex; flex-direction: column; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif; font-size: 14px; justify-content: center; letter-spacing: 0.25px; line-height: 20px; padding: 24px; text-decoration: none;"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor" /></svg> <div style="color: #787588; margin-top: 16px;">Post by @dokas@mas.to</div> <div style="font-weight: 500;">View on Mastodon</div> </a> </blockquote>
<script data-allowed-prefixes="https://mas.to/" async="" src="https://mas.to/embed.js"></script>

<p>👋 Hi Phil!</p>

<p>This blog has been <span data-note="Not particularly active, but around...">around</span> for a <em>long</em> time. From 2010 until ~2 months ago the RSS feed pointed to a feedburner proxy which pointed to an old atom feed which broke sometime in the last 15 years.</p>

<p>2 months ago I fixed the RSS feed and updated the link to remove feedburner, since it’s utility seems to’ve gone the way of Google Reader 🪦</p>

<p>This is all great for new subscribers like Phil (❤️), but I wonder if there are any other RSS-lovers out there with my old feedburner link in their readers?</p>

<p>If so, <strong>hello</strong>! As of today I’ve pointed the old feedburner proxy at the working feed.</p>]]></content><author><name>Chris James Martin</name></author><category term="field-notes" /><category term="RSS" /><category term="Feedburner" /><summary type="html"><![CDATA[The old feedburner RSS proxy now points to the working RSS feed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roundhere.net/assets/images/fixed-rss/fixed-the-glitch.gif" /><media:content medium="image" url="https://roundhere.net/assets/images/fixed-rss/fixed-the-glitch.gif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Add AirPrint to any printer with a Raspberry Pi + CUPS</title><link href="https://roundhere.net/field-notes/2025/05/raspberry-pi-print-server/" rel="alternate" type="text/html" title="Add AirPrint to any printer with a Raspberry Pi + CUPS" /><published>2025-05-07T00:00:00+00:00</published><updated>2025-05-07T00:00:00+00:00</updated><id>https://roundhere.net/field-notes/2025/05/raspberry-pi-print-server</id><content type="html" xml:base="https://roundhere.net/field-notes/2025/05/raspberry-pi-print-server/"><![CDATA[<figure class="captioned" style="padding: 5px 5px 0">
<a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/5535973168/" title="Hello Chris, This is God">
  <img src="https://live.staticflickr.com/5058/5535973168_6c91747f01_c.jpg" width="800" height="514" alt="Hello Chris, This is God" />
</a>
<figcaption>If ChatGPT could print.</figcaption>
</figure>

<p>I have an old <a href="https://support.brother.com/g/b/producttop.aspx?c=us&amp;lang=en&amp;prod=hl2170w_all">Brother HL-2170W</a> laser printer. It’s bomb-proof, and doesn’t have any weird toner subscriptions, DRM, or whatever else they’re doing to printers these days.</p>

<p>However, it does not support modern protocols like AirPrint, and while it <em>technically</em> has WiFi, the firmware hasn’t been updated in a decade(?) and only supports WEP, so that’s not going to work.</p>

<p>Right now, it’s connected via Ethernet to the AP in my living room, and we just print to it from whichever computer feels like working with it directly that day. But I’d really rather not have a printer in my living room.</p>

<p>Recently, I rediscovered a few <a href="/journal/2025/04/weeknotes-spring-break/#fun-with-raspberry-pis">Raspberry Pi 1s</a> and realized they’d be perfect to modernize this otherwise excellent printer and turn it into an AirPrint-compatible print server.</p>

<h3 id="supplies">Supplies</h3>

<ul>
  <li>Raspberry Pi<br />
<small>Any <a href="https://amzn.to/4389X41">Raspberry Pi</a>, I used a Pi 1 for this project but a <a href="https://amzn.to/4jFIPjL">Pi Zero</a> would work great.<small></small></small></li>
  <li>If using a Pi 1 or 2: USB Wi-Fi adapter<br />
<small>I already had <a href="https://amzn.to/3YymBI5">this one</a>, but any basic adapter should work.</small></li>
  <li>MicroSD card<br />
<small>Needs to be &gt;2GB, I think. I used an 8GB I had lying around.</small></li>
  <li>USB cable to connect the printer<br />
<small>This was the one thing I didn’t have! <a href="https://amzn.to/3F3W9zo">I ordered 3 for ~$7</a>, and now have 2 for future projects that need old USB cables.</small></li>
  <li>Power supply for the Pi<br />
<small>Should be able to use any usb brick + microusb cable, but I have had issues with some not supplying enough current. If you have issues with the Pi looping on boot, check the power supply.</small></li>
</ul>

<h3 id="1-install-raspberry-pi-os-lite">1. Install Raspberry Pi OS (Lite)</h3>

<p>Use the <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a> to install the latest Raspberry Pi OS Lite (click through “Raspberry Pi OS (other)” to get to the lite version) onto your SD card. There’s no need for a full desktop environment for this job.</p>

<figure class="captioned">
  <img src="/assets/images/pi-print-server/pi-os-lite.png" alt="Raspberry Pi OS Lite selection in Raspberry Pi Imager" />
  <figcaption>Raspberry Pi OS (other) -&gt; Raspberry Pi OS Lite</figcaption>
</figure>

<p>Before flashing, preconfigure some settings:</p>

<ul>
  <li>Hostname</li>
  <li>User + Password</li>
  <li>Wi-Fi credentials</li>
  <li>SSH public key</li>
</ul>

<div class="side-by-side-images">
  <figure class="captioned">
  <img src="/assets/images/pi-print-server/os-settings.png" alt="Raspberry Pi OS settings in Raspberry Pi Imager" />
  <figcaption>Hostname + Username + WiFi Settings</figcaption>
</figure>
  <figure class="captioned">
  <img src="/assets/images/pi-print-server/ssh-key.png" alt="Raspberry Pi OS SSH Public Key in Raspberry Pi Imager" />
  <figcaption>SSH Public Key</figcaption>
</figure>
</div>

<p><small>Setting the SSH public key is optional, but really nice to do ahead of time. It looks like you can just click that “RUN SSH-KEYGEN” button and it’ll probably do everythig for you. Otherwise grab your public key out of <code class="language-plaintext highlighter-rouge">~/.ssh</code> (if you already have one).</small></p>

<h3 id="2-boot-and-ssh-into-the-pi">2. Boot and SSH into the Pi</h3>

<ul>
  <li>Plug the Pi into power and wait for it to boot and connect to the network.</li>
  <li>You can find the Pi’s IP address from your router admin, or try using the hostname you configured:</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh &lt;your-user&gt;@&lt;<span class="nb">hostname</span><span class="o">&gt;</span>.local
</code></pre></div></div>

<h3 id="3-update-and-install-cups">3. Update and Install CUPS</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt upgrade
<span class="nb">sudo </span>apt <span class="nb">install </span>cups
<span class="nb">sudo </span>apt <span class="nb">install </span>printer-driver-brlaser
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">printer-driver-brlaser</code> package installs a CUPS-compatible driver for many Brother printers, including the HL-2170.</p>

<p>This process should work for any printer but I only installed the brlaser driver because it’s all I needed. The <code class="language-plaintext highlighter-rouge">foomatic-db</code> package contains drivers for <em>many</em> different printers, but I recommend searching for your specific printer and installing only what you need.</p>

<h3 id="4-configure-cups">4. Configure CUPS</h3>

<h4 id="add-your-user-to-the-lpadmin-group">Add your user to the <code class="language-plaintext highlighter-rouge">lpadmin</code> group</h4>

<p>Add the user that you configured in the Raspberry Pi imager to the <code class="language-plaintext highlighter-rouge">lpadmin</code> group so that you can use it to manage printers:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>usermod <span class="nt">-aG</span> lpadmin &lt;your-user&gt;
</code></pre></div></div>

<h4 id="listen-and-allow-access-to-cups-web-interface-on-local-network">Listen and allow access to CUPS web interface on local network</h4>

<p>By default, CUPS only listens on <code class="language-plaintext highlighter-rouge">localhost</code>. To allow access from other devices on your network:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nano /etc/cups/cupsd.conf
</code></pre></div></div>

<p>Make these changes:</p>

<h4 id="listen-on-local-network">Listen on local network</h4>

<p>Find:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Only listen for connections from the local machine.
Listen localhost:631
</code></pre></div></div>
<p>Change to:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Listen for connections on the local network.
Listen &lt;hostname&gt;.local:631
</code></pre></div></div>

<h4 id="allow-local-network-access-to-admin-web-interface">Allow local network access to admin web interface</h4>

<p>Update the following sections:</p>

<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Restrict access to the server...
</span>&lt;<span class="n">Location</span> /&gt;
  <span class="n">Order</span> <span class="n">allow</span>,<span class="n">deny</span>
  <span class="n">Allow</span> @<span class="n">local</span>
&lt;/<span class="n">Location</span>&gt;

<span class="c"># Restrict access to the admin pages...
</span>&lt;<span class="n">Location</span> /<span class="n">admin</span>&gt;
  <span class="n">Order</span> <span class="n">allow</span>,<span class="n">deny</span>
  <span class="n">Allow</span> @<span class="n">local</span>
&lt;/<span class="n">Location</span>&gt;

<span class="c"># Restrict access to configuration files...
</span>&lt;<span class="n">Location</span> /<span class="n">admin</span>/<span class="n">conf</span>&gt;
  <span class="n">AuthType</span> <span class="n">Default</span>
  <span class="n">Require</span> <span class="n">user</span> @<span class="n">SYSTEM</span>
  <span class="n">Order</span> <span class="n">allow</span>,<span class="n">deny</span>
  <span class="n">Allow</span> @<span class="n">local</span>
&lt;/<span class="n">Location</span>&gt;
</code></pre></div></div>

<p>Save and exit, then restart CUPS:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart cups
</code></pre></div></div>

<hr />

<h3 id="5-access-cups-web-interface">5. Access CUPS Web Interface</h3>

<p>From another device on the same network, go to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://&lt;hostname&gt;.local:631
</code></pre></div></div>

<p>You will probably get an SSL nastygram from your browser. Tell it you want to go to the website anyway.</p>

<ul>
  <li>From the Administration page, click “Add Printer”.</li>
  <li>Log in with the system username and password you set up in the Raspberry Pi Imager.</li>
</ul>

<p>In the Add Printer flow, select these options on each page:</p>

<ol>
  <li>Select the USB-connected Brother printer. It should be listed as a “Local Printer”.<br />
  <small>My printer also showed up (twice) under discovered network printers. In theory you could set this up with the printer connected via. ethernet to your network and get AirPrint support without connecting to the Raspberry Pi via. USB, but that’s not my goal.</small></li>
  <li>Make sure the name/description/location look good, and <strong>select “Share This Printer”</strong>.</li>
  <li>Choose the closest printer to your specific model, for my <code class="language-plaintext highlighter-rouge">HL-2170W</code> I picked <code class="language-plaintext highlighter-rouge">HL-2140</code>. Unless you installed other drivers it will use <code class="language-plaintext highlighter-rouge">brlaser</code> no matter which model you select.</li>
  <li>After clicking “Add Printer” on the final screen, you should see “Printer <your-printer> has been added successfully."</your-printer></li>
</ol>

<figure class="captioned">
  <img src="/assets/images/pi-print-server/add-printer.png" alt="Driver selection screen in CUPS web admin interface" />
  <figcaption>Model selection, these will all use the brlaser driver.</figcaption>
</figure>

<p>Continue to “Set Printer Options” and make any appropriate changes. I only needed to set the default paper size from A4 to Letter.</p>

<p>You should now be able to go to the “Printers” section in the CUPS web interface, select your newly added printer, and print a test page! 🎉</p>

<figure class="captioned">
  <img src="/assets/images/pi-print-server/test-page.jpeg" alt="CUPS test page" />
  <figcaption>You can have any color you'd like, as long as it's a shade of grey.</figcaption>
</figure>

<h3 id="6-fix-cups-on-reboot">6. Fix CUPS on reboot</h3>

<p>On my setup, CUPS starts but the web interface and printers are not accessable after reboot — likely because it starts before the network is ready. This means the printers and the web interface aren’t available until the service is restarted.</p>

<h4 id="restart-cups-when-the-network-comes-up">Restart CUPS when the network comes up</h4>

<p>Create a script at <code class="language-plaintext highlighter-rouge">/etc/network/if-up.d/cups</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nano /etc/network/if-up.d/cups
</code></pre></div></div>

<p>Paste this:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>
<span class="nb">sudo </span>systemctl restart cups.service
</code></pre></div></div>

<p>Then make it executable:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo chmod</span> +x /etc/network/if-up.d/cups
</code></pre></div></div>

<p>This ensures that CUPS is restarted once the network is available.</p>

<h3 id="thats-it">That’s it!</h3>

<p>You should now be able to print via. AirPrint or network printing from devices on your local network.</p>

<ul>
  <li>✅ The printer shows up via AirPrint on iPhones and iPads</li>
  <li>✅ It appears as a network printer on MacOS</li>
  <li>✅ I can move my printer out of my living room</li>
  <li>☐ It should also work on Windows/Android/etc, but I haven’t tried…</li>
</ul>

<p><small>Reference: <a href="https://web.archive.org/web/20240614070401/https://developer.com/mobile/cups-and-raspberry-pi-airprinting/">This archived post</a> gives a pretty solid walkthrough of similar steps to setting up a Raspberry Pi print server, but I don’t need the full foomatic-db as I’m only interested in setting up my brother laser (for now).</small></p>]]></content><author><name>Chris James Martin</name></author><category term="field-notes" /><category term="Raspberry Pi" /><category term="TIL" /><category term="Brother HL-2170" /><category term="CUPS" /><summary type="html"><![CDATA[I found an unused Raspberry Pi and used it to add AirPrint to my old Brother printer.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roundhere.net/assets/images/pi-print-server/hello-chris.jpg" /><media:content medium="image" url="https://roundhere.net/assets/images/pi-print-server/hello-chris.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Weeknotes: Spring Break, Science Projects, Raspberry Pi Fun, and Trains!</title><link href="https://roundhere.net/journal/2025/04/weeknotes-spring-break/" rel="alternate" type="text/html" title="Weeknotes: Spring Break, Science Projects, Raspberry Pi Fun, and Trains!" /><published>2025-04-21T00:00:00+00:00</published><updated>2025-04-21T00:00:00+00:00</updated><id>https://roundhere.net/journal/2025/04/weeknotes-spring-break</id><content type="html" xml:base="https://roundhere.net/journal/2025/04/weeknotes-spring-break/"><![CDATA[<p>Last week was spring break for our local school district, and Lauren and I both took breaks from work to have a family-focused week. It was a low-stress, low-pressure, great time.</p>

<p>We often put a lot of pressure on ourselves to “make the most” of these school/work breaks, but we’ve learned over the years that trying to cram too much (too many activities, but also too much expectation) into a week off can be… too much. This year, with a bit of initial discomfort, we went into the week with minimal plans and expectations.</p>

<h3 id="ski-day">Ski Day</h3>

<p>Monday we headed out to Alpine Meadows for a family ski day. We don’t get the opportunity often to all ski together, and after shaking out some initial kid-grumpies related to our differing ability levels, we found a vibe where everyone had fun.</p>

<p>The big kids wound up one-skiing the whole mountain to level the playing field and introduce some hilarity.</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54466507171/" title="Family Ski!">
  <img src="https://live.staticflickr.com/65535/54466507171_f2b25e911a_c.jpg" width="800" height="600" alt="Family Ski!" />
</a></p>

<p>One-skiing from the summit chair: Lucas and Isaac had slightly different approaches 😂</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54462238442/in/datetaken/" title="When two skis are too easy.">
  <img src="https://live.staticflickr.com/31337/54462238442_5298271e33_c.jpg" width="800" height="450" alt="When two skis are too easy." />
</a></p>

<p>The ski season is quickly winding down, so this will probably be one of the last days we all get out together. It was a good one.</p>

<h3 id="discovery-museum-reno">Discovery Museum Reno</h3>

<p>Tuesday we went down to the <a href="https://nvdm.org">Discovery Museum in Reno</a> with some friends. The kids happily spent hours exploring the various exhibits; all ages were engaged, including the adults.</p>

<p>Surprisingly, I only have one photo from the day: this impressive certificate I earned for solving a bunch of puzzles in one of the exhibits.</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54468250567" title="Mindbender Society">
  <img src="https://live.staticflickr.com/65535/54468250567_176453d001_c.jpg" width="800" height="600" alt="Mindbender Society" />
</a></p>

<h3 id="science-fair-project">Science Fair Project</h3>

<p>Brilliant move by Lucas’ school to schedule their science fair project the week after spring break… at least for kids like Lucas who will take advantage of the time to get work done.</p>

<p>I was able to help him build a “fluid conductivity tester” using a multimeter as an ammeter, and a USB controller providing a very low amperage (µA) circuit for testing.</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54463160333/in/datetaken/" title="Science Fair Project Time!">
  <img src="https://live.staticflickr.com/65535/54463160333_e380c65080_c.jpg" width="800" height="600" alt="Science Fair Project Time!" />
</a></p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54462894586/in/datetaken/" title="Testing Conductivity of Liquids">
  <img src="https://live.staticflickr.com/65535/54462894586_bef89fc136_c.jpg" width="800" height="600" alt="Testing Conductivity of Liquids" />
</a></p>

<p>We need to come up with more projects to build (and include the other kids). It was really fun working together.</p>

<h3 id="fun-with-raspberry-pis">Fun With Raspberry Pi(s)</h3>

<p>While Lucas worked away on his project, I dug through my electronics toolbox and did some inventory. Turns out I had 4 old Raspberry Pi boards (and a bunch of Arduinos) buried in there just begging to be used for something.</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54463527298/in/datetaken/" title="Raspberry Pis">
  <img src="https://live.staticflickr.com/65535/54463527298_13cb0374aa_c.jpg" width="800" height="600" alt="Raspberry Pis" />
</a></p>

<p>I set up one of the version 1 boards with <a href="https://pi-hole.net">Pi-hole</a> for DNS-powered ad blocking and <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/">Cloudflared</a> to play with Cloudflare tunnels. Pretty fun little side project!</p>

<p>I’m not sure what I’ll do with the version 3 with the screen, but it’s a good excuse to <a href="https://www.thingiverse.com/thing:1601055">3D print an enclosure</a>! So that’ll be happening soon.</p>

<h3 id="trains">Trains</h3>

<p>We wrapped the week with another museum trip down to the <a href="https://www.californiarailroad.museum">California Railroad Museum</a> in Sacramento.</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54463498250/in/datetaken/" title="Sacramento Northern">
  <img src="https://live.staticflickr.com/65535/54463498250_29c8c30f8d_c.jpg" width="800" height="600" alt="Sacramento Northern" />
</a></p>

<p>The railroad museum is excellent. We visited many years ago, taking the train up from SF and spending the night in Old Sacramento, and we recreated the experience for the first time for all but the oldest kids. We explored the museum on Friday, spent the night at a hotel just down the street, and enjoyed a ride along the river on some of their vintage carriages on Saturday morning.</p>

<p><a data-flickr-embed="true" data-sharing="false" href="https://www.flickr.com/photos/cjmartin/54463125164/in/datetaken/" title="Train Ride Along the River">
  <img src="https://live.staticflickr.com/65535/54463125164_b6e4275c31_c.jpg" width="800" height="600" alt="Train Ride Along the River" />
</a></p>

<p>This is an excellent little overnight excursion for families. Next time we’ll take the California Zephyr down from Truckee for even more train time. 🚞🚉🚂</p>

<h3 id="a-good-week">A Good Week</h3>

<p>We wrapped the week with Easter festivities (way too much candy) and a lovely, last-minute dinner celebration with friends. It’s the time of the year that always feels like we come out of hibernation, and all of our activities suddenly flip over to summer mode. I’m glad we stayed close to home to enjoy it.</p>]]></content><author><name>Chris James Martin</name></author><category term="weeknotes" /><category term="raspberry-pi" /><summary type="html"><![CDATA[A recap of spring break adventures, science projects, Raspberry Pi experiments, and trains!]]></summary></entry><entry><title type="html">‘Slopsquatting’ on Hallucinated Package Names</title><link href="https://roundhere.net/links/2025/04/hallucination-slopsquatting/" rel="alternate" type="text/html" title="‘Slopsquatting’ on Hallucinated Package Names" /><published>2025-04-12T00:00:00+00:00</published><updated>2025-04-12T00:00:00+00:00</updated><id>https://roundhere.net/links/2025/04/hallucination-slopsquatting</id><content type="html" xml:base="https://roundhere.net/links/2025/04/hallucination-slopsquatting/"><![CDATA[<p>From the department of 🤦‍♂️</p>

<aside class="cheshire-note">
  <a href="https://www.theregister.com/2025/04/12/ai_code_suggestions_sabotage_supply_chain/">LLMs can't stop making up software dependencies and sabotaging everything</a> (<a href="https://bsky.app/profile/daviddlevine.com/post/3lmnllla4vr2q">via</a>)
</aside>

<p>Apparently LLMs don’t just hallucinate package names (and include unnecessary <em>real</em> packages), but they hallucinate the <em>same</em> non-existent package names enough that it’s possible bad actors could register malicious packages under the made up names.</p>

<blockquote>
  <p>As noted by security firm Socket recently, the academic researchers who explored the subject last year found that re-running the same hallucination-triggering prompt ten times resulted in 43 percent of hallucinated packages being repeated every time and 39 percent never reappearing.</p>
</blockquote>

<p>This isn’t a new concept. “Typosquatting” has long been an issue, registering frequently typo’d domain names for scam sites and phishing attempts.</p>

<p>And I guess it’s already happening in the wild…</p>

<blockquote>
  <p>“When we investigate, we sometimes find realistic looking READMEs, fake GitHub repos, even sketchy blogs that make the package seem authentic,” he said, adding that Socket’s security scans will catch these packages because they analyze the way the code works.”</p>
</blockquote>

<p>This is the icing on the cake:</p>

<blockquote>
  <p>“Even worse, when you Google one of these slop-squatted package names, you’ll often get an AI-generated summary from Google itself confidently praising the package, saying it’s useful, stable, well-maintained. But it’s just parroting the package’s own README, no skepticism, no context. To a developer in a rush, it gives a false sense of legitimacy.</p>

  <p>“What a world we live in: AI hallucinated packages are validated and rubber-stamped by another AI that is too eager to be helpful.”</p>
</blockquote>

<p>Just this morning I was using Claude to prototype an AI Agent that uses MCP, and it generated a script with these super useful sounding packages:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># MCP and agent libraries
</span><span class="kn">from</span> <span class="n">mcp.agent</span> <span class="kn">import</span> <span class="n">Agent</span>
<span class="kn">from</span> <span class="n">mcp.tools</span> <span class="kn">import</span> <span class="n">ToolRegistry</span>
<span class="kn">from</span> <span class="n">mcp.tools.rss</span> <span class="kn">import</span> <span class="n">RssFeedTool</span>
<span class="kn">from</span> <span class="n">mcp.tools.github</span> <span class="kn">import</span> <span class="n">GitHubTool</span>
<span class="kn">from</span> <span class="n">mcp.memory</span> <span class="kn">import</span> <span class="n">MemoryStore</span>
<span class="kn">from</span> <span class="n">mcp.scheduler</span> <span class="kn">import</span> <span class="n">Scheduler</span>
<span class="kn">from</span> <span class="n">mcp.llm</span> <span class="kn">import</span> <span class="n">LLMClient</span>
</code></pre></div></div>

<p>As an experienced developer I was skeptical that such a convenient collection of tools existed for such a new standard, so I followed up:</p>

<blockquote class="prompt">
This looks great, can you point me to documentation for the mcp libraries?
</blockquote>

<p>And got the response:</p>

<blockquote class="prompt">
I should clarify something important: the MCP (Model Context Protocol) libraries used in my example (mcp.agent, mcp.tools, etc.) are conceptual and don't currently exist as published packages in this exact form. I created a hypothetical implementation based on what an ideal MCP framework might look like.
</blockquote>

<p>Thanks for the clarification, Claude.</p>

<p>So, how would one profit off of malicious mcp libraries? Asking for a friend.</p>]]></content><author><name>Chris James Martin</name></author><category term="links" /><category term="ai" /><category term="llm" /><category term="security" /><summary type="html"><![CDATA[Practice safe vibecoding, kids.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roundhere.net/assets/images/LIEbraries.png" /><media:content medium="image" url="https://roundhere.net/assets/images/LIEbraries.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Resurrecting launchcalendar.org</title><link href="https://roundhere.net/projects/2025/04/zombie-launchcalendar/" rel="alternate" type="text/html" title="Resurrecting launchcalendar.org" /><published>2025-04-11T00:00:00+00:00</published><updated>2025-04-11T00:00:00+00:00</updated><id>https://roundhere.net/projects/2025/04/zombie-launchcalendar</id><content type="html" xml:base="https://roundhere.net/projects/2025/04/zombie-launchcalendar/"><![CDATA[<p>I’m reviving an old side project called <a href="https://launchcalendar.org">launchcalendar.org</a>, which I began in 2016. I started it when I realized I had no idea how many rockets were being launched weekly (way more than I thought!).</p>

<p>I was learning about them after the fact via <a href="https://flickr.com/photos/cjmartin/galleries/72157663755931904/">images posted to Flickr</a>, but I wanted a calendar feed to subscribe to that would let me track the launch times with links to a live stream, payload information, and launch location. Basically calendar invites to watch rockets take off.</p>

<p>At the time, I created a rough prototype using Jekyll + GitHub Pages. Each post would be a launch, and an iCal calendar file, which could be subscribed to in Google Calendar or Apple Calendar, was generated from the posts. However, it didn’t solve my problem because I still needed to track and enter all the launches. Launches are often delayed, and those updates needed to be reflected, so I was manually entering all the data.</p>

<p>While the system worked and would have been great for subscribers, it didn’t solve my use case. I still had to do all the work to track and enter the launches. I ended up taking on some contract work, and the project fell off my plate.</p>

<p>I still think it’s a good idea, and the scope is small enough to be a good project to explore interests without requiring a whole lot of “other stuff.” Since it was built with Jekyll and hosted on GitHub Pages (no server logic, no DB), everything was still functional. I re-registered the domain, pointed the DNS at the GitHub Pages repo, and reactivated my Mapbox account because I was using Mapbox maps on the individual launch pages, which I never finished.</p>

<p>With those two things done, the project was up and running just as I left it in 2016: <a href="https://launchcalendar.org">launchcalendar.org</a>.</p>

<figure class="captioned" style="width: 500px;">
  <img src="/assets/images/launchcalendar/index-2016.png" alt="Ugly list of launch schedule! - Still ugly, but back up and running." />
  <figcaption>Ugly list of launch schedule! - Still ugly, but back up and running.</figcaption>
</figure>

<p>There’s a solid foundation for a working system. With newer technologies and workflows, I believe I can make this project better. My immediate plan is to clean up the website and finalize design ideas for each launch page. Finish it to the point I wanted it to be in 2016, which still doesn’t solve the issue of data input.</p>

<h4 id="data-entry-plans">Data Entry Plans</h4>

<p>To get data into the system, I plan to set up a process where humans (probably me) can enter and update launch data on the website. Updates will generate pull requests, which I can review and approve. Once approved, they’ll be merged, and the data will update automatically.</p>

<h4 id="ai-agent">AI Agent</h4>

<p>There are a number of sites that publish great data about launches and space news in general. I did not and do not want to scrape anything to programmatically pull data. However, I think if an AI agent could search for information and tell me about it, that would solve much of the problem!</p>

<p>I plan to build an AI agent to search for launch data and generate draft entries and updates about launches. The agent will search for upcoming launches, live stream links, photos, updates/delays, etc., and open pull requests like a human would. Then I can review the PRs, ensure all the info is correct, make extra sure everything is attributed correctly with links to sources, and then publish the updates. I wouldn’t trust an AI agent to always be correct and automate the whole process, but if it can find relevant information and file pull requests for me to review, I think this thing could work.</p>

<h4 id="other-improvements">Other Improvements</h4>

<p>I’d like to add Bluesky posts in addition to the iCal calendar feed, so people can follow on Bluesky if they’d prefer. I briefly started thinking about rebuilding this whole thing on AT Protocol (I really want to build something on AT Protocol), but the Jekyll setup is so beautifully simple that I think it’s perfect for this project.</p>

<h4 id="follow-along">Follow Along</h4>

<p>I’ll post as I make progress here, but you can also <a href="webcal://launchcalendar.org/calendar.ics">subscribe in your calendar app</a>, or <a href="https://calendar.google.com/calendar/render?cid=https://launchcalendar.org/calendar.ics">Google calendar</a> if you want to see new launches as they’re added.</p>

<p>And of course you can <a href="https://github.com/cjmartin/launchcalendar.org">follow the whole project on GitHub</a>.</p>]]></content><author><name>Chris James Martin</name></author><category term="projects" /><category term="launchcalendar" /><category term="rockets" /><category term="space" /><category term="jekyll" /><category term="development" /><summary type="html"><![CDATA[Reviving my 2016 side project, launchcalendar.org, to track rocket launches with new tools and workflows]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roundhere.net/assets/images/launchcalendar/gcal-list.png" /><media:content medium="image" url="https://roundhere.net/assets/images/launchcalendar/gcal-list.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Tools: Jekyll Tools</title><link href="https://roundhere.net/tools/2025/04/jekyll-tools/" rel="alternate" type="text/html" title="Tools: Jekyll Tools" /><published>2025-04-09T00:00:00+00:00</published><updated>2025-04-09T00:00:00+00:00</updated><id>https://roundhere.net/tools/2025/04/jekyll-tools</id><content type="html" xml:base="https://roundhere.net/tools/2025/04/jekyll-tools/"><![CDATA[<aside class="cheshire-note">
  <a href="https://github.com/cjmartin/jekyll-tools">Jekyll Tools repo on GitHub</a>
</aside>

<p>My to-do list for this morning says “Work on Taxes.” Instead, I’m writing tools to make updates here.</p>

<p>A few things had been bugging me:</p>

<ul>
  <li>Posts were sorted by date but not time, so posts on the same day were sorted alphabetically instead of chronologically.</li>
  <li>My permalinks were overly simplistic. All posts used the format <code class="language-plaintext highlighter-rouge">/journal/title-slug</code>. While unique titles avoided namespace issues, as I added categories and date-based index pages, I wanted the URLs to reflect some of that data.</li>
  <li>Jekyll’s method of including categories in permalinks adds <em>all</em> categories like <code class="language-plaintext highlighter-rouge">/category1/category2</code>. I would like to only show a “main” category in the url.</li>
</ul>

<p>I solved these issues with a combination of plugins to handle things dynamically at build time and scripts to bake some data into the posts.</p>

<h3 id="sorting">Sorting</h3>

<p>Jekyll sorts posts by date from the filename. If you have a <a href="https://en.wikipedia.org/wiki/ISO_8601">properly formatted</a> <code class="language-plaintext highlighter-rouge">datetime</code> value in your posts’ front matter, you can sort by date + time. I don’t. I use a simple <code class="language-plaintext highlighter-rouge">time</code> value in 12-hour format because I’m lazy and don’t want to think about date formats.</p>

<p>I solved this with a custom plugin to calculate an ISO 8601 datetime value at build time from the <code class="language-plaintext highlighter-rouge">filename date</code> and post <code class="language-plaintext highlighter-rouge">time</code>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _plugins/add_datetime_field.rb</span>
<span class="nb">require</span> <span class="s1">'time'</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:post_read</span> <span class="k">do</span> <span class="o">|</span><span class="n">site</span><span class="o">|</span>
  <span class="n">site</span><span class="p">.</span><span class="nf">posts</span><span class="p">.</span><span class="nf">docs</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">post</span><span class="o">|</span>
    <span class="k">if</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'time'</span><span class="p">]</span>
      <span class="n">post_date</span> <span class="o">=</span> <span class="n">post</span><span class="p">.</span><span class="nf">date</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%Y-%m-%d'</span><span class="p">)</span>
      <span class="n">post_time</span> <span class="o">=</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'time'</span><span class="p">]</span>

      <span class="k">begin</span>
        <span class="n">time_obj</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">post_time</span><span class="p">)</span>
        <span class="n">combined</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">post_date</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">time_obj</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%H:%M'</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
        <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'datetime'</span><span class="p">]</span> <span class="o">=</span> <span class="n">combined</span><span class="p">.</span><span class="nf">iso8601</span>
      <span class="k">rescue</span> <span class="no">ArgumentError</span> <span class="o">=&gt;</span> <span class="n">e</span>
        <span class="no">Jekyll</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">warn</span> <span class="s2">"Datetime Plugin:"</span><span class="p">,</span> <span class="s2">"Could not parse time '</span><span class="si">#{</span><span class="n">post_time</span><span class="si">}</span><span class="s2">' in </span><span class="si">#{</span><span class="n">post</span><span class="p">.</span><span class="nf">path</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">e</span><span class="p">.</span><span class="nf">message</span><span class="si">}</span><span class="s2">"</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>With the <code class="language-plaintext highlighter-rouge">datetime</code> value added, sorting posts by date + time is as easy as:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">sorted_posts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">sort</span><span class="p">:</span><span class="w"> </span><span class="s2">"datetime"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">reverse</span><span class="w"> </span><span class="cp">%}</span>
<span class="cp">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">sorted_posts</span><span class="w"> </span><span class="na">limit</span><span class="o">:</span><span class="mi">10</span><span class="w"> </span><span class="cp">%}</span>
  &lt;div class="post"&gt;
    &lt;h1 class="post-title"&gt;&lt;a href="<span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="cp">}}</span>"&gt;<span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="cp">}}</span>&lt;/a&gt;&lt;/h1&gt;
    &lt;p class="meta"&gt;<span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date_to_string</span><span class="w"> </span><span class="cp">}}{%</span><span class="w"> </span><span class="nt">if</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">place</span><span class="w"> </span><span class="cp">%}</span> &amp;#8212; <span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">place</span><span class="w"> </span><span class="cp">}}{%</span><span class="w"> </span><span class="nt">endif</span><span class="w"> </span><span class="cp">%}</span>&lt;/p&gt;
    &lt;div class="post-content text"&gt;
      <span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">content</span><span class="w"> </span><span class="cp">}}</span>
    &lt;/div&gt;
  &lt;/div&gt;
<span class="cp">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="cp">%}</span>
</code></pre></div></div>

<p>For paginated pages, I used the <code class="language-plaintext highlighter-rouge">jekyll-pagination-v2</code> plugin, which supports sorting.</p>

<p>The v2 plugin is drop-in compatible with <code class="language-plaintext highlighter-rouge">jekyll-pagination</code>, all I had to do was update my <code class="language-plaintext highlighter-rouge">_config.yml</code> from:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">paginate</span><span class="pi">:</span> <span class="m">10</span>
<span class="na">paginate_path</span><span class="pi">:</span> <span class="s">/journal/page:num/</span>
</code></pre></div></div>

<p>To ↓</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pagination</span><span class="pi">:</span>
  <span class="na">enabled</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">per_page</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">permalink</span><span class="pi">:</span> <span class="s">/page:num/</span>
  <span class="na">sort_field</span><span class="pi">:</span> <span class="s">datetime</span>
  <span class="na">sort_reverse</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>Now posts are sorted by date + time on the main index and paginated <code class="language-plaintext highlighter-rouge">/journal</code> pages. Yay!</p>

<h3 id="updating-permalinks">Updating Permalinks</h3>

<p>Before updating permalinks, I ensured existing links would still work. The <a href="https://github.com/jekyll/jekyll-redirect-from"><code class="language-plaintext highlighter-rouge">jekyll-redirect-plugin</code></a> maps old permalinks to new ones.</p>

<blockquote>
  <p>Redirects are performed by serving an HTML file with an HTTP-REFRESH meta tag pointing to your destination. No .htaccess file, nginx conf, xml file, or anything else is generated. It simply creates HTML files.</p>
</blockquote>

<p>Not as great as a proper 301 or 302, but fine for my needs.</p>

<p>After installing the plugin, I added this to <code class="language-plaintext highlighter-rouge">_config.yml</code> to avoid generating a <code class="language-plaintext highlighter-rouge">redirects.json</code> that I don’t need:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">redirect_from</span><span class="pi">:</span>
  <span class="na">json</span><span class="pi">:</span> <span class="kc">false</span>
</code></pre></div></div>

<p><strong>Since all existing posts needed redirects</strong>, I used a Python script to write the old permalink into each post’s front matter.</p>

<p>See <a href="https://github.com/cjmartin/jekyll-tools/blob/main/add_redirects.py"><code class="language-plaintext highlighter-rouge">add_redirects.py</code></a> and its <a href="https://github.com/cjmartin/jekyll-tools/blob/main/README.md#add_redirectspy">README section</a>.</p>

<p>After running ↑ that script ↑ every existing post had an entry in it’s front matter like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">redirect_from</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">/journal/voice-memo-manager/</span>
</code></pre></div></div>

<p>To handle requests to the old url.</p>

<p>Once redirects were set up, I updated the default post permalink in <code class="language-plaintext highlighter-rouge">_config.yaml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">defaults</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">posts"</span>
    <span class="na">values</span><span class="pi">:</span>
      <span class="na">permalink</span><span class="pi">:</span> <span class="s">/journal/:year/:month/:slug/</span>
</code></pre></div></div>

<p>Now posts have permalinks with year and month, which will be useful for future index pages. Old <code class="language-plaintext highlighter-rouge">/journal/:title</code> links redirect nicely. Huzzah.</p>

<h3 id="single-category-permalinks">Single Category Permalinks</h3>

<p>Jekyll supports <code class="language-plaintext highlighter-rouge">:categories</code> in permalinks, but I dislike how it handles posts in multiple categories. For example, this post is in both <code class="language-plaintext highlighter-rouge">tools</code> and <code class="language-plaintext highlighter-rouge">field-notes</code>. It will appear on <code class="language-plaintext highlighter-rouge">/tools</code> and <code class="language-plaintext highlighter-rouge">/field-notes</code> category pages.</p>

<p>Using <code class="language-plaintext highlighter-rouge">/:categories/:year/:month/:slug/</code> would result in <code class="language-plaintext highlighter-rouge">/tools/field-notes/2025/04/jekyll-tools</code>. I dislike this because <code class="language-plaintext highlighter-rouge">/tools/field-notes</code> will never be a valid category URL.</p>

<p>I prefer specifying a <code class="language-plaintext highlighter-rouge">link_category</code> in the post front matter to specify the primary category in the permalink.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">categories</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">tools</span>
  <span class="pi">-</span> <span class="s">field-notes</span>
<span class="na">link_category</span><span class="pi">:</span> <span class="s">tools</span>
</code></pre></div></div>

<p>So the post permalink becomes <code class="language-plaintext highlighter-rouge">/tools/2025/04/jekyll-tools</code>.</p>

<p>Jekyll doesn’t support custom front matter variables in permalinks, so I created a plugin to set a permalink for posts with <code class="language-plaintext highlighter-rouge">link_category</code> or <code class="language-plaintext highlighter-rouge">categories</code>.</p>

<ul>
  <li>If <code class="language-plaintext highlighter-rouge">link_category</code> is set, it is used as the primary category in the permalink.</li>
  <li>If <code class="language-plaintext highlighter-rouge">link_category</code> is not set but the post has <code class="language-plaintext highlighter-rouge">categories</code>, the first category is used.</li>
</ul>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _plugins/add_custom_permalink.rb</span>
<span class="nb">require</span> <span class="s1">'time'</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:post_read</span> <span class="k">do</span> <span class="o">|</span><span class="n">site</span><span class="o">|</span>
  <span class="n">site</span><span class="p">.</span><span class="nf">posts</span><span class="p">.</span><span class="nf">docs</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">post</span><span class="o">|</span>
    <span class="c1"># Determine the link_category</span>
    <span class="n">link_category</span> <span class="o">=</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'link_category'</span><span class="p">]</span>
    <span class="k">if</span> <span class="o">!</span><span class="n">link_category</span> <span class="o">&amp;&amp;</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'categories'</span><span class="p">]</span> <span class="o">&amp;&amp;</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'categories'</span><span class="p">].</span><span class="nf">any?</span>
      <span class="n">link_category</span> <span class="o">=</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'categories'</span><span class="p">].</span><span class="nf">first</span>
    <span class="k">end</span>

    <span class="c1"># Skip this post if no link_category or categories are available</span>
    <span class="k">next</span> <span class="k">unless</span> <span class="n">link_category</span>

    <span class="c1"># Extract year and month from the post date</span>
    <span class="n">year</span> <span class="o">=</span> <span class="n">post</span><span class="p">.</span><span class="nf">date</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%Y'</span><span class="p">)</span>
    <span class="n">month</span> <span class="o">=</span> <span class="n">post</span><span class="p">.</span><span class="nf">date</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s1">'%m'</span><span class="p">)</span>
    <span class="n">slug</span> <span class="o">=</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'slug'</span><span class="p">]</span> <span class="o">||</span> <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'title'</span><span class="p">].</span><span class="nf">downcase</span><span class="p">.</span><span class="nf">strip</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="s2">" "</span><span class="p">,</span> <span class="s2">"-"</span><span class="p">).</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[^\w-]/</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>

    <span class="c1"># Generate the custom permalink</span>
    <span class="n">custom_permalink</span> <span class="o">=</span> <span class="s2">"/</span><span class="si">#{</span><span class="n">link_category</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">year</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">month</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">slug</span><span class="si">}</span><span class="s2">/"</span>

    <span class="c1"># Set the custom permalink</span>
    <span class="n">post</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'permalink'</span><span class="p">]</span> <span class="o">=</span> <span class="n">custom_permalink</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now posts with categories have permalinks including one primary category instead of the default <code class="language-plaintext highlighter-rouge">/journal</code>. Hooray!</p>

<p>I also added a script to set <code class="language-plaintext highlighter-rouge">link_category</code> for all posts with existing categories. See <a href="https://github.com/cjmartin/jekyll-tools/blob/main/set_link_category.py"><code class="language-plaintext highlighter-rouge">set_link_category.py</code></a> and its <a href="https://github.com/cjmartin/jekyll-tools/blob/main/README.md#set_link_categorypy">README section</a>.</p>

<p>This script scans posts and sets the first category as <code class="language-plaintext highlighter-rouge">link_category</code> in their front matter. While the plugin fallback handles this, the script ensures permalinks won’t break if I add categories to old posts.</p>

<p>Now I have sorted posts with permalinks that will work well with future index pages. Not bad for a morning of not getting my taxes done!</p>]]></content><author><name>Chris James Martin</name></author><category term="tools" /><category term="field-notes" /><category term="jekyll" /><category term="development" /><category term="tools" /><summary type="html"><![CDATA[Jekyll Tools repo on GitHub]]></summary></entry><entry><title type="html">Tools: Voice Memo Manager</title><link href="https://roundhere.net/tools/2025/04/voice-memo-manager/" rel="alternate" type="text/html" title="Tools: Voice Memo Manager" /><published>2025-04-08T00:00:00+00:00</published><updated>2025-04-08T00:00:00+00:00</updated><id>https://roundhere.net/tools/2025/04/voice-memo-manager</id><content type="html" xml:base="https://roundhere.net/tools/2025/04/voice-memo-manager/"><![CDATA[<aside class="cheshire-note">
  <a href="https://github.com/cjmartin/Voice-Memo-Manager">Voice Memo Manager on Github</a>
</aside>

<p>I spend a lot of time in the car driving my kids to and from school and (many, many) other activities. This means that roughly half of my driving time is occupied with conversation and kid-centric audio, but the other half is great for thinking, listening to podcasts, and turning over thoughts and ideas in my head.</p>

<p>The non-kid car time generally adds up to <em>at least</em> an hour a day. Unfortunately much of the thoughts and ideas I might have during this time are lost or forgotten because I don’t have a good system for documenting them in the moment.</p>

<p><a href="https://interconnected.org/home/2025/03/20/diane">Matt Webb’s recent post</a> about using <a href="https://whispermemos.com">Whisper Memos</a> to transcribe his verbal outline of a talk (that he recorded while on a run), and then run that transcript through Claude to produce a high-level outline got me thinking about how I could do something similar to capture my thoughts while driving around.</p>

<h3 id="process-development">Process Development</h3>

<p><strong>Problem</strong></p>

<p>I immediately ran into an issue. Much of the time my thinking in the car is happening while I’m <em>listening</em> to something. Often the thoughts and ideas are directly derived or inspired by content that I’m consuming in an auditory format.</p>

<p>This is not super compatible with Whisper Memos, which seems designed to take a long(er) form recording and transcribe it into legible paragraphs (very cool), then send it in am email. I tried recording a new memo each time I wanted to note something, but that quickly led to a bunch of short clips, each generating a transcript and it’s own email. This seems messy, and just not the use case Whisper Memos is designed for.</p>

<p><strong>Solution</strong></p>

<p>iOS already has a voice memos app, which conveniently syncs all of your notes to the corresponding MacOS version. Bonus, I can use Siri to record a new voice memo; this seems like it might have potential!</p>

<p>But, The macOS Voice Memos app is… not great.</p>

<p>It’s nice that it automatically syncs recordings across your iPhone, Apple Watch, and Mac. But as of MacOS 14 (Sonoma), which I’m currently running, it still doesn’t support transcriptions. There’s no way to generate a transcript from a voice memo—let alone export one. You can’t even multi-select memos in the sidebar to drag them into another app. The actual audio files are buried somewhere in the file system.</p>

<p>This felt like a perfect use case for a bit of AI-assisted “vibe coding.”</p>

<p>I asked ChatGPT “Is there a way to access recordings and transcripts from the iOS Voice Memos app on MacOS?” (it was correct that you can, but then incorrect about where the files are stored), then continued with a very long chat that eventually led to a full-on, web-based local application. It lets me access and work with voice memos synced from my mobile devices to my Mac.</p>

<p>The process was surprisingly fun. ChatGPT needed a lot of help along the way, but it is entirely possible to very quickly write useful tools with ChatGPT as an assistant that doesn’t mind if you ask it to tedious things over and over again. The result was a genuinely useful (if not particularly beautiful) tool.</p>

<p><img src="/assets/images/vmm.gif" alt="Voice Memos Manager" /></p>

<p>I won’t get into the specific details of the app’s functionality here, but if you’re looking for something like this you can see the <span data-note="Is the code perfect and beautiful? No. But it doesn't need to be. I would have gotten caught up in perfectionism if I sat down and wrote this thing from scratch; it wouldn't exist, much less be up on Github.">code</span> and very detailed (thanks ChatGPT) <a href="https://github.com/cjmartin/Voice-Memo-Manager">Readme on GitHub</a>.</p>

<p>If you’re more curious about the process of building with ChatGPT, you can see the <a href="https://chatgpt.com/share/67f56359-7de8-8003-b552-00800ee724bc">full chat on ChatGPT</a>.</p>

<h3 id="does-it-work">Does it work?</h3>

<p>Yes! I had been listening to an <a href="https://se-radio.net/2025/01/se-radio-651-paul-frazee-on-bluesky-and-the-at-protocol/">interview with Paul Frazee</a> about Bluesky and the AT Protocol, and over the few days of listening I recorded ~20 individual voice memos of my takeaways from the conversation. I was able to quickly organize, transcribe, and export those notes as one big chunk of text; then work with it to create a <a href="https://roundhere.net/journal/se-radio-paul-frazee/">cleaned up summary</a> of what I learned. I retain things much better if I write them down, and I never would have gotten it done without those transcribed voice memos.</p>

<p>I’ve started recording all kinds of thoughts that otherwise would have been forgotten in the chaos of my daily life. This little tool that I never would have built without “vibe coding” makes those recordings useful to me.</p>

<p><a href="https://roundhere.net/journal/context-window-matt-webb/"><em>“On the one hand, we have difficult things become easy; on the other hand, we have easy things become absolutely trivial”</em></a> - indeed.</p>]]></content><author><name>Chris James Martin</name></author><category term="tools" /><category term="tools" /><category term="ai" /><category term="code" /><category term="openai" /><category term="development" /><category term="vibe code" /><summary type="html"><![CDATA[I built a simple voice memo tool with ChatGPT to organize and transcribe thoughts captured with the iOS/MacOS/WatchOS voice memos app. Inspired by Whisper Memos, it helps me turn scattered thoughts into something usable.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roundhere.net/assets/images/vmm.gif" /><media:content medium="image" url="https://roundhere.net/assets/images/vmm.gif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Listening: Prefetcher on Building PinkSea on the AT Protocol</title><link href="https://roundhere.net/links/2025/04/listening-software-sessions-pinksea/" rel="alternate" type="text/html" title="Listening: Prefetcher on Building PinkSea on the AT Protocol" /><published>2025-04-07T00:00:00+00:00</published><updated>2025-04-07T00:00:00+00:00</updated><id>https://roundhere.net/links/2025/04/listening-software-sessions-pinksea</id><content type="html" xml:base="https://roundhere.net/links/2025/04/listening-software-sessions-pinksea/"><![CDATA[<aside class="cheshire-note">
  <a href="https://www.softwaresessions.com/episodes/pinksea/">Sorfware Sessions: Prefetcher on Building PinkSea on the AT Protocol</a>
</aside>

<blockquote>
  <p>“ATProto is a massive network, and at least for me, when I saw the initial graph, I was just very confused. I absolutely did not know what I was looking at. But let’s start with the base building block… the PDS.”</p>
</blockquote>

<p>I was looking for more info about AT Protocol from an independent developer perspective, and found this <a href="https://www.softwaresessions.com/episodes/pinksea">Software Sessions podcast episode</a> featuring <a href="https://bsky.app/profile/prefetcher.miku.place">Prefetcher</a>, where he discusses building <a href="https://pinksea.art">PinkSea</a> on the AT Protocol.</p>

<p>Around half way through the episide the conversation gets into the technical aspects of his development process, particularly around the AT Protocol’s infrastructure, including Personal Data Servers (PDS), relays, app views, the PLC directory, and DIDs. I enjoyed the whole thing, but if you want to jump straight to the AT Protocol technical info, it starts around 32 minutes in.</p>

<p>This is the first Software Sessions podcast I’ve listened to, and I appreciated the podcast’s structured format, which allows for easy navigation between sections, even on CarPlay. If I ever make a podcast again I’ll have to include <a href="https://podcasters.apple.com/support/5482-using-chapters-on-apple-podcasts">chapter metadata</a>. It’s very well done.</p>]]></content><author><name>Chris James Martin</name></author><category term="links" /><category term="podcast" /><category term="atprotocol" /><category term="development" /><summary type="html"><![CDATA[ATProto is a massive network, and at least for me, when I saw the initial graph, I was just very confused. I absolutely did not know what I was looking at. But let's start with the base building block… the PDS.]]></summary></entry></feed>