Weeknotes – Paul Mucur2023-11-20T12:00:28+00:00https://mudge.name/weeknotesPaul MucurWeeknotes #103https://mudge.name/2023/06/29/weeknotes-103/2023-06-29T10:38:00+00:002023-06-29T10:38:00+00:00Travelling to Brighton, migrating to Porkbun, and self-hosting GitHub Actions runners.<ul>
<li>
<p>I’m writing these notes from a train on Platform 6 in Leeds station, the second of four trains that will take me to Brighton for tomorrow’s <a href="https://brightonruby.com/">Brighton Ruby</a> conference.</p>
</li>
<li>
<p>To be even more specific, as with my past two weeknotes, I’m using <a href="https://ia.net/writer">iA Writer</a> and <a href="https://workingcopy.app">Working Copy</a> to publish these on the move.</p>
</li>
<li>
<p>The liquidation of <a href="https://www.ghostcassette.com/">my company</a> is steadily progressing with all debts settled and a shareholders’ meeting scheduled for next week. In reality, that will involve me and a solicitor on a Teams call but perhaps I’ll wear a cravat.</p>
</li>
<li>
<p>After over a decade of being a <a href="https://www.gandi.net/en-GB">Gandi</a> customer, the <a href="https://chaos.social/@jonty/110542930325547466">recent announcement they will be doubling their prices</a> following their <a href="https://domainnamewire.com/2023/03/02/total-web-solutions-acquires-domain-registrar-gandi-forming-new-entity/">acquisition</a> finally pushed me to switch my domains to <a href="https://porkbun.com/">Porkbun</a>.</p>
<p>If you’re considering the same, I recommend their <a href="https://kb.porkbun.com/article/89-how-to-transfer-a-domain-to-porkbun-with-no-downtime">guide to transferring domains with little to DNS downtime</a>.</p>
<p>The only difficulty I had during the transfer is that Porkbun don’t offer a way to import an existing zone file so you have to manually enter each record using their web interface. This is no great hardship however, giving me an excuse to audit everything and the move seems to have gone smoothly.</p>
</li>
<li>
<p>We’ve been using <a href="https://docs.github.com/en/actions">GitHub Actions</a> more and more at work and I was concerned we would burn through our monthly credit limit. Thankfully, GitHub make it easy to <a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners">self-host runners</a> and <a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/managing-access-to-self-hosted-runners-using-groups">restrict access to selected repositories</a> using runner groups. We’ve now shifted the majority of our scheduled jobs to our own private runners.</p>
<p>I’d recommend trying to match the <a href="https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources">resources and operating systems of GitHub’s own runners</a> otherwise you might run into issues with actions that rely on prebuilt software such as <a href="https://github.com/ruby/setup-ruby#supported-platforms">setup-ruby</a>. We went for a clean install of <a href="https://releases.ubuntu.com/jammy/">Ubuntu 22.04</a>, installing only <code class="language-plaintext highlighter-rouge">curl</code> and <code class="language-plaintext highlighter-rouge">build-essential</code> before setting up the action runner and it is working well so far.</p>
</li>
<li>
<p>I’m looking forward to seeing some old faces from <a href="https://london.computation.club/">London Computation Club</a> as I haven’t seen most of them for several years now. “Remember the time we <a href="https://github.com/computationclub/computationclub.github.io/wiki/The-Shunting-Yard-Algorithm">played with wooden trains</a>?”</p>
</li>
</ul>
Weeknotes #102https://mudge.name/2023/06/21/weeknotes-102/2023-06-21T09:25:00+00:002023-06-21T09:25:00+00:00Another holiday, a cousin calculator, the dangers of i-SPY, the inhumanity of siblings, and avoiding thinking about computers.<ul>
<li>
<p>We’re on holiday again. C–– is currently pushing a toy construction vehicle around a grand entranceway filled with portraits of First World War generals.</p>
</li>
<li>
<p>We’re staying in a house in the Scottish Borders with E––’s extended family, a group of 19: 14 adults and five children.</p>
</li>
<li>
<p>Logistics are somewhat complicated and I find myself relearning hard-won parenting lessons, e.g. there’s a reason we avoid three course evening meals with a one- and three-year old (because they are a nightmare).</p>
</li>
<li>
<p>The children are cousins of some sort (<a href="http://www.searchforancestors.com/utility/cousincalculator.html">Ancestor Search Cousin Relationship Calculator</a> tells me they are <em>second</em> cousins) and similar in age (two one-year-olds and three three-year-olds) so they have been playing together. They quickly found a gnarled tree to clamber up though the nature of three-year-olds’ interpersonal skills led my first cousin to name it the “Tree of Tears”.</p>
</li>
<li>
<p>As C–– grows increasingly bored with long drives, we attempted to spice up the trip to Scotland with an <a href="https://collins.co.uk/collections/i-spy">i-SPY</a> book. Unfortunately, this backfired when he complained of feeling “poorly” and looked suspiciously pale. We managed to pull over in time before any breakfasts made an unwelcome second appearance but we might avoid intense reading on future car journeys.</p>
</li>
<li>
<p>Coming from a small family, it is a novelty to witness bickering between siblings of all ages: some in their thirties and forties, others in their sixties and seventies. Normally kind and courteous relatives are, at times, downright horrid to one another, hands raised in a universal signal of “stop talking” and the occasional dreaded shushing when someone tries to interject. Maybe I’ll introduce <a href="https://gds.blog.gov.uk/2016/10/07/platform-as-a-service-team-takes-even-handed-approach-to-meetings/">the GDS team’s full series of hand signals</a> to broker a fragile peace.</p>
</li>
<li>
<p>I posted <a href="https://ruby.social/@mudge/110536287876333703">a short thread to ruby.social about my experience starting our own mail server to send transactional mail</a> at work despite it seeming an extremely ill-advised thing to do in 2023.</p>
</li>
<li>
<p>Otherwise, I have successfully avoided thinking too much about computers of various sizes throughout the holiday.</p>
</li>
<li>
<p>The only exception was figuring out how to cobble together a baby monitor out of an iPhone and a MacBook (as our normal baby monitor doesn’t have the range in this big, old house) using a muted FaceTime call and <a href="https://support.apple.com/en-gb/guide/iphone/iph29145acf1/ios#:~:text=You%20can%20automatically%20direct%20the,then%20choose%20an%20audio%20destination">iOS’s auto-answer functionality</a>.</p>
</li>
</ul>
Weeknotes #101https://mudge.name/2023/06/05/weeknotes-101/2023-06-05T13:46:00+00:002023-06-05T13:46:00+00:00A trip abroad, winding up my company, hearing aids and failing to discipline.<ul>
<li>
<p>We’re in Turkey (or is that <a href="https://en.m.wikipedia.org/wiki/Name_of_Turkey">Türkiye</a>?) for the first time in eight years.</p>
</li>
<li>
<p>It is our first time travelling abroad with the kids but a combination of a) the novelty of waking up at 3am and seeing Ilkley by night, b) flying on an aeroplane, and c) a cache of offline episodes of “<a href="https://en.m.wikipedia.org/wiki/Thomas_%26_Friends">Thomas & Friends: Big World! Big Adventures!</a>” saw us through.</p>
</li>
<li>
<p>We’re staying with my parents in a house on the Aegean coast. They normally come every year and the internet (essential for an afternoon binge-watch of “<a href="https://www.netflix.com/title/81033141">Mighty Express</a>”) was repeatedly flaking out. Given their wireless router had that telltale yellowed ABS plastic and all around us are reminders of the awesome destructive power of sea air, I convinced them to replace it with a new one from the local <a href="https://www.teknosa.com/">Teknosa</a>.</p>
</li>
<li>
<p>Thankfully, their old AirTies router revealed its PPPoE connection credentials (set up by a family friend and therefore undocumented) in two ways: by inspecting the <code class="language-plaintext highlighter-rouge">value</code> of the password field in the web-based admin panel and by reading the <code class="language-plaintext highlighter-rouge">config.bin</code> file downloaded when you choose to back up the router settings. The latter is an easily read XML configuration file.</p>
</li>
<li>
<p>After four years professionally programming computers (and helping others do the same) as <a href="https://www.ghostcassette.com/">Ghost Cassette Ltd</a>, I have taken a permanent role at <a href="https://www.raspberrypi.com/">Raspberry Pi</a> and am winding up my company.</p>
</li>
<li>
<p>More specifically, I’m starting a <a href="https://www.gov.uk/liquidate-your-company/members-voluntary-liquidation">members’ voluntary liquidation</a>. It is a little sad to formally declare I “do not want to run the business any more” as it saw me through the birth of two children and our move up t’north. On the other hand, I’m looking forward to dismantling the Rube Goldberg machine of exactly the right amount of dividends and salary I used to pay myself and being a Pay As You Earn employee again.</p>
</li>
<li>
<p>My mum’s hearing is faltering to the point of failure but she refuses to wear her hearing aids (“they don’t make a difference!” she protests, convincing no-one). As communication is increasingly difficult, I’ve showed her how to use <a href="https://support.apple.com/en-gb/guide/iphone/iph8bf9386f5/ios">iOS Live Listen</a> with her AirPods about which she is much more enthusiastic. I hoped she would benefit from <a href="https://support.apple.com/en-lk/guide/airpods/dev966f5f818/web">Conversation Boost</a> but that is a feature only available with AirPods Pro. Rather than give her much-more-expensive hearing aids another go, she’s ordered a pair.</p>
</li>
<li>
<p>Disciplining a three-and-a-half year old is unsurprisingly difficult but I hit a low point yesterday during a fight with C—— about perhaps <em>not</em> pouring bubble mixture all over his grandfather’s table when he said “maybe I should have a different daddy.”</p>
</li>
</ul>
Weeknotes #100https://mudge.name/2022/11/20/weeknotes-100/2022-11-20T13:54:56+00:002022-11-20T13:54:56+00:00Self-hosting Mastodon, MIME negotiation for Rails APIs take two and tooting with abandon.<ul>
<li>
<p>With the flurry of interest in <a href="https://github.com/mastodon/mastodon">Mastodon</a> and <a href="https://activitypub.rocks">ActivityPub</a>, I was hoping to <a href="https://til.simonwillison.net/mastodon/custom-domain-mastodon">run an instance on my own domain</a>. However, to make things more challenging, I wanted to see if there was a way to do it without paying for any more infrastructure.</p>
<p class="center"><img src="/i/pi400.jpg" width="425" height="425" alt="A Raspberry Pi 400 on top of a filing cabinet connected to an eero" /></p>
<p>I set up my <a href="https://www.raspberrypi.com/products/raspberry-pi-400/">Raspberry Pi 400</a> as per the <a href="https://docs.joinmastodon.org/admin/install/">installing Mastodon from source</a> instructions (though I needed to follow <a href="https://tomstu.art">Tom</a>’s advice and <code class="language-plaintext highlighter-rouge">export NODE_OPTIONS=--openssl-legacy-provider</code> in order for the JavaScript dependencies to install on Node.js 18) and set about exposing it to the internet.</p>
<p>This led me to discover that my home network had a <a href="https://support.eero.com/hc/en-us/articles/207621056-How-do-I-set-up-my-eero-if-I-want-to-keep-my-existing-router-">double NAT</a>: my ISP’s router was running one network and my <a href="https://www.eero.com">eeros</a> were running another. After repeatedly breaking my entire network to try and fix it (which involved <a href="https://gist.github.com/chriscpritchard/db98167c0a1372ef16e131bfe9b76956">SSHing into my router as <code class="language-plaintext highlighter-rouge">engineer</code> to get my PPPoE credentials</a>) before discovering my <a href="https://support.eero.com/hc/en-us/articles/207852843-Does-eero-support-PPPoE-">eeros don’t support connecting directly to the internet</a>, I ended up forwarding both HTTP and HTTPS to my instance.</p>
<p>As I didn’t want to pay for a static IP, I wrote a bash script to update a <code class="language-plaintext highlighter-rouge">CNAME</code> on <code class="language-plaintext highlighter-rouge">mudge.name</code> every five minutes with my public IP as reported by <a href="https://api.ipify.org">ipify</a> using <a href="https://api.gandi.net/docs/livedns/">GANDI’s LiveDNS API</a>.</p>
<p>I then followed the <a href="https://masto.host/mastodon-usernames-different-from-the-domain-used-for-installation/">instructions to use <code class="language-plaintext highlighter-rouge">@mudge@mudge.name</code> rather than <code class="language-plaintext highlighter-rouge">@mudge@social.mudge.name</code></a> by <a href="https://github.com/mudge/mudge.github.com/commit/46be2ab6ab2de81b6b2b6f90fcbdba3b7a51eeed">serving a response for <code class="language-plaintext highlighter-rouge">https://mudge.name/.well-known/host-meta</code> that pointed to my instance</a>. Tom pointed out this works because <a href="https://github.com/mastodon/mastodon/blob/v3.5.3/app/lib/webfinger.rb#L61-L62">Mastodon will fall back to checking <code class="language-plaintext highlighter-rouge">host-meta</code> if its WebFinger request fails with a 404</a>.</p>
<p>Once everything was working, I started to grow a little unsure of the wisdom of exposing two ports into my home network from the internet despite my attempts to lock things down with <code class="language-plaintext highlighter-rouge">iptables</code> and <a href="http://www.fail2ban.org/wiki/index.php/Main_Page">Fail2ban</a>.</p>
<p>Several days in, we had a power cut after which I could no longer SSH into my server. At that point, I decided I’d leave the hosting to <a href="https://ruby.social/@james">James</a> and stick with <a href="https://ruby.social/">Ruby.social</a>.</p>
</li>
<li>
<p>I’ve been working on a streaming API using <a href="https://github.com/rack/rack/blob/main/SPEC.rdoc#label-Hijacking">Rack Hijacking</a>. As this involves working directly with sockets, we needed a way to test this alongside our more typical Rails controllers.</p>
<p>We settled on using <a href="https://ruby-doc.org/stdlib-3.1.2/libdoc/socket/rdoc/UNIXSocket.html#pair-method"><code class="language-plaintext highlighter-rouge">UNIXSocket.pair</code></a> to simulate the two ends of a streaming connection: one socket for the client end and one socket for the server end. We can pass this into our request <code class="language-plaintext highlighter-rouge">env</code> as a result of calling <code class="language-plaintext highlighter-rouge">rack.hijack</code> and then read off anything that has been written by the application, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">it</span> <span class="s2">"streams the headers to the client"</span> <span class="k">do</span>
<span class="n">client</span><span class="p">,</span> <span class="n">server</span> <span class="o">=</span> <span class="no">UNIXSocket</span><span class="p">.</span><span class="nf">pair</span>
<span class="n">get</span> <span class="s2">"/stream"</span><span class="p">,</span> <span class="ss">env: </span><span class="p">{</span> <span class="s2">"rack.hijack"</span> <span class="o">=></span> <span class="o">-></span> <span class="p">{</span> <span class="n">server</span> <span class="p">}</span> <span class="p">}</span>
<span class="n">headers</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">readline</span><span class="p">(</span><span class="s2">"</span><span class="se">\r\n\r\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">expect</span><span class="p">(</span><span class="n">headers</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="s2">"HTTP/1.1 200</span><span class="se">\r\n</span><span class="s2">Content-Type: text/event-stream</span><span class="se">\r\n\r\n</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>In that same project, I had occasion to re-examine <a href="https://github.com/rails/rails/blob/a807a4f4f95798616a2a85856f77fdfc48da4832/actionpack/lib/action_dispatch/http/mime_negotiation.rb">MIME negotiation in Rails</a>. I <a href="/2022/03/13/weeknotes-90/">previously wrote</a> about how Rails will always prefer its notion of <code class="language-plaintext highlighter-rouge">format</code> (e.g. from a URL or file extension) over the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept">HTTP <code class="language-plaintext highlighter-rouge">Accept</code> header</a> especially if it contains a <a href="https://github.com/rails/rails/blob/a807a4f4f95798616a2a85856f77fdfc48da4832/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L171-L173">browser-like wildcard</a>. For our API, we don’t expect our clients to be browsers and would rather Rails only decide on the appropriate content type based on the <code class="language-plaintext highlighter-rouge">Accept</code> header even if it has a wildcard.</p>
<p>I wrote up <a href="https://gist.github.com/mudge/acde31a5319726b9fdba419ffe7f5bcb">an example of how to do this including a controller spec showing how it works with various examples</a>. The crux of it is the following:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApiController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="ss">:only_respect_accept_header</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">only_respect_accept_header</span>
<span class="n">request</span><span class="p">.</span><span class="nf">set_header</span><span class="p">(</span>
<span class="s2">"action_dispatch.request.formats"</span><span class="p">,</span>
<span class="n">requested_mime_types</span><span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">type</span><span class="o">|</span> <span class="n">type</span><span class="p">.</span><span class="nf">symbol</span> <span class="o">||</span> <span class="n">type</span><span class="p">.</span><span class="nf">ref</span> <span class="o">==</span> <span class="s2">"*/*"</span> <span class="p">}</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">requested_mime_types</span>
<span class="no">Mime</span><span class="o">::</span><span class="no">Type</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="nf">get_header</span><span class="p">(</span><span class="s2">"HTTP_ACCEPT"</span><span class="p">).</span><span class="nf">to_s</span><span class="p">).</span><span class="nf">presence</span> <span class="o">||</span> <span class="p">[</span><span class="no">Mime</span><span class="o">::</span><span class="no">ALL</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>I’ve been <a href="https://ruby.social/@mudge">tooting with abandon on Ruby.social</a>. It’s far easier when people aren’t mistaking you for <a href="https://en.wikipedia.org/wiki/Peiter_Zatko">another mudge</a>.</p>
</li>
</ul>
Weeknotes #99https://mudge.name/2022/11/03/weeknotes-99/2022-11-03T13:41:44+00:002022-11-03T13:41:44+00:00Pulling keycaps, a refactoring wishlist and the virtue of leaving jobs.<ul>
<li>
<p>After resisting the temptation to indulge in an exotic keyboard for my entire programming career, I switched to a <a href="https://kinesis-ergo.com/keyboards/advantage360/">Kinesis Advantage360</a> this week.</p>
<p>As a frequent sufferer of repetitive strain injury, I’d been watching out for the release of the Advantage360 since reading <a href="https://martinfowler.com/articles/kinesis-advantage2.html">Martin Fowler</a> and <a href="https://avdi.codes/why-you-should-spend-350-on-a-computer-keyboard/">Avdi Grimm’s reviews of its predecessor</a>.</p>
<p class="center"><img src="/i/kinesis.jpg" width="375" height="250" alt="" /></p>
<p>I like to think of myself as having simple tastes, rejecting indulgent customisation and configuration of hardware and software beyond the basics. This week, I found myself using a keycap puller to replace keys and getting giddy about the fact I can <a href="https://www.reddit.com/r/kinesisadvantage/comments/ykeapu/comment/iuvdr68/?utm_source=share&utm_medium=web2x&context=3">turn my <code class="language-plaintext highlighter-rouge">Esc</code> key into <code class="language-plaintext highlighter-rouge">Ctrl</code> when holding it down</a>.</p>
</li>
<li>
<p>An equally exciting purchase this week was the arrival of a replacement <a href="https://www.lamonaspares.co.uk/p/lower-basket-a1758970900">lower basket for our dishwasher</a>.</p>
</li>
<li>
<p>My recent focus on <a href="https://github.com/mudge/re2">re2</a> led me to write up a <a href="https://github.com/mudge/re2/issues/59">refactoring wishlist</a>. Unfortunately, a combination of the clocks changing and a badly-timed childhood illness means I’ve made very little progress.</p>
</li>
<li>
<p>I’ve talked about my tendency to busy myself with “jobs” and am making an effort to recognise this as less a personality quirk and more a possibly destructive form of obsession. On cue, the extractor fan in the windowless utility room where I do most of my bread-making stopped working. Rather than tackling it immediately, I forced myself to leave it until the weekend.</p>
<p>Several days later, without me getting involved, it started working again.</p>
</li>
</ul>
Weeknotes #98https://mudge.name/2022/10/27/weeknotes-98/2022-10-27T09:26:02+00:002022-10-27T09:26:02+00:00Hosting YARD documentation on GitHub Pages, pattern matching and the IKEA nesting instinct.<ul>
<li>
<p>With <a href="/2022/10/20/weeknotes-97/">last week</a>’s <a href="https://github.com/mudge/re2/releases/tag/v1.5.0">release of my gem, re2</a>, I had to update <a href="https://mudge.name/re2/">its documentation</a>. I use <a href="https://yardoc.org">YARD</a> to generate the documentation from the <a href="https://github.com/mudge/re2">source code</a> and host it using <a href="https://pages.github.com">GitHub Pages</a>.</p>
<p>After generating the latest documentation with <code class="language-plaintext highlighter-rouge">yard doc</code>, I noticed some broken links when I deployed it, specifically when trying to load the <a href="https://mudge.name/re2/_index.html">alphabetic index</a>. This is a <a href="https://github.com/lsegal/yard/issues/1027">known issue with YARD and GitHub Pages</a> as <a href="https://jekyllrb.com">Jekyll</a> will ignore any files beginning with an underscore.</p>
<p><a href="https://github.com/clowder">Chris</a> suggested that I stop GitHub Pages running the documentation through Jekyll altogether by <a href="https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/">placing a <code class="language-plaintext highlighter-rouge">.nojekyll</code> file in the root of the repository</a> and that fixed the missing index.</p>
</li>
<li>
<p><a href="https://twitter.com/tomstuart/status/1583421297240944640">Tom tweeted</a> about an <a href="https://github.com/ruby/ruby/pull/6216">upcoming change to Ruby</a> that will allow you to use <a href="https://ruby-doc.org/core-3.1.2/MatchData.html"><code class="language-plaintext highlighter-rouge">MatchData</code></a> objects when <a href="https://ruby-doc.org/core-3.1.2/doc/syntax/pattern_matching_rdoc.html">pattern matching</a>, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="no">NAN_REGEXP</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="k">in</span> <span class="p">{</span> <span class="n">sign</span><span class="p">:,</span> <span class="ss">payload: </span><span class="p">}</span>
<span class="no">Nan</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">payload: </span><span class="n">payload</span><span class="o">&</span><span class="p">.</span><span class="nf">to_i</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">negated: </span><span class="n">sign</span> <span class="o">==</span> <span class="s1">'-'</span><span class="p">)</span>
<span class="k">elsif</span> <span class="no">INFINITE_REGEXP</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="k">in</span> <span class="p">{</span> <span class="ss">sign: </span><span class="p">}</span>
<span class="no">Infinite</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">negated: </span><span class="n">sign</span> <span class="o">==</span> <span class="s1">'-'</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>As I haven’t yet tried pattern matching in recent versions of Ruby, I <a href="https://github.com/mudge/re2/pull/58">implemented it for <code class="language-plaintext highlighter-rouge">RE2::MatchData</code></a> and <a href="https://github.com/mudge/re2/releases/tag/v1.6.0">released it in 1.6.0</a>.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="no">RE2</span><span class="p">(</span><span class="s1">'foo(?P<suffix>...)'</span><span class="p">).</span><span class="nf">match</span><span class="p">(</span><span class="s1">'foobar'</span><span class="p">)</span> <span class="k">in</span> <span class="ss">suffix:
</span><span class="nb">puts</span> <span class="n">suffix</span>
<span class="k">end</span>
<span class="c1"># bar</span>
</code></pre></div> </div>
<p>It was quite fun to figure out exactly how <a href="https://mudge.name/re2/RE2/MatchData.html#deconstruct-instance_method"><code class="language-plaintext highlighter-rouge">RE2::MatchData#deconstruct</code></a> and <a href="https://mudge.name/re2/RE2/MatchData.html#deconstruct_keys-instance_method"><code class="language-plaintext highlighter-rouge">RE2::MatchData#deconstruct_keys</code></a> should behave but the <a href="https://github.com/ruby/ruby/blob/5129ca3e056e1ce3189ba39fa311d4d687b97b45/test/ruby/test_regexp.rb#L611-L641">tests in Ruby</a> were a huge help.</p>
</li>
<li>
<p>We’ve been slowly but steadily replacing flimsy things in our kitchen with <a href="https://www.oxo.com">OXO</a> Good Grips equivalents when they break:</p>
<ul>
<li><a href="https://www.oxo.com/oxo-good-grips-chef-s-precision-digital-instant-read-thermometer.html">Thermometer</a></li>
<li><a href="https://www.oxo.com/categories/cooking-and-baking/mix-measure/measuring-scales/oxo-good-grips-11-lb-stainless-steel-food-scale-w-pull-out-display.html">Scales</a></li>
<li><a href="https://www.oxo.com/salad-spinner.html">Salad spinner</a></li>
</ul>
<p>Draw your own similarities to the <a href="https://youtu.be/exL51n3py6g">IKEA scene from “Fight Club”</a>.</p>
</li>
</ul>
Weeknotes #97https://mudge.name/2022/10/20/weeknotes-97/2022-10-20T11:07:46+00:002022-10-20T11:07:46+00:00Not saving money on a Sonos Port, an update to re2 and telling a lie.<ul>
<li>
<p>After learning about the existence of the <a href="https://www.sonos.com/en-gb/shop/port">Sonos Port</a> from <a href="https://atp.fm/504">an episode of the Accidental Tech Podcast</a>, I decided to give <a href="https://sound.balenalabs.io">balenaSound</a> a go on my <a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">Raspberry Pi 4</a>.</p>
<p>I already ran <a href="https://github.com/dtcooper/raspotify">raspotify</a> on it so I could stream music to my aging hi-fi via the Pi’s 3.5mm headphone jack and some <a href="https://en.wikipedia.org/wiki/DIN_connector">DIN connectors</a> but the addition of <a href="https://www.apple.com/uk/airplay/">AirPlay 2</a> and Bluetooth was tempting.</p>
<p><img src="/i/balena.png" class="pull-right" width="375" height="379" alt="" /> Looking at <a href="https://github.com/balenalabs/balena-sound">its source code</a>, I could see <a href="https://github.com/balenalabs/balena-sound/blob/3ab86443ae02a9cbaf3648f1100e596246a61d0f/plugins/airplay/start.sh">balenaSound relies on shairport-sync</a> to power its AirPlay features but I balked at the <a href="https://github.com/mikebrady/shairport-sync/blob/98d2ecb3102550aa32edf280594a741e1bea2b25/BUILD.md">build instructions</a>. <a href="https://github.com/mattmacleod">Matt</a> recommended <a href="https://www.balena.io">balena</a> for managing Raspberry Pi devices to me years ago but this seemed the ideal time to try it. Once you get past the confusing terminology (what exactly <em>is</em> a fleet anyway?), being able to see the status of services and see live logs in a web UI is impressive.</p>
<p>Sadly, I couldn’t get any of the features to work as it seemed to only ever output sound via HDMI despite <a href="https://github.com/balenablocks/audio#environment-variables">setting various environment variables</a>.</p>
<p>In the end, I re-imaged my Pi with the <a href="https://www.raspberrypi.com/software/operating-systems/#raspberry-pi-os-32-bit">latest Raspberry Pi OS Lite</a> with <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a> and manually installed raspotify and shairport-sync. After <a href="https://www.raspberrypi.com/documentation/computers/configuration.html#changing-the-audio-output">setting the audio output with <code class="language-plaintext highlighter-rouge">raspi-config</code></a>, everything works fine.</p>
<p>I would say this saved me the price of a Port but I impulse bought a <a href="https://www.sonos.com/en-gb/shop/sub">Sub</a> instead.</p>
</li>
<li>
<p>Two weeks after A—— was born, someone <a href="https://github.com/mudge/re2/pull/56">contributed a pull request</a> against <a href="https://github.com/mudge/re2">my gem, re2</a>.</p>
<p>This week, after some back and forth, I was finally able to release the new functionality in <a href="https://github.com/mudge/re2/releases/tag/v1.5.0">re2 1.5.0</a>.</p>
<p>The main change is the addition of an <code class="language-plaintext highlighter-rouge">RE2::Set</code> API which binds to the <a href="https://github.com/google/re2/blob/b733fc4bff1796842c89ca67a6c4b11e521cde73/re2/set.h">underlying library’s class of the same name</a>. However, this behaves differently between ABI versions of the library:</p>
<ul>
<li>Version 0 of <code class="language-plaintext highlighter-rouge">RE2::Set::Match()</code> does not output any error information, instead returning <code class="language-plaintext highlighter-rouge">false</code> if a match fails for any reason.</li>
<li>Later versions of re2 <a href="https://github.com/google/re2/commit/ee52f030e7fed3fb0cfc5f41c367a898194cf776">do output error information if requested</a>.</li>
</ul>
<p>The main blocker with the addition of this feature to my gem was how to handle this difference gracefully.</p>
<p>Things I considered:</p>
<ul>
<li>Could I backfill the behaviour of later re2 versions to version 0 so clients couldn’t tell the difference?
<ul>
<li>No, version 0 doesn’t give me enough information to fully recreate the behaviour of later versions.</li>
</ul>
</li>
<li>Should <a href="https://mudge.name/re2/RE2/Set.html#match-instance_method"><code class="language-plaintext highlighter-rouge">RE2::Set#match</code></a> behave differently depending on your ABI version without warning, leaving the responsibility of knowing the expected behaviour to the user?
<ul>
<li>I didn’t like the idea that upgrading the ABI version of the library would suddenly cause existing code to behave differently without warning.</li>
</ul>
</li>
<li>Should we expose a method that tells users which behaviour they will see?
<ul>
<li>I added a <a href="https://mudge.name/re2/RE2/Set.html#match_raises_errors%3F-class_method"><code class="language-plaintext highlighter-rouge">RE2::Set.match_raises_errors?</code></a> method that will return true if the ABI version supports it as I need to rely on it <a href="https://github.com/mudge/re2/blob/ac29a2231a8740676136d96fc353fe04971403f1/spec/re2/set_spec.rb#L110">in my tests</a>.</li>
</ul>
</li>
<li>Should we have two ways of calling <code class="language-plaintext highlighter-rouge">match</code>: one that does not raise exceptions (available to all ABI versions) and one that does (available only to later versions)? If someone tries to call the latter API on an ABI version that doesn’t support it, raise an exception to explain it is unsupported.
<ul>
<li><a href="https://github.com/mudge/re2/commit/a2209cc02d12ae93793645a0cc7c61819f5ef4a3">This is what I ended up implementing</a>.</li>
</ul>
</li>
</ul>
<p>The final API works like so:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># On ABI version 0</span>
<span class="n">set</span> <span class="o">=</span> <span class="no">RE2</span><span class="o">::</span><span class="no">Set</span><span class="p">.</span><span class="nf">new</span>
<span class="n">set</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="s2">"foobar"</span><span class="p">)</span>
<span class="c1"># RE2::Set::UnsupportedError: current version of RE2::Set::Match() does not</span>
<span class="c1"># output error information, :exception option can only be set to false</span>
<span class="n">set</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="s2">"foobar"</span><span class="p">,</span> <span class="ss">exception: </span><span class="kp">false</span><span class="p">)</span>
<span class="c1"># => false</span>
<span class="c1"># On later ABI versions</span>
<span class="n">set</span> <span class="o">=</span> <span class="no">RE2</span><span class="o">::</span><span class="no">Set</span><span class="p">.</span><span class="nf">new</span>
<span class="n">set</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="s2">"foobar"</span><span class="p">)</span>
<span class="c1"># RE2::Set::MatchError: #match must not be called before #compile</span>
<span class="n">set</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="s2">"foobar"</span><span class="p">,</span> <span class="ss">exception: </span><span class="kp">false</span><span class="p">)</span>
<span class="c1"># => false</span>
</code></pre></div> </div>
</li>
<li>
<p><a href="/2022/10/12/weeknotes-96/">Last week</a>, I said I am trying two things to prevent migraines. I lied as I’m also trying some other things:</p>
<ol>
<li>I’ve reduced the amount of coffee I drink in the morning (15g instead of 18g).</li>
<li>I went for an eye test for the first time since January 2020 but my prescription hasn’t changed.</li>
</ol>
<p>(So far, one or more of these things seem to be working.)</p>
</li>
</ul>
Weeknotes #96https://mudge.name/2022/10/12/weeknotes-96/2022-10-12T14:04:45+00:002022-10-12T14:04:45+00:00Fewer pills, more ducks, being wrong about database transactions, scratching someone else’s car and carpet fitting.<ul>
<li>
<p>I stopped <a href="/2022/08/22/weeknotes-95/">taking Amitriptyline</a> after a run of headaches and migraines and have instead decided to try two things:</p>
<ol>
<li>Going for a lunchtime run around the local tarn twice a week, greeting the ducks as I complete two laps.</li>
<li>Using the <a href="https://en.wikipedia.org/wiki/Pomodoro_Technique">Pomodoro Technique</a> to force me to take breaks throughout the workday.</li>
</ol>
</li>
<li>
<p>I struggled to find a simple macOS application to help with the Pomodoro Technique but settled on <a href="https://flowapp.info">Flow</a> as it gives me a timer for four 25 minute sessions separated by 5 minute breaks followed by a longer 30 minute break.</p>
</li>
<li>
<p>Yesterday, I realised my understanding of database transactions was wrong.</p>
<p>I thought it was safe to do the following in <a href="https://rubyonrails.org">Rails</a>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Key</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
<span class="n">key</span> <span class="o">=</span> <span class="no">Key</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="nb">name</span><span class="p">:).</span><span class="nf">first_or_initialize</span>
<span class="n">key</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">age: </span><span class="mi">42</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>However, a concurrent transaction could write another <code class="language-plaintext highlighter-rouge">Key</code> with the same name between the <a href="http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-where"><code class="language-plaintext highlighter-rouge">where</code></a> and the <a href="http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update-21"><code class="language-plaintext highlighter-rouge">update!</code></a>. This would cause a conflict if <code class="language-plaintext highlighter-rouge">name</code> is unique.</p>
<p>Using <a href="http://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert">upsert</a> (which uses <a href="https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT">PostgreSQL’s <code class="language-plaintext highlighter-rouge">INSERT ... ON CONFLICT</code></a>) would help but you still can’t rely on any state you read in that first query.</p>
<p>If you’re only ever expecting <code class="language-plaintext highlighter-rouge">UPDATE</code>s to cause conflicts (and never <code class="language-plaintext highlighter-rouge">INSERT</code>s), you can use <a href="https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE"><code class="language-plaintext highlighter-rouge">SELECT ... FOR UPDATE</code></a> via <a href="http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-lock"><code class="language-plaintext highlighter-rouge">lock</code></a>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Key</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
<span class="n">key</span> <span class="o">=</span> <span class="no">Key</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="nb">name</span><span class="p">:).</span><span class="nf">lock</span><span class="p">.</span><span class="nf">first_or_initialize</span>
<span class="n">key</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">age: </span><span class="mi">42</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>If you might have conflicts from <code class="language-plaintext highlighter-rouge">INSERT</code>s too, you can go for the strictest <a href="https://www.postgresql.org/docs/current/transaction-iso.html">transaction isolation level</a> and keep retrying your transaction in case there’s a conflict:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">begin</span>
<span class="no">Key</span><span class="p">.</span><span class="nf">transaction</span><span class="p">(</span><span class="ss">isolation: :serializable</span><span class="p">)</span> <span class="k">do</span>
<span class="n">key</span> <span class="o">=</span> <span class="no">Key</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="nb">name</span><span class="p">:).</span><span class="nf">first_or_initialize</span>
<span class="n">key</span><span class="p">.</span><span class="nf">update!</span><span class="p">(</span><span class="ss">age: </span><span class="mi">42</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">rescue</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">SerializationFailure</span>
<span class="k">retry</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>After watching <a href="https://youtu.be/0nbkaYsR94c">Joel Spolsky’s “You Suck At Excel”</a>, I replaced my overcomplicated <a href="/2020/05/31/weeknotes-31/"><code class="language-plaintext highlighter-rouge">budget.rb</code></a> with a spreadsheet that is both easier to read and produces numbers in which I have much more confidence.</p>
</li>
<li>
<p>We borrowed my dad’s car to drive the family to Cornwall and suffered the narrow roads and surprisingly spiky roadside foliage. Before we returned, I ordered an <a href="https://www.halfords.com/motoring/car-cleaning/all-car-cleaning/autoglym-complete-scratch-removal-kit-349894.html">Autoglym Scratch Removal Kit</a> as <a href="https://www.which.co.uk/reviews/new-and-used-cars/article/best-car-scratch-removers-aGrzV1F66I0H">recommended by Which?</a> and it worked on both our recent scratches and ones I’m now sure were the work of my dad over the past eight years.</p>
</li>
<li>
<p>We had carpet fitted in C——’s bedroom though the fitter asked us to take up the floor and remove any large furniture beforehand. This led to us dismantling two <a href="https://www.ikea.com/gb/en/cat/pax-system-19086/">IKEA PAX wardrobes</a> and <a href="https://www.ikea.com/gb/en/p/gonatt-cot-with-drawer-white-90467089/">a cot</a> before ripping up the wood floor. There was something cathartic about the totally empty room, stripped back to its stone underfloor.</p>
<p>The carpets were fitted in a matter of hours but reconstructing the wardrobes in place (given the low ceiling) required three adults. Planing the doors that no longer fit was a perilous experience.</p>
</li>
<li>
<p>While nailing the flimsy back to the wardrobe, I unthinkingly cautioned one of the people helping me by saying “watch your fingies!”</p>
</li>
</ul>
Weeknotes #95https://mudge.name/2022/08/22/weeknotes-95/2022-08-22T14:44:32+00:002022-08-22T14:44:32+00:00Weeding, filing, finishing video games, a spa town called home, parenting hell and a butt full of rain.<ul>
<li>
<p>Armed with a wire brush on a broom handle and “<a href="https://radiohead.com/library#okc/ok-computer">OK Computer</a>”, I attempted to weed our driveway using good, old-fashioned elbow grease but soon resorted to spraying it indiscriminately with <a href="https://en.wikipedia.org/wiki/Pelargonic_acid">pelargonic acid</a> weed killer.</p>
<p>In the direct sunlight, this killed off the various weeds within an hour.</p>
<p>For several weeks, I avoided thinking about how to remove the dried, shrivelled remains, leaving the sack of <a href="https://gbr.sika.com/dms/getdocument.get/5942bdca-b955-4a9f-8548-2c1a8682c165/sika_setting_sand.pdf">setting sand</a> in our garage unopened. The weeds have now regrown.</p>
</li>
<li>
<p>When we moved into our house over a year ago, I brought with me two plastic bags full of unsorted papers. These contained a variety of documents to be sorted and letters to be binned but I’d put the job off for several years, letting the bag sit on the floor of my office. In a quiet moment <del>of madness</del>, I decided to tackle this filing backlog once and for all.</p>
<p>It took several days to finish, sorting papers into loose categories and filling the recycling bin. In the end, you can’t really tell the difference.</p>
</li>
<li>
<p>After enjoying <a href="https://tomstu.art/weeknotes-133-delicious-dust">Tom’s reaction to “Stray”</a>, I was pleasantly surprised to discover it wasn’t a PS5-exclusive but is also available for the PS4. I bought it from the <a href="https://www.playstation.com/en-gb/games/stray/">PlayStation store</a> and made short work of it in a few days.</p>
<p>Missing having something to play, I picked <a href="/2022/01/06/weeknotes-84/">“Chicory” for the Nintendo Switch</a> back up and enjoyed finishing that too.</p>
</li>
<li>
<p>We rented “<a href="https://a24films.com/films/everything-everywhere-all-at-once">Everything Everywhere All At Once</a>” and, like many others, loved it. I also recommend <a href="https://popculturedetective.agency/2022/everyone-everywhere-needs-waymond-wang">The Pop Culture Detective Agency’s “Everyone Everywhere Needs Waymond”</a>, an analysis of the film and its character of Waymond played by <a href="https://en.wikipedia.org/wiki/Ke_Huy_Quan">Ke Huy Quan</a>.</p>
</li>
<li>
<p>For the first time in years, I went into Leeds city centre with E—— in order to browse sofas at John Lewis.</p>
<p>We had lunch at <a href="https://www.hellohouseoffu.com">House of Fu</a> with a five week old A——. Finishing our meal with a vegan ice cream sandwich reminded me of a trip to <a href="https://www.thespreadeaglelondon.co.uk">The Spread Eagle</a> with <a href="https://tomstu.art">Tom</a>, Leo and <a href="https://tuzz.tech">Chris</a> years ago.</p>
<p>Aside from <a href="/2022/03/28/weeknotes-91/">disastrous attempted trips to London</a>, I hadn’t been in a real <em>city</em> since we moved up north. I relished asking a barista about a roaster’s coffee beans in a <a href="https://www.ifupnorth.co.uk/vicar-lane/if-vicar-lane-home">tiny café</a> but it felt equally comforting to return to the “spa and festival town” we call home.</p>
</li>
<li>
<p>When A—— was two weeks old, I bumped into another parent from C——’s nursery. He asked how things were going and I said things were going relatively well, that the second child seemed easier than the first perhaps because you have much better perspective the second time around. His response?</p>
<blockquote>
<p>Yeah, I remember feeling like that for the first two weeks. But it’s all downhill from there.</p>
</blockquote>
<p>I’ve certainly been guilty of this myself but it really does feel like parents can’t help but engage in a sort of misery competition with one another.</p>
<blockquote>
<p>Two years old? Oh, I remember those good times, just you wait until they’re three!</p>
</blockquote>
</li>
<li>
<p>For relief, I’ve listened to 40 episodes of “<a href="https://www.spreaker.com/show/rob-beckett-and-josh-widdicombes-lockdow">Rob Beckett and Josh Widdicombe’s Parenting Hell</a>”. Among the many topics discussed, the snowballing complexity of bedtime routines was eerily familiar.</p>
<p>C——’s nightly routine involves taking “no more than three” (typically five to seven) toy vehicles with him up the stairs to his bedroom. Once there, each vehicle is slowly parked against his wardrobe to the following script:</p>
<blockquote>
<p>I’m ever so tired, I had so much fun <em>insert activity from the day here</em>, now I need a big rest. Parked.</p>
</blockquote>
<p>It’s a rare chance to hear about his time at nursery or learn what sticks in his mind from our days together.</p>
<p>He started to mix up the final part of this ritual by replacing “big rest” with “big… breakfast”, “big… Stroganoff” or—one he was particularly proud of—”big… poo to eat”.</p>
</li>
<li>
<p>After <a href="/2022/06/13/weeknotes-94/">several migraines in a single week</a>, I’ve started taking <a href="https://en.wikipedia.org/wiki/Amitriptyline">Amitriptyline</a> every night before I go to bed.</p>
<p>Having two young children means my day is increasingly made up of routine (waking with the oldest, getting them both dressed, emptying the dishwasher, making coffee, making breakfast, putting them down for naps, etc.) It’s strange to take medication daily but it seems to have reduced the frequency of migraines without clearing them up altogether.</p>
</li>
<li>
<p>I didn’t <a href="/2022/06/13/weeknotes-94/">ruin my lawn by feeding it</a> and am now the proud owner of a <a href="https://www.diy.com/departments/ward-210l-water-butt/405866_BQ.prd">210 litre water butt</a> (installed with a little extra <a href="https://en.wikipedia.org/wiki/Thread_seal_tape">PTFE tape</a>). A little thrill runs through me every time it rains knowing I’m hoarding <a href="https://youtu.be/rNdrgjjsHsM">water from the sky</a> like some sort of Yorkshire <a href="https://en.wikipedia.org/wiki/Immortan_Joe">Immortan Joe</a>.</p>
</li>
<li>
<p>After a summer morning paddling in the River Wharfe, we stopped at a nearby deli for ice cream in their garden. E—— and C—— went to pay at the till as I wrangled A—— into her pram. I perused their selection of coffee beans before noticing C—— was missing. “Isn’t he with you?”</p>
<p>I left the deli and couldn’t see C—— anywhere until I spotted a stranger walking hand-in-hand with him back from the busy junction at the top of the road. “He was going to cross the road!” she cried as we were reunited.</p>
<p>C—— later said he “didn’t know” he shouldn’t walk home by himself.</p>
</li>
<li>
<p>Years ago, I bought a set of tools for taking the dashboard of my car apart. I used them once to fit a Bluetooth adapter to the car radio but they have since sat wrapped in their original bubble wrap, unused.</p>
<p>This morning I wondered if they would be any good for lifting the wooden floor in the spare bedroom. It took a while to find them but when I did, there it was: a handheld pry bar.</p>
</li>
</ul>
Weeknotes #94https://mudge.name/2022/06/13/weeknotes-94/2022-06-13T13:57:09+00:002022-06-13T13:57:09+00:00Birthdays.<ul>
<li>
<p>I am now a father of two.</p>
</li>
<li>
<p>Instead of <a href="https://www.nhs.uk/conditions/baby/caring-for-a-newborn/helping-your-baby-to-sleep/">sleeping when the baby sleeps</a>, I have been busying myself with inessential jobs:</p>
<ul>
<li>
<p>Following a tip from a paint sprayer, I covered up the most obvious scratches on our 15 year old Volkswagen Golf with a permanent marker.</p>
</li>
<li>
<p>I fixed the leaking shower in C——’s bathroom by <a href="https://www.showerdoc.com/blog/the-shower-doctor-surgery-how-to-replace-an-aqualisa-cartridge">replacing its thermostatic cartridge</a>. After hacking through an entire tube’s worth of caulk to remove the shower controls from the wall, I cut myself while trying to remove a rusted screw. Replacement cartridge installed, we no longer discover phantom shower spray on the bathroom floor at random times of the day.</p>
</li>
<li>
<p>Having learned how our shower controls are meant to be put together, I took the other shower apart to check it.</p>
</li>
<li>
<p>As shower repairs require cutting off the water, I discovered our stopcock wasn’t actually fully stopping our water supply. I’d made do during the shower repair by running the tap on the closest sink. I suspected I’d overtightened a leaking gland nut on the stopcock which was preventing its tap from fully closing. I <a href="https://www.yourrepair.co.uk/blog/how-to-fix-a-leaking-stopcock">loosened the gland nut and used PTFE tape as new packing</a> so I didn’t have to tighten it as much.</p>
</li>
<li>
<p>The door in our “utility” (a little, windowless room off our kitchen where I do most of my baking) always swung shut, requiring a doorstop to keep it open. Fed up with this, I <a href="https://youtu.be/hdtkdg0GBms">took the top hinge pin out and bent it slightly</a> so it no longer closes by itself.</p>
</li>
<li>
<p>We’ve been in our house a year now and this is first time I’ve ever been responsible for a garden lawn. It has not been doing well with more white clover and creeping buttercup than grass.</p>
<p>I decided to treat it with some “feed and weed” which has led me to borrow my parents’ scarifier and take one step further along the path of completely ruining my garden.</p>
</li>
<li>
<p>When E—— reported that our bathroom toilet was running, I attempted to take it apart. This triggered my second migraine in two days so it was time for a break.</p>
</li>
</ul>
</li>
<li>
<p>In an attempt to combat my self-destructive love of “jobs” during downtime and instead cultivate the ability to simply do nothing, I resubscribed to <a href="https://www.headspace.com">Headspace</a> for a year.</p>
</li>
<li>
<p>C—— keeps me busy with a steady supply of broken toys that need repairing. <a href="https://www.bbc.co.uk/programmes/b08l581p">The Repair Shop</a> has taught me that it is not enough to apply glue to things: they must also be held together while setting. When a wheel broke off a toy Jeep, I made use of this revelation and used rubber bands to hold it in place overnight.</p>
<p class="center"><img src="/i/wheel-repair.jpg" alt="" width="375" height="500" /></p>
</li>
<li>
<p>A—— was born a few days after my birthday. My late grandfather and I shared our birthday and the essential turmoil of the occasion made me reminisce about him.</p>
<p>When I was a boy, he’d let me change the gears of his car as he drove, calling out “into second”, “into third” and so on. He would never have his seatbelt fastened before pulling away, instead reserving that for the short drive to the nearest junction, occasionally joined by a quick shave from the electric razor he kept in the ashtray.</p>
<p>Known to enjoy an afternoon nap in his reclining chair, my sister and I once applied make-up to his sleeping face. I vividly recall using a felt-tip pen to draw an expression of surprise, confident that his continued unconsciousness implied consent.</p>
<p>I can’t recall his reaction upon waking but I do remember my grandmother doubled over with laughter in the kitchen.</p>
</li>
</ul>
Weeknotes #93https://mudge.name/2022/05/14/weeknotes-93/2022-05-14T13:21:54+00:002022-05-14T13:21:54+00:00Soldering remote control cars, emotional furniture, noodling with Tailscale, finishing The Last of Us Part II and the relationship between pasta and running.<ul>
<li>
<p>My sister bought C a surprisingly fast remote control car for Christmas which stopped working. Replacing the batteries made no difference so I took it apart and discovered a disconnected wire.</p>
<p class="center"><img src="/i/broken-car.jpg" alt="" width="375" height="500" /></p>
<p>All of the repairs I’ve done on his toys so far have involved little more than <a href="https://www.go-araldite.com">Araldite</a> and <a href="/2022/01/27/weeknotes-87/">tweezers</a> and it had been a while since <a href="/2020/12/21/weeknotes-59-and-60/">my last successful bit of soldering</a>. With little to lose and <a href="https://www.youtube.com/watch?v=vIT4ra6Mo0s">Maciek’s recommendation for soldering lessons on YouTube</a>, I managed to solder the loose wire back to its pin.</p>
<p>C is free to once again repeatedly crash the car loudly into the nearest wall.</p>
</li>
<li>
<p>While he looked for things to turn with an Allen key, we took the bars off one side of C’s cot, officially transforming it into a bed.</p>
<p>While we had been working up to this event for a week, reminding him daily that the change was coming, my own feelings about this seemingly insignificant bit of furniture modification took me by surprise.</p>
<p>He declared that he isn’t quite ready for a duvet and continues to sleep in a sleeping bag instead. I can’t blame him as it is even more convenient than the <a href="https://www.scandinaviastandard.com/two-duvets-in-scandinavia/">Scandinavian two duvet system</a>.</p>
</li>
<li>
<p>I’m taking some time off work but have been characteristically struggling to unwind. I spent a few days clearing things off our shared to-do list, arranging a boiler service, putting up curtains and a mirror and indulging in some pointless “noodling” on my computer.</p>
</li>
<li>
<p>After archiving yet another daily DMARC report without reading it, I read <a href="https://support.google.com/a/answer/10032472?hl=en">Google’s recommendation that you use a 3rd-party service to process them</a>. I signed up for <a href="https://dmarc.postmarkapp.com">Postmark’s free DMARC monitoring</a> and am glad for the quieter inbox.</p>
</li>
<li>
<p>After reading about <a href="https://tailscale.com/blog/database-for-2022/">Tailscale’s use of Litestream</a> and being recommended <a href="https://tailscale.com">their service</a> by several people, I decided to give it a trial.</p>
<p>I <a href="https://tailscale.com/download/">installed the client</a> on my laptop and my <a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">Raspberry Pi 4</a> and dug out an old USB hard drive in an enclosure to <a href="/2019/11/12/using-a-raspberry-pi-for-time-machine/">use with Time Machine</a>. Rather than setting up Time Machine over my local network, I instead used <a href="https://tailscale.com/kb/1081/magicdns/">Tailscale’s MagicDNS</a> to connect to the Samba server on my Pi at <code class="language-plaintext highlighter-rouge">raspberrypi.mudge.github.beta.tailscale.net</code>. This way, I could back up to my USB hard drive anywhere I had internet access.</p>
<p>Sadly, the initial backup was incredibly slow (probably due to the age of the drive and its enclosure) so I abandoned this though Tailscale worked without issue.</p>
</li>
<li>
<p>I finally finished <a href="https://www.playstation.com/en-gb/games/the-last-of-us-part-ii/">The Last of Us Part II</a> which I thoroughly enjoyed (even if it wasn’t exactly uplifting).</p>
<details><summary>Spoilers follow.</summary><p>At the halfway point of the game (when Abby catches up to Ellie in the theatre), I thought I knew how it was going to end: that, through Abby's eyes, we would slowly learn that Ellie had been the bad guy all along. That her mindless quest for revenge at any cost would make her irredeemable in our eyes. I hoped the game would try to change our mind about someone we had grown to love over the course of Part I so successfully that when the game returned us to that scene in the theatre in Abby's shoes, we would happily pull the trigger.</p><p>It didn't go that way but I still welcomed the chance to scrutinise Ellie's (appalling) behaviour in a type of game where that rarely happens.</p></details>
</li>
<li>
<p>With that done, <a href="http://willhigo.com">Will</a> gave me <a href="https://www.playstation.com/en-gb/games/control/">Control: Ultimate Edition</a> as an early birthday gift. The Last of Us Part II is a hard act to follow but I’m intrigued by its world which reminds me of <a href="https://www.imdb.com/title/tt0830361/?ref_=nm_flmg_act_12">The Lost Room</a>.</p>
</li>
<li>
<p>I’ve taken to playing <a href="https://framed.wtf">Framed</a> daily with two friends, both of whom consider themselves cinephiles. I’m definitely not a film aficionado (though I visited our <a href="https://www.ilkleycinema.co.uk">local cinema for the first time</a>) but have only been truly stumped once out of 19 films so far.</p>
</li>
<li>
<p>I <a href="https://shop.play.date">pre-ordered a Playdate</a>.</p>
</li>
<li>
<p>After a long break due to illness and laziness, I managed to go for a run along the river. This helped both with my continuing project to relax and the discovery of a delightful local Italian restaurant that serves fantastic pasta dishes for lunch.</p>
</li>
</ul>
Weeknotes #92https://mudge.name/2022/04/03/weeknotes-92/2022-04-03T11:43:00+00:002022-04-03T11:43:00+00:00Testing negative, a leaky gutter, failing to block YouTube videos, compressing assets with Propshaft and recording video game sounds with OBS.<ul>
<li>
<p>One week after <a href="/2022/03/28/weeknotes-91/">testing positive for COVID-19</a>, C and I have now tested negative for at least two days. I occasionally have a coughing fit but have otherwise recovered.</p>
</li>
<li>
<p>In the middle of the night, we suddenly heard a rapid pitter-patter coming from outside our bedroom window. Venturing out, we discovered the gutter directly above was rapidly dripping rainwater onto the sill.</p>
<p>I’ve attempted to plug the leak with silicone but <a href="https://youtu.be/qpCNrxaV8A0">various</a> <a href="https://youtu.be/kvamTRBhqLA">YouTube</a> videos indicate that the real fix is to replace the faulty gutter union. However, given that investigating the guttering required E to hold onto my belt as I balanced on a yoga chair on a third storey balcony, it might be time to call out a professional.</p>
</li>
<li>
<p>C continues to enjoy a daily dose of vehicle videos on YouTube, some of which <a href="https://medium.com/@jamesbridle/something-is-wrong-on-the-internet-c39c471271d2">I don’t want him to see</a>. However, I can’t find out how to block a video or channel from search with a regular YouTube account. Instead, this appears to be a feature exclusive to <a href="https://www.youtubekids.com">YouTube Kids</a>, a separate app for accessing YouTube with extra parental controls.</p>
<p>As I attempted to do all of this on a <a href="https://store.google.com/gb/product/chromecast_google_tv?pli=1&hl=en-GB">Chromecast with Google TV</a>, I first had to log out of my Google Workspace account (which silently lacks the ability to add child accounts), sign in with a “regular” Gmail account, set up a child account through something called <a href="https://families.google.com/familylink/">Google Family Link</a> and finally discover that <a href="https://support.google.com/youtubekids/answer/9618031?hl=en-GB">you can’t sign in to YouTube Kids on a Chromecast with Google TV</a> so you can’t block any content.</p>
</li>
<li>
<p>I’ve been making steady progress through <a href="https://www.playstation.com/en-gb/games/the-last-of-us-part-ii/">The Last of Us Part II</a>. It really is <em>very</em> good.</p>
</li>
<li>
<p>I was caught out by <a href="https://github.com/rails/propshaft/issues/86">the lack of GZip and Brotli compression</a> when switching from <a href="https://github.com/rails/sprockets">Sprockets</a> to <a href="https://github.com/rails/propshaft/">Propshaft</a>: Sprockets will generate compressed versions of your assets (e.g. <code class="language-plaintext highlighter-rouge">application-decafbad.js.gz</code> and <code class="language-plaintext highlighter-rouge">application-decafbad.js.br</code>) which will automatically be served by <a href="https://github.com/rails/rails/blob/926b803297c4bc529ee46296a7cd5cc02bb21100/actionpack/lib/action_dispatch/middleware/static.rb"><code class="language-plaintext highlighter-rouge">ActionDispatch::Static</code></a>. However, Propshaft will <em>not</em> do this so your application will serve uncompressed versions by default if you’re not compressing them with a CDN or other web server.</p>
<p>Thankfully, <a href="https://github.com/rack/rack/blob/9a018c0c8fea537a8f8744000ae1dcb3e93cacb9/lib/rack/deflater.rb">Rack includes a <code class="language-plaintext highlighter-rouge">Rack::Deflater</code> middleware</a> which will handle GZip compression and <a href="https://github.com/marcotc/rack-brotli"><code class="language-plaintext highlighter-rouge">Rack::Brotli</code> is available as a gem</a>.</p>
<p>You can enable both by adding the following to your Rails application’s <code class="language-plaintext highlighter-rouge">config/application.rb</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">middleware</span><span class="p">.</span><span class="nf">insert_before</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Static</span><span class="p">,</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Deflater</span>
<span class="n">config</span><span class="p">.</span><span class="nf">middleware</span><span class="p">.</span><span class="nf">insert_before</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Static</span><span class="p">,</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Brotli</span><span class="o">::</span><span class="no">Deflater</span>
</code></pre></div> </div>
</li>
<li>
<p>Continuing the inconsequential quest to tidy my desk that started by <a href="/2022/03/13/weeknotes-90/">ordering a monitor arm</a>, I installed a <a href="https://www.fully.com/en-gb/accessories/jarvis-accessories/fully-cable-management-tray.html">Fully cable management tray and solution kit</a> to partially tame the tangle of wires and power strips dangling from the underside.</p>
</li>
<li>
<p>I started playing a new build of <a href="https://tuzz.tech">Chris Patuzzo</a>’s “Worship the Sun” and <a href="/2021/04/11/weeknotes-76/">once again</a> recorded myself with <a href="https://obsproject.com">OBS</a>. The latest build now has sound but I had to jump through some hoops to capture this in my recordings as macOS doesn’t support capturing system audio without third-party software:</p>
<ol>
<li>
<p>Install the <a href="https://github.com/ExistentialAudio/BlackHole">BlackHole</a> virtual audio driver from Homebrew:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>brew <span class="nb">install </span>blackhole-2ch
</code></pre></div> </div>
</li>
<li>
<p>Add a “Multi-Output Device” in the macOS Audio MIDI Setup application, combining both “BlackHole 2ch” and my external headphones, ensuring the headphones are the “master device”.</p>
</li>
<li>
<p>Right-clicking that new “Multi-Output Device” and using it for sound output.</p>
</li>
<li>
<p>Adding an “Audio Input Capture” to OBS and choosing the “BlackHole 2ch” device.</p>
</li>
<li>
<p>Adding a Compressor audio filter to the “Audio Input Capture” with the <a href="https://obsproject.com/kb/compressor-filter">recommended sidechain compression/ducking settings</a> so that the game audio dips in volume as I speak into my microphone.</p>
</li>
</ol>
<p>None of this has improved my puzzle solving ability.</p>
</li>
</ul>
Weeknotes #91https://mudge.name/2022/03/28/weeknotes-91/2022-03-28T10:16:00+00:002022-03-28T10:16:00+00:00Testing positive.<ul>
<li>
<p>C and I tested positive for COVID-19 on Saturday. The previous day, after testing negative in the morning, we’d driven down to London for a friend’s 40th birthday in Leytonstone. We had decided to make a weekend of it, seeing some other friends for a Mother’s Day roast on Sunday.</p>
<p>I only managed to see <a href="https://tomstu.art/weeknotes-116-remember-this">Tom</a> on the Friday night before the faint positive lateral flow test led us to cancel all plans and drive back up north. While we didn’t suffer any unusual delays, the return journey took seven hours.</p>
</li>
<li>
<p>As usual, I am recovering by repairing things with <a href="https://www.go-araldite.com">Araldite</a> and watching <a href="https://www.imdb.com/title/tt9032400/">films of questionable quality</a>.</p>
</li>
<li>
<p>In my <a href="/2022/03/13/weeknotes-90/">last weeknotes</a>, I linked to various bits of the <a href="https://github.com/rails/rails/">Ruby on Rails source code</a>. It is easy to link to a few lines of code only to have those links break as the code changes or files are rearranged. Thankfully, GitHub has <a href="https://docs.github.com/en/repositories/working-with-files/using-files/getting-permanent-links-to-files">a keyboard shortcut for expanding a URL to its “canonical” form</a>, e.g. converting <code class="language-plaintext highlighter-rouge">https://github.com/rails/rails/blob/main/README.md</code> to <code class="language-plaintext highlighter-rouge">https://github.com/rails/rails/blob/8372cf7374a1962ee0abdb9dedf7f690f280acc9/README.md</code>: press the “y” key.</p>
</li>
<li>
<p>I was confused about the differences between <a href="https://relishapp.com/rspec/rspec-rails/v/5-0/docs/request-specs/request-spec">RSpec’s “request specs”</a>, <a href="https://relishapp.com/rspec/rspec-rails/v/5-0/docs/feature-specs/feature-spec">“feature specs”</a>, <a href="https://relishapp.com/rspec/rspec-rails/v/5-0/docs/system-specs/system-spec">“system specs”</a>, <a href="https://guides.rubyonrails.org/testing.html#system-testing">Rails’ “system testing”</a> and <a href="https://guides.rubyonrails.org/testing.html#integration-testing">“integration testing”</a>.</p>
<p>To summarise:</p>
<ul>
<li>RSpec request specs are a wrapper around Rails’ integration tests</li>
<li>RSpec system specs are a wrapper around Rails’ system tests</li>
<li>RSpec feature specs don’t correspond exactly to one of Rails’ own tests but are for acceptance testing with <a href="https://github.com/teamcapybara/capybara">Capybara</a></li>
</ul>
</li>
<li>
<p>After completing the CSS animations module of <a href="https://css-for-js.dev">Josh W Comeau’s “CSS for JavaScript Developers” course</a>, I updated the animation of <a href="https://www.ghostcassette.com">my company logo</a> to “rewind” itself. It’s a silly thing but I had a lot of fun implementing it.</p>
</li>
</ul>
Weeknotes #90https://mudge.name/2022/03/13/weeknotes-90/2022-03-13T09:40:00+00:002022-03-13T09:40:00+00:00MIME negotiation and the Accept header in Rails APIs, detecting client disconnects and the guilty pleasure of action films.<ul>
<li>
<p>I’ve been working on an API written using <a href="https://rubyonrails.org">Ruby on Rails</a> and was attempting to understand how Rails handles content types when sending the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept">HTTP <code class="language-plaintext highlighter-rouge">Accept</code> header</a>.</p>
<p>Given a controller action like so:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">show</span>
<span class="vi">@post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">html</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">json</span> <span class="p">{</span> <span class="n">render</span> <span class="ss">json: </span><span class="vi">@post</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>With a corresponding route:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">only: :show</span>
</code></pre></div> </div>
<p>Fetching <code class="language-plaintext highlighter-rouge">/posts/1</code> in your browser will try to render an HTML response by default. You can specify the request <code class="language-plaintext highlighter-rouge">format</code> by using an explicit file extension in the URL: <code class="language-plaintext highlighter-rouge">/posts/1.html</code> will return the HTML version and <code class="language-plaintext highlighter-rouge">/posts/1.json</code> will return JSON.</p>
<p>But what if you can’t read the <code class="language-plaintext highlighter-rouge">format</code> out of the URL, e.g. if you accept dots in the post ID?</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">only: :show</span><span class="p">,</span> <span class="ss">id: </span><span class="sr">/.+/</span>
</code></pre></div> </div>
<p>Another way to request a content type is to send <code class="language-plaintext highlighter-rouge">Accept: text/html</code> or <code class="language-plaintext highlighter-rouge">Accept: application/json</code> in your request which will return the HTML and JSON versions respectively.</p>
<p>If you want the JSON representation to be the default and for the HTML version to only be returned if someone explicitly requests it using an <code class="language-plaintext highlighter-rouge">Accept: text/html</code> request header, you might try setting a default <code class="language-plaintext highlighter-rouge">format</code> in your route, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span> <span class="ss">:posts</span><span class="p">,</span> <span class="ss">only: :show</span><span class="p">,</span> <span class="ss">id: </span><span class="sr">/.+/</span><span class="p">,</span> <span class="ss">defaults: </span><span class="p">{</span> <span class="ss">format: :json</span> <span class="p">}</span>
</code></pre></div> </div>
<p>This means <code class="language-plaintext highlighter-rouge">/posts/1</code> now returns JSON by default but requesting <code class="language-plaintext highlighter-rouge">/posts/1</code> with <code class="language-plaintext highlighter-rouge">Accept: text/html</code> returns the JSON version even though we asked for HTML.</p>
<p>This is because Rails’ notion of <a href="https://github.com/rails/rails/blob/a807a4f4f95798616a2a85856f77fdfc48da4832/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L81-L85">a request’s <code class="language-plaintext highlighter-rouge">format</code> takes precedence over anything specified in the <code class="language-plaintext highlighter-rouge">Accept</code> header</a> (see also a <a href="https://gist.github.com/higgis/3793544">discussion between James Higgs and Simon Coffey about this issue</a>). By setting a default <code class="language-plaintext highlighter-rouge">format</code> of <code class="language-plaintext highlighter-rouge">:json</code>, this will always be used instead of anything else in the request.</p>
<p>There is another approach: don’t use a default <code class="language-plaintext highlighter-rouge">format</code> but instead rely on a little documented feature of Rails’ <code class="language-plaintext highlighter-rouge">respond_to</code>: <a href="https://github.com/rails/rails/blob/a807a4f4f95798616a2a85856f77fdfc48da4832/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L76">put the default response <em>first</em></a>.</p>
<p>If we change our controller to the following:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">show</span>
<span class="vi">@post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">json</span> <span class="p">{</span> <span class="n">render</span> <span class="ss">json: </span><span class="vi">@post</span> <span class="p">}</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">html</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>When we now request <code class="language-plaintext highlighter-rouge">/posts/1</code> with the default <code class="language-plaintext highlighter-rouge">Accept: */*</code> header (as set by <a href="https://curl.se">curl</a> and <a href="https://httpie.io">httpie</a>), we’ll get JSON by default instead of HTML. Specifying <code class="language-plaintext highlighter-rouge">Accept: text/html</code> will now return the HTML version as expected.</p>
<p>However, what if someone doesn’t specify an <code class="language-plaintext highlighter-rouge">Accept</code> header at all (e.g. an HTTP client on an embedded device)? In that case, Rails will use a default content type of HTML (<a href="https://github.com/rails/rails/blob/a807a4f4f95798616a2a85856f77fdfc48da4832/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L87-L88">unless it is an <code class="language-plaintext highlighter-rouge">XMLHttpRequest</code> in which case it will use JavaScript</a>) which isn’t what we expect.</p>
<p>To work around this, we can use <a href="https://github.com/rails/rails/blob/a807a4f4f95798616a2a85856f77fdfc48da4832/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L116-L130">Rails’ <code class="language-plaintext highlighter-rouge">request.format=</code> API</a> to explicitly set our default <code class="language-plaintext highlighter-rouge">format</code> if no <code class="language-plaintext highlighter-rouge">Accept</code> header is present:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">before_action</span> <span class="ss">:set_default_format_to_json</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="vi">@post</span> <span class="o">=</span> <span class="no">Post</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">json</span> <span class="p">{</span> <span class="n">render</span> <span class="ss">json: </span><span class="vi">@post</span> <span class="p">}</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">html</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">set_default_format_to_json</span>
<span class="n">request</span><span class="p">.</span><span class="nf">format</span> <span class="o">=</span> <span class="ss">:json</span> <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s2">"Accept"</span><span class="p">].</span><span class="nf">blank?</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>Now we’ll serve JSON when someone requests <code class="language-plaintext highlighter-rouge">/posts/1</code> with the default <code class="language-plaintext highlighter-rouge">Accept: */*</code> header, <code class="language-plaintext highlighter-rouge">Accept: application/json</code> <em>and</em> when there is no <code class="language-plaintext highlighter-rouge">Accept</code> header at all.</p>
</li>
<li>
<p>We needed to improve detection of clients disconnecting from a streaming endpoint using <a href="https://github.com/rack/rack/pull/481">Rack’s Hijack API</a>. As <a href="https://stackoverflow.com/a/17665015">this is a difficult problem</a>, I turned to <a href="https://workingwithruby.com/wwtcps/intro">Jesse Storimer’s “Working With TCP Sockets”</a>.</p>
<p>When a client disconnects cleanly, their socket becomes <a href="https://ruby-doc.org/core-3.1.1/IO.html#eof-3F-method"><code class="language-plaintext highlighter-rouge">eof?</code></a> which means it becomes ready to read (e.g. with <a href="https://ruby-doc.org/core-3.1.1/IO.html#select-method"><code class="language-plaintext highlighter-rouge">IO.select</code></a> or <a href="https://ruby-doc.org/stdlib-3.1.1/libdoc/io/rdoc/IO.html#wait_readable-method"><code class="language-plaintext highlighter-rouge">IO#wait_readable</code></a>). Following <a href="https://github.com/rails/rails/blob/c3d4ab00b69daf6044edd3f425ead228a540e68b/actioncable/lib/action_cable/connection/stream_event_loop.rb">Action Cable’s event loop</a>’s lead, we implemented a system using <a href="https://github.com/socketry/nio4r">New I/O for Ruby</a>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Create a single selector.</span>
<span class="n">selector</span> <span class="o">=</span> <span class="no">NIO</span><span class="o">::</span><span class="no">Selector</span><span class="p">.</span><span class="nf">new</span>
<span class="c1"># Register each rack.hijack socket for reading.</span>
<span class="n">monitor</span> <span class="o">=</span> <span class="n">selector</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="n">io</span><span class="p">,</span> <span class="ss">:r</span><span class="p">)</span>
<span class="c1"># Associate a connection helper object with the monitor</span>
<span class="c1"># to handle cleaning up after client disconnect.</span>
<span class="n">monitor</span><span class="p">.</span><span class="nf">value</span> <span class="o">=</span> <span class="n">connection</span>
<span class="c1"># Run an infinite loop in a separate thread, checking</span>
<span class="c1"># for when the rack.hijack socket becomes readable which,</span>
<span class="c1"># in our case, *must* mean the client has disconnected.</span>
<span class="c1">#</span>
<span class="c1"># As we're using the blocking form of select, in reality we</span>
<span class="c1"># use selector.wakeup to periodically interrupt the thread</span>
<span class="c1"># (e.g. when registering and deregistering sockets).</span>
<span class="no">Thread</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="kp">loop</span> <span class="k">do</span>
<span class="n">selector</span><span class="p">.</span><span class="nf">select</span> <span class="k">do</span> <span class="o">|</span><span class="n">monitor</span>
<span class="n">connection</span> <span class="o">=</span> <span class="n">monitor</span><span class="p">.</span><span class="nf">value</span>
<span class="n">connection</span><span class="p">.</span><span class="nf">close</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>As this won’t catch all client disconnects, we also attempt to write a keepalive message to the socket every 3 seconds and, if anything goes wrong, clean things up.</p>
</li>
<li>
<p>The announcement of the <a href="https://www.apple.com/uk/studio-display/">Apple Studio Display</a> and <a href="https://twitter.com/jsnell/status/1501398193812107265">Jason Snell’s declaration of “I am not interested in having a stand on my desk”</a> led me to order <a href="https://www.fully.com/jarvis-monitor-arm.html">Fully’s Jarvis Monitor Arm</a> but no new display (yet).</p>
</li>
<li>
<p>After a three week hiatus due to a combination of illness and hosting guests, I pulled on my running tights and managed to run along the river this morning.</p>
</li>
<li>
<p>When particularly ill, I sat on the sofa and watched several action films, embracing the likelihood they would be terrible guilty pleasures: <a href="https://www.imdb.com/title/tt7126948/">Wonder Woman 1984</a>, <a href="https://www.imdb.com/title/tt7888964/">Nobody</a> and <a href="https://www.imdb.com/title/tt6856242/">The King’s Man</a>. Following that bit of escapism, I watched all of <a href="https://www.imdb.com/title/tt6741278/">Invincible</a> which was genuinely good (if you can stomach incredible violence).</p>
</li>
<li>
<p>I baked bread for the first time in a while.</p>
<p class="center"><img src="/i/2022-03-13-bread.jpg" width="375" height="500" alt="" /></p>
</li>
</ul>
Weeknotes #89https://mudge.name/2022/02/17/weeknotes-89/2022-02-17T11:34:00+00:002022-02-17T11:34:00+00:00Improved lighting, de-duplicating photos in Shortcuts, sourdough variations, rusty Clojure and too many YouTube videos.<ul>
<li>
<p>In an effort to improve my lighting on video calls, I bought an <a href="https://www.elgato.com/en/key-light-air">Elgato Key Light Air</a> and positioned it behind my monitor. The Elgato Control Center software adds an icon to the macOS menu bar, so <a href="https://tomstu.art/weeknotes-110-future-point">like Tom</a>, I wrote a <a href="https://support.apple.com/en-gb/guide/shortcuts/welcome/ios">shortcut</a> to toggle the light by <a href="https://tomstu.art/weeknotes-13-realer-problems-exist#key-light">making HTTP requests to it</a>.</p>
<p class="center"><img src="/i/key-light.jpg" width="375" height="211" alt="" /></p>
<p>I originally intended to run the shortcut from <a href="https://obdev.at/products/launchbar/index.html">LaunchBar</a> but in practice, I’ve been toggling the light from a <a href="https://support.apple.com/en-gb/HT207122">widget</a> on my phone as a sort of <a href="https://www.elgato.com/en/stream-deck">Stream Deck</a>.</p>
</li>
<li>
<p>I revisited the <a href="/2022/01/27/weeknotes-87/#nursery-shortcut">shortcut I wrote to scrape photos from C’s nursery app</a> as it was importing duplicates due to the underlying API only respecting dates and not exact time ranges.</p>
<p>Thankfully, Shortcuts provides a “Generate Hash” action which can generate a MD5, SHA1, SHA256 or SHA512 hash for a given input. I use this to first populate a dictionary with all existing photos’ hashes and then check against this dictionary before importing each new photo.</p>
</li>
<li>
<p>Thanks to tips mostly from <a href="https://twitter.com/cassarani">Leo</a>, I’ve been slowly diverging from <a href="https://tartinebakery.com/stories/country-bread">Tartine’s Country Bread recipe</a>:</p>
<p class="center"><img src="/i/coil-fold-bread.jpg" width="375" height="500" alt="" /></p>
<ul>
<li>I use rye flour rather than a blend of white and wholemeal both when feeding my starter and making the initial leaven.</li>
<li>I use <a href="https://challengerbreadware.com/bread-techniques/coil-fold/">coil folds</a> during bulk fermentation.</li>
<li>I use <a href="https://youtu.be/rBH_NsTeNzE">Simple Sourdough’s preshaping and final shaping technique</a> for batards.</li>
<li>I bake the dough cold from the fridge in a <a href="https://challengerbreadware.com/product/challenger-bread-pan/">Challenger bread pan</a>, following their <a href="https://challengerbreadware.com/cast-iron-pan-care-use/">instructions for baking the perfect loaf</a> with one key difference:</li>
<li>I <a href="https://youtu.be/_sJ0HhqN6UM">turn the oven off for 20 minutes after putting the dough in the oven</a>.</li>
</ul>
</li>
<li>
<p>For the first time in four years, I released a new version of my Clojure library <a href="https://github.com/mudge/riveted">riveted</a>, <a href="https://github.com/mudge/riveted/blob/main/CHANGELOG.md#020---2022-02-15">adding the ability to parse XML from byte arrays directly</a>. I have almost entirely forgotten how to write code in a purely functional style.</p>
</li>
<li>
<p>I took yesterday off work as C caught another nasty cold. I’ve watched more YouTube videos for the query “vehicles for toddlers” than can possibly be healthy.</p>
</li>
</ul>
Weeknotes #88https://mudge.name/2022/02/10/weeknotes-88/2022-02-10T12:21:00+00:002022-02-10T12:21:00+00:00A seemingly impossible puzzle, avoiding lost updates, fridge drains and draughts.<ul>
<li>
<p>Over Christmas, <a href="/2020/02/23/weeknotes-17/#hero">my father-in-law</a> received a wooden puzzle we all failed to solve. The best attempts would always result in a single piece left over.</p>
<p class="center"><img src="/i/puzzle.jpg" width="375" height="375" alt="" /></p>
<p>Determined to solve it, I thought of <a href="https://tuzz.tech/">Chris’</a> programming language <a href="https://sentient-lang.org">Sentient</a> but struggled to represent the puzzle and its pieces in code. Research revealed the puzzle consists of <a href="https://en.m.wikipedia.org/wiki/Polyomino">polyominoes</a> and is an example of the <a href="https://en.m.wikipedia.org/wiki/Exact_cover">exact cover problem</a> which is <a href="https://en.m.wikipedia.org/wiki/NP-completeness">NP-complete</a>.</p>
<p><a href="https://en.m.wikipedia.org/wiki/Knuth%27s_Algorithm_X">Donald Knuth used an algorithm called “Algorithm X”</a> to solve the exact cover problem and I found an <a href="https://cemulate.github.io/polyomino-solver/">online polyomino solver by Chase Meadors</a> which let me quickly input the exact puzzle and generate a valid solution.</p>
<p>My use of machines to solve the problem was controversial but, as <a href="https://tomstu.art">Tom</a> put it:</p>
<blockquote>
<p>It doesn’t really qualify as a puzzle in that sense does it [due to the NP-completeness]? It’s more like a game where you have to guess a number between one and a billion.</p>
</blockquote>
</li>
<li>
<p>I’ve been attempting to design an API that prevents simultaneous updates of a resource from overwriting each other (<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#avoiding_mid-air_collisions">“mid-air collisions”</a>).</p>
<p>In researching the problem and existing solutions, <a href="http://github.com/clowder">Chris</a> found <a href="https://www.w3.org/1999/04/Editing/">“Detecting the Lost Update Problem Using Unreserved Checkout”</a> by Henrik Nielsen and Daniel LaLiberte of the <a href="https://www.w3.org">W3C</a> 23 years ago.</p>
<p>The <a href="https://www.w3.org/1999/04/Editing/#3">protocol interactions</a> show how a combination of the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag"><code class="language-plaintext highlighter-rouge">ETag</code></a>, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match"><code class="language-plaintext highlighter-rouge">If-None-Match</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match"><code class="language-plaintext highlighter-rouge">If-Match</code></a> HTTP headers make it possible to implement <a href="https://en.wikipedia.org/wiki/Optimistic_concurrency_control">optimistic concurrency control</a> à la <a href="https://redis.io/topics/transactions#optimistic-locking-using-check-and-set">Redis’ <code class="language-plaintext highlighter-rouge">WATCH</code></a> and <a href="https://www.consul.io/api-docs/kv#cas">Consul’s KV store’s <code class="language-plaintext highlighter-rouge">cas</code> parameter</a>.</p>
</li>
<li>
<p>I continue to listen to the songs from <a href="https://www.netflix.com/gb/title/81289483">Bo Burnham’s “Inside”</a> on repeat:</p>
<blockquote>
<p><a href="https://youtu.be/k1BneeJTDcU">Here’s a tip for straining pasta, here’s a nine year old who died.</a></p>
</blockquote>
</li>
<li>
<p>After finding the crisper drawer in our fridge full of water, I learnt that <a href="https://youtu.be/JOEODVb4a8o">fridges have drains that get blocked</a>. It also solved the mystery of the strange, flanged plastic wand left in the fridge door by the previous owners.</p>
</li>
<li>
<p>I’m now on my third attempt to fix our draughty front door having bought yet another sort of door seal.</p>
<p>I’m gradually approaching the point where it will have been cheaper to replace the whole door rather than keep on buying the wrong type of weatherproofing.</p>
</li>
</ul>
Weeknotes #87https://mudge.name/2022/01/27/weeknotes-87/2022-01-27T13:53:00+00:002022-01-27T13:53:00+00:00Types of ambulance, expecting the worst, someone else doing a better job, reverse engineering a nursery app and immense satisfaction.<ul>
<li>
<p><a href="/2022/01/16/weeknotes-85/#vehicles">C’s love of vehicles</a> is now focussed on a series of videos with 8-bit graphics, a chiptune soundtrack and vehicles such as Type II Ambulance, Type III Ambulance and Bloodmobile.</p>
</li>
<li>
<p>After spending nearly two decades at my parents’ house without me and some encouragement from my father, I brought my electric guitar and amplifier home. I do not expect to play it but I enjoy its stickers for <a href="https://en.wikipedia.org/wiki/Boards_of_Canada">Boards of Canada</a>, <a href="https://en.wikipedia.org/wiki/Thee_Silver_Mt._Zion_Memorial_Orchestra">A Silver Mt. Zion</a> and a genuine “Danger Electric Shock Risk” warning sign.</p>
</li>
<li>
<p>I started playing “The Last of Us Part II” in <a href="/2022/01/06/weeknotes-84/#five-minute-bursts">short bursts, as predicted</a>. I am already hooked and think it looks fantastic despite running on my old, non-Pro PlayStation 4.</p>
<p>That said, while I marvel at the dappled sunlight shining through trees twisting from ruined highways, I am constantly on edge, expecting the worst.</p>
</li>
<li>
<p>Last Sunday, when we woke C from his afternoon nap to go swimming, he turned over in his cot and said for the first time, “I don’t want to go swimming, I want to go back to sleep.”</p>
</li>
<li>
<p>We have two decorators in this week painting our previously dingy, grey living room a slightly brighter colour. Having recently painted my office and now living with the slivers of olive green I failed to mask properly, it is nice to admire someone else’s much better work.</p>
</li>
<li>
<p>C’s nursery provided us with a login for an iOS app where his keyworkers will occasionally share photos as well as what he has eaten that day, how long he slept, etc. As the app’s UI doesn’t fill my screen and I’m only really interested in the photos, I decided to use <a href="https://mitmproxy.org">mitmproxy</a> to investigate how the app works.</p>
<p>From spying on the network requests made when I login and load the day’s photos of C, I could see that the app talks to a JSON API hosted on <a href="https://azure.microsoft.com/en-gb/">Microsoft Azure</a>. Logging in sends the username and password and returns a temporary access token that can then be used to request photos for a given time range.</p>
<p>At first, I wrote a scrappy Ruby script to download the images to disk but then wondered if I could import them directly into <a href="https://www.apple.com/uk/macos/photos/">Photos for macOS</a>. I wondered if I could <a href="/2019/11/13/scripting-photos-for-macos-with-javascript/">script Photos with JavaScript</a> but making HTTP requests seemed overly complicated as neither <a href="http://developer.mozilla.org/en-US/docs/Web/API/Fetch"><code class="language-plaintext highlighter-rouge">fetch</code></a> nor <a href="http://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest"><code class="language-plaintext highlighter-rouge">XMLHttpRequest</code></a> are available.</p>
<p id="nursery-shortcut"><a href="https://tomstu.art">Tom</a> suggested I look at <a href="https://support.apple.com/en-gb/guide/shortcuts/welcome/ios">Shortcuts</a> instead and linked me to an article explaining how to <a href="https://support.apple.com/en-gb/guide/shortcuts-mac/apd58d46713f/mac">request your first API in Shortcuts</a>. By combining that with a tutorial on <a href="https://www.reddit.com/r/shortcuts/comments/ajdq6d/data_storage_part_1_storing_simple_values/">storing simple values from the Shortcuts subreddit</a>, I was able to fetch the latest photos from the API and directly import them into Photos. By saving the last fetched date in a JSON configuration file on iCloud Drive, I can repeatedly run the Shortcut without worrying about duplicate photos.</p>
<p>What’s more, this same Shortcut works on both my Mac and my phone.</p>
</li>
<li>
<p>The Shortcuts for macOS editor is quite unstable: it would often crash when trying to populate the autocomplete for a given field but the iOS version seems much better. I suspect the ideal device for authoring Shortcuts is an iPad but I painstakingly put this together on my phone’s narrow screen instead.</p>
</li>
<li>
<p>C won a small, plastic pull back car from a game of <a href="https://en.wikipedia.org/wiki/Pass_the_parcel">Pass the Parcel</a> at nursery last year. It stopped working one night this week, making a clicking sound when you attempted to wind it up.</p>
<p>I took it apart, revealing a series of small cogs (one of which fell out before I could see where it was supposed to be) and a coiled spring. Inspired by <a href="http://www.clockworkshop.co.uk">Steve Fletcher</a>, I got a pair of tweezers from our bathroom cabinet and used a <a href="https://youtu.be/QdvfiVebb_s">video showing how these mechanisms work</a> to fix it.</p>
<p>The sense of satisfaction was immeasurable.</p>
</li>
</ul>
Weeknotes #86https://mudge.name/2022/01/20/weeknotes-86/2022-01-20T14:29:00+00:002022-01-20T14:29:00+00:00Archiving recipes, making family tech support less frustrating, the kindness of neighbours and migrating away from Sprockets and Webpacker.<ul>
<li>
<p>After it was recommended on several podcasts, I downloaded <a href="https://mela.recipes">Mela</a> so I could save <a href="https://www.theguardian.com/profile/felicity-cloake+food/series/how-to-cook-the-perfect----">all of Felicity Cloake’s “How to cook the perfect…” recipes</a>. However, its automatic recipe extraction didn’t seem to work on The Guardian’s articles despite <a href="https://tomstu.art">Tom</a> trying it out for me before buying. I realised it was due to The Guardian’s use of <a href="https://amp.dev">AMP</a> on mobile and once I switched to the regular version of the site, I was able to successfully archive <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2011/nov/10/how-to-cook-perfect-porridge">how to make perfect porridge</a> for posterity.</p>
</li>
<li>
<p>I’ve made the mistake of doing tech support for Mac-using family members by asking them to point their phone cameras at their screen. This is inevitably frustrating and results in my repeatedly asking “I’m staring at the corner of your screen, can you please move the camera?”</p>
<p>I can highly recommend switching to <a href="https://support.apple.com/en-gb/guide/messages/icht11883/mac">Messages’ little known screen sharing feature</a> instead.</p>
</li>
<li>
<p>After relying on the kindness of neighbours for years, I finally caved and bought the <a href="https://www.hp.com/gb-en/shop/product.aspx?id=W2G51A&opt=B19&sel=PRN">cheapest HP laser printer I could find</a>. Setting it up on our network required using <a href="https://www.hpsmart.com/gb/en">an iOS app</a>. Given there are apps I bought in the early days of the App Store that no longer run on modern devices, I wonder how long HP will continue to maintain an app for an ageing printer.</p>
</li>
<li>
<p>At work, we finished upgrading all our <a href="https://rubyonrails.org">Ruby on Rails</a> applications to <a href="https://rubyonrails.org/2022/1/6/Rails-7-0-1-has-been-released">7.0.1</a> and <a href="https://www.ruby-lang.org/en/news/2021/12/25/ruby-3-1-0-released/">Ruby 3.1.0</a>. The biggest change was to migrate from <a href="https://github.com/rails/sprockets">Sprockets</a> and <a href="https://github.com/rails/webpacker">Webpacker</a> to <a href="https://github.com/rails/propshaft">Propshaft</a>, <a href="https://github.com/rails/cssbundling-rails">CSS Bundling for Rails</a> and <a href="https://github.com/rails/jsbundling-rails">JavaScript Bundling for Rails</a>.</p>
<p><a href="https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755">DHH has written about Rails 7’s approach to JavaScript</a> and the move from <a href="https://webpack.js.org">webpack</a> to <a href="https://esbuild.github.io">esbuild</a> has resulted in significantly faster build times. However, we were caught out by a few things in testing:</p>
<ol>
<li>esbuild doesn’t support <a href="https://github.com/css-modules/css-modules">CSS Modules</a> out of the box but you can use <a href="https://www.npmjs.com/package/esbuild-css-modules-plugin">a plugin</a> to handle them.</li>
<li>esbuild will bundle the full, development version of <a href="https://reactjs.org">React</a> unless you specify the <a href="https://esbuild.github.io/api/#minify">minify</a> option or <a href="https://github.com/evanw/esbuild/blob/a3d25dd6c2c7c75388c0e4d35dfb487d27c905b4/docs/architecture.md#constant-folding">set <code class="language-plaintext highlighter-rouge">NODE_ENV</code> to <code class="language-plaintext highlighter-rouge">production</code></a>.</li>
<li>esbuild <a href="https://esbuild.github.io/api/#target">targets the latest JavaScript and CSS features</a> by default and doesn’t fully support transforming JavaScript to <a href="https://caniuse.com/?search=es5">ES5</a> (which means dropping support for IE 11). However, we can target our most popular browsers by setting <code class="language-plaintext highlighter-rouge">target</code> to <code class="language-plaintext highlighter-rouge">edge95,chrome92,safari12,firefox78</code>.</li>
</ol>
<p>Similarly, when switching from Sprockets to the <a href="https://sass-lang.com/dart-sass">Dart Sass</a> CLI, we didn’t realise we were no longer benefiting from <a href="https://github.com/ai/autoprefixer-rails">Autoprefixer Rails</a> and so had to combine it with <a href="https://github.com/postcss/postcss">PostCSS</a> in our <code class="language-plaintext highlighter-rouge">build:css</code> script (though we still have to figure out the development setup).</p>
<p>All of this complexity might seem painful but there’s something appealing about these moving pieces being more visible.</p>
</li>
</ul>
Weeknotes #85https://mudge.name/2022/01/16/weeknotes-85/2022-01-16T13:57:00+00:002022-01-16T13:57:00+00:00A stack of books, YouTube weaning, Treat Days and exactly one weather app.<ul>
<li>
<p>I currently have a <a href="https://en.wikipedia.org/wiki/Stack_(abstract_data_type)">stack</a> of unfinished science fiction novels:</p>
<ol>
<li><a href="https://en.wikipedia.org/wiki/Rendezvous_with_Rama">Arthur C. Clarke’s “Rendezvous with Rama”</a></li>
<li><a href="https://en.wikipedia.org/wiki/Mars_trilogy#Blue_Mars_–_Long-term_results">Kim Stanley Robinson’s “Blue Mars”</a></li>
<li><a href="https://en.wikipedia.org/wiki/The_Sparrow_(novel)">Mary Doria Russell’s “The Sparrow”</a></li>
<li><a href="https://en.wikipedia.org/wiki/Death%27s_End">Liu Cixin’s “Death’s End”</a></li>
<li><a href="https://en.wikipedia.org/wiki/Foundation_and_Empire">Isaac Asimov’s “Foundation and Empire”</a></li>
<li><a href="https://www.hachettebookgroup.com/titles/adrian-tchaikovsky/children-of-ruin/9780316452540/">Adrian Tchaikovsky’s “Children of Ruin”</a></li>
<li><a href="https://en.wikipedia.org/wiki/Station_Eleven">Emily St. John Mandel’s “Station Eleven”</a></li>
</ol>
<p>I’ll start a novel with the best of intentions but my interest will wane and I’ll push another book onto the pile. The taller the stack gets, the less likely it is I’ll return to books lower down. It doesn’t help that it has now been years since I stopped reading, say, “Children of Ruin” so I’ll probably have to restart it. Seeing the full stack in one place makes me realise I should think twice before adding anything new.</p>
</li>
<li>
<p id="vehicles">C is a big fan of vehicles (almost entirely land-based, he has no interest in the sea) and we made the mistake of browsing YouTube for "construction vehicles for kids". We're now attempting to wean him off a <a href="https://www.youtube.com/channel/UCVEDZVtA5NUtjxSXHjtvkag">German channel featuring an adult playing with children's toys</a>.</p>
</li>
<li>
<p>E and I watched “<a href="https://www.imdb.com/title/tt5363618/">Sound of Metal</a>” on Prime Video and thoroughly enjoyed it. In retrospect, it was an odd choice for breaking in our new soundbar.</p>
</li>
<li>
<p>Following the gluttony of Christmas and <a href="/2022/01/06/weeknotes-84/#secretly-eating-crisps">inactivity of the past year</a>, I’ve made an effort to cut back on pointless snacking, especially in the evenings. Breaking the habit of a square (or two) of “<a href="https://www.lindt.co.uk/lindt-excellence-dark-mint-intense-bar-100g-">Mint Intense</a>” chocolate once C is in bed is proving difficult.</p>
<p>As a Christmas gift, E and I went for a spa day on Thursday before heading to my parents’ for my dad’s birthday. It is increasingly clear that my diet has a lot in common with the <a href="https://youtu.be/1NjTWvl8x-U">Butterfield Diet Plan</a>.</p>
</li>
<li>
<p><a href="/2022/01/06/weeknotes-84/#tech-podcasts">Listening to a lot of tech podcasts</a> while finishing painting my office led me to delete the many weather apps on my phone. I had hoped to never need one but, after getting drenched while running along the river, I begrudgingly reinstalled <a href="https://www.meetcarrot.com/weather/">Carrot Weather</a> and nothing else.</p>
</li>
<li>
<p>My mum’s American-style fridge freezer broke after 16 years. She ordered a replacement but it wouldn’t fit through the one door necessary to get it into her kitchen. A lesson in <a href="https://www.relay.fm/rd/102">preparing the way</a>.</p>
</li>
<li>Three <a href="/2022/01/02/2021-yearnotes/#running">runs</a> in three weeks. 🏃🏃🏃/🏃🏃🏃</li>
</ul>
Weeknotes #84https://mudge.name/2022/01/06/weeknotes-84/2022-01-06T16:12:00+00:002022-01-06T16:12:00+00:00Correcting mistakes, end-of-year podcast episodes, the best loaves yet, house pride and why you should have some Steri-Strips in your first aid kit.<ul>
<li>
<p>It is a new year and it is time to correct <a href="/2021/10/17/weeknotes-95-104/#numbering-mistake">past mistakes</a>.</p>
</li>
<li>
<p id="tech-podcasts">I’ve been listening to a lot of end-of-year episodes of tech podcasts (particularly ones I haven't heard before): <a href="https://www.relay.fm/connected/378">Connected</a>, <a href="https://www.relay.fm/cortex/123">Cortex</a>, <a href="https://www.relay.fm/mpu/620">Mac Power Users</a> and <a href="https://www.relay.fm/upgrade/387">Upgrade</a>. A common theme seems to be the rise of note-taking applications such as <a href="https://obsidian.md">Obsidian</a> and <a href="https://www.craft.do">Craft</a>. My colleague is an avid <a href="https://www.notion.so">Notion</a> and <a href="https://tot.rocks">Tot</a> user but I rely solely on <a href="https://apps.apple.com/us/app/notes/id1110145109">Apple's built-in Notes app</a>.</p>
<p>I tend to be minimalist when it comes to software but in the spirit of trying new things (e.g. I’m currently trying <a href="https://mimestream.com">Mimestream</a> as a new email client), I’m increasingly tempted.</p>
</li>
<li>
<p>After <a href="https://tomstu.art/weeknotes-97-epic-yawnfest">Tom</a> and Leo recommended it, I got a <a href="https://challengerbreadware.com/product/challenger-bread-pan/">Challenger Bread Pan</a> for Christmas from E. I’ve baked four loaves in it using the <a href="https://challengerbreadware.com/cast-iron-pan-care-use/">recommended instructions</a> and they have been some of my best yet.</p>
<p class="center"><img src="/i/challenger-pan.jpg" width="375" height="281" alt="" /></p>
</li>
<li>
<p>Over Christmas, we stayed at my parents’ for two nights so I got to see my dad playing with C. When I was C’s age, my dad worked nights at a kebab shop in Leeds city centre and he says he can’t remember me or my sister from that time. I watched the two of them pretend to buy and taste various treats from a toy ice cream van.</p>
</li>
<li>
<p>In a burst of house pride, I’ve put up new shelves and a bathroom mirror, replaced the rotten draught excluder from our front door and have just applied the first coat of “cornflower blue” paint in my office to replace the olive green from the previous owners.</p>
<p>I even changed my Terminal font from <a href="https://www.jetbrains.com/lp/mono/">JetBrains Mono</a> to <a href="https://github.com/tonsky/FiraCode">Fira Code</a> and picked a new <a href="https://basicappleguy.com/basicappleblog/macosbliss">desktop wallpaper</a>.</p>
</li>
<li>
<p>For Christmas, <a href="http://willhigo.com">Will</a> gave me “<a href="https://www.playstation.com/en-gb/games/the-last-of-us-part-ii/">The Last of Us: Part II</a>” for the PlayStation 4. I had bought myself “<a href="https://www.nintendo.co.uk/Games/Nintendo-Switch-download-software/Chicory-A-Colorful-Tale-2090478.html">Chicory</a>” for the Nintendo Switch but don’t make any time for playing video games.</p>
<p>However, I have a plan: I’ve ordered a <a href="https://store.google.com/gb/product/chromecast_google_tv?hl=en-GB">Chromecast with Google TV</a> so I can move my PlayStation 4 from downstairs (where it currently provides access to Netflix, Disney+, etc. on my 12 year old, non-smart TV) upstairs.</p>
<p id="five-minute-bursts">This potentially creates a single place where I can play harrowing video games in five minute bursts over the next few years.</p>
</li>
<li>
<p><strong>Injury detail warning:</strong> A few months ago, E cut her finger badly enough that the <a href="https://111.nhs.uk">NHS 111</a> expert system told us to go to our nearest hospital’s Accident and Emergency department. The deciding option contained the phrase “visible meat and/or gristle”. Thankfully, it wasn’t too severe and healed up without issue.</p>
<p>When preparing a birthday meal for Will, E cut herself again and this time we were prepared with <a href="https://www.boots.com/boots-skin-closures-8-closures-10115222">skin closures</a>, dressing pads, microporous tape and finger bandages.</p>
</li>
<li>
<p>In my <a href="/2022/01/02/2021-yearnotes/">rushed yearnotes</a>, I said I would try to run every week.</p>
<p>One thing I enjoyed from listening to all those end-of-year podcast episodes was the discovery of <a href="https://www.themesystem.com">CGP Grey and Myke Hurley’s “theme system”</a>. Instead of setting a fragile goal like “run every week” which is so easily unmet, I should have framed 2022 as “The Year of Fitness” or “The Year of Health”.</p>
<p id="secretly-eating-crisps">At the very least, I want to be more active than the latter half of 2021 which was spent mostly in a desk chair, interrupted only by going downstairs to secretly eat crisps.</p>
</li>
<li>Good news, everyone. There’s a new series of <a href="https://www.channel4.com/programmes/the-great-pottery-throw-down">The Great Pottery Throw Down</a>.</li>
</ul>
2021 Yearnoteshttps://mudge.name/2022/01/02/2021-yearnotes/2022-01-02T19:59:00+00:002022-01-02T19:59:00+00:00Another year of notes.<p>As <a href="https://zerodivisionerror.substack.com/p/the-one-after-a-while">Maciej put it</a>:</p>
<blockquote>
<p>I feel that if I start explaining why I have not written sooner, I’ll end up not finishing this post at all, so instead let me just skip that part altogether.</p>
</blockquote>
<p>Following reflections on <a href="/2019/01/02/2018-yearnotes/">2018</a>, <a href="/2020/01/05/weeknotes-10/">2019</a> and <a href="/2021/01/03/weeknotes-62/">2020</a> and having fixed a drier, recharged a car battery and eaten far too much over the past fortnight, it’s time to consider the <a href="https://en.wikipedia.org/wiki/2021">2021st year of the Common Era</a>.</p>
<ul>
<li>
<p><a href="/2020/12/21/weeknotes-59-and-60/">This time last year</a> we moved in with my parents for “the foreseeable” following the death of my grandfather and only a day or two before <a href="https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/948593/COVID-19_Tier_Posters_16_December_2020_04.pdf">Coronavirus tier 4</a> was introduced.</p>
</li>
<li>
<p>We stayed for six months, only returning to London to sell our flat and leave the city after calling it home for 12 years.</p>
</li>
<li>
<p>We moved to Ilkley, a town picked largely at random and because it is approximately 30-40 minutes’ drive from both my and E’s families.</p>
</li>
<li>
<p>C gets to see his grandparents almost weekly and we all celebrated his second birthday together.</p>
</li>
<li>
<p>After seven months in West Yorkshire, I’m slowly losing confidence in how to pronounce the word “butter”.</p>
</li>
<li>
<p>Last year’s weeknotes began with new running shoes and E and I managed weekly runs while living at my parents’ but—despite our proximity to <a href="https://en.wikipedia.org/wiki/Ilkley_Moor">the moor</a>—the rest of the year has been much more sedentary.</p>
</li>
<li>
<p>I’m still working my way through <a href="/2021/11/06/weeknotes-105-107/">two boxes of Hallowe’en sweets</a>.</p>
</li>
<li>
<p>While E and I are fully vaccinated and now boosted, the month before Christmas was mostly spent miserable with various colds brought home from nursery by C.</p>
</li>
<li>
<p>I overcame my fear of modern CSS by taking <a href="https://css-for-js.dev">Josh Comeau’s “CSS for JS” course</a>.</p>
</li>
<li>
<p>Almost all of the TV shows I watched involved some sort of comforting competition between skilled craftspeople: <a href="https://www.channel4.com/programmes/the-great-pottery-throw-down">The Great Pottery Throwdown</a>, <a href="https://thegreatbritishbakeoff.co.uk">The Great British Bake-Off</a>, <a href="https://www.channel4.com/programmes/handmade-britains-best-woodworker">Handmade: Britain’s Best Woodworker</a>, <a href="https://www.bbc.co.uk/programmes/b00mx9xb">MasterChef: The Professionals</a>.</p>
</li>
<li>
<p>I remain incapable of properly relaxing and the daily routine of having a two year old certainly hasn’t helped. Working from home has been fantastic for many reasons but it adds to the <a href="https://en.wikipedia.org/wiki/Groundhog_Day_(film)">Groundhog Day</a> feeling of the past year. I’m not entirely sure how to address that but I’ll start by trying to see local friends with greater regularity than once every few months.</p>
</li>
<li>
<p id="running">As for my deteriorating fitness: my hamstrings currently ache from my first run in months and I will attempt to run along the <a href="https://en.wikipedia.org/wiki/River_Wharfe">River Wharfe</a> every weekend.</p>
</li>
</ul>
Weeknotes 109https://mudge.name/2021/11/21/weeknotes-109/2021-11-21T14:09:00+00:002021-11-21T14:09:00+00:00Switching to Apple Silicon, focus, securing wall plugs and needing a cuddle.<ul>
<li>
<p>I’ve switched to my <a href="https://www.apple.com/uk/macbook-pro-14-and-16/">new laptop</a>.</p>
<p>To upgrade, I dragged over my <code class="language-plaintext highlighter-rouge">.ssh</code>, <code class="language-plaintext highlighter-rouge">.gnupg</code>, <code class="language-plaintext highlighter-rouge">Work</code>, <code class="language-plaintext highlighter-rouge">Projects</code> and <code class="language-plaintext highlighter-rouge">Music</code> directories as well as 23 years of photos and videos from a <a href="https://support.apple.com/en-us/HT201250">Time Machine</a> backup on an <a href="https://www.samsung.com/semiconductor/minisite/ssd/product/portable/t5/">external SSD</a>. Everything else was installed via <a href="https://brew.sh">Homebrew</a> and, while I have a <a href="https://github.com/Homebrew/homebrew-bundle"><code class="language-plaintext highlighter-rouge">Brewfile</code></a>, I went through <a href="https://konmari.com/marie-kondo-rules-of-tidying-sparks-joy/">the ritual</a> of <code class="language-plaintext highlighter-rouge">brew install</code>ing everything individually.</p>
<p>I’ve only run into three issues so far:</p>
<ol>
<li>
<p>The <a href="https://devcenter.heroku.com/articles/heroku-cli#apple-silicon-issues">Heroku CLI</a> uses a non-Apple Silicon native build of <a href="https://nodejs.org/en/">Node.js</a> so it requires <a href="https://support.apple.com/en-us/HT211861">Rosetta 2</a> which you can install via <code class="language-plaintext highlighter-rouge">softwareupdate</code>:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>softwareupdate <span class="nt">--install-rosetta</span>
</code></pre></div> </div>
</li>
<li>
<p>Similarly, older versions of <a href="https://www.terraform.io">Terraform</a> aren’t compiled for arm64 on macOS so trying to <a href="https://github.com/asdf-community/asdf-hashicorp">install them with <code class="language-plaintext highlighter-rouge">asdf</code></a> will fail unless you explicitly tell it to install the amd64 version:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">ASDF_HASHICORP_OVERWRITE_ARCH</span><span class="o">=</span>amd64 asdf <span class="nb">install</span>
<span class="go">Downloading terraform version 0.11.14 from https://releases.hashicorp.com/terraform/0.11.14/terraform_0.11.14_darwin_amd64.zip
</span></code></pre></div> </div>
</li>
<li>
<p><a href="https://docs.brew.sh/Installation">Homebrew installs to <code class="language-plaintext highlighter-rouge">/opt/homebrew</code> for Apple Silicon</a> and not <code class="language-plaintext highlighter-rouge">/usr/local</code> so any paths in <code class="language-plaintext highlighter-rouge">.zshrc</code>, <code class="language-plaintext highlighter-rouge">.gnupg/gpg-agent.conf</code>, etc. need updating.</p>
</li>
</ol>
</li>
<li>
<p>E recommended I listen to the <a href="https://www.bbc.co.uk/programmes/m00114h7">“Unfocus” episode of BBC Radio 4’s The Digital Human</a>:</p>
<blockquote>
<p>We’ve all had experiences of our attention wandering, usually at those moments when we most need to concentrate.</p>
<p>But, in our productivity-driven society, are we placing too much emphasis on paying attention and failing to recognise the benefits of more unstructured thought processes? After all, focus comes at a cost. With numerous demands on our attention, it’s all too easy to experience burnout. Unfocus can recharge our batteries and allow us to be creative by making connections and connecting with other people.</p>
<p>In this episode, Aleks Krotoski explores some of the different modes of attention we can switch between and asks whether, perhaps, we should be awarding our unfocus equal status to our focus.</p>
</blockquote>
</li>
<li>
<p>I put some blinds up in a bay window but drilled some of the holes too big. Thankfully, <a href="https://youtu.be/pxD9mLqhuSc">someone tested 11 different methods for securing loose wall plugs</a> revealing a solution that is obvious in retrospect: use a <em>second</em> wall plug to secure the first.</p>
</li>
<li>
<p>Every Sunday, I take C to swimming lessons at the local grammar school. Each lesson culminates in a brief bit of free time where he hopes to play with one of two watering cans. However, he is currently full of cold and feverish, occasionally calling out “I need a cuddle”.</p>
</li>
<li>
<p>A combination of parenthood and moving out of London means we rarely get to eat out but we’ve been occasionally using <a href="https://www.dishpatch.co.uk">Dishpatch</a> to have date nights at home. They can be quite involved but mean a change from our usual routine.</p>
<p class="center"><img src="/i/dishpatch.jpg" width="375" height="500" alt="" /></p>
</li>
</ul>
Weeknotes 108https://mudge.name/2021/11/14/weeknotes-108/2021-11-14T15:35:00+00:002021-11-14T15:35:00+00:00Nuzzling, replacing Enzyme, Tailwind CSS and our current TV rotation.<ul>
<li>
<p>After <a href="/2021/11/06/weeknotes-105-107/">writing about white noise apps last week</a>, <a href="https://scottm.medium.com">Scott</a> pointed out <a href="https://support.apple.com/en-gb/HT212775">iOS 15 and iPadOS 15 have ambient noises built-in</a>.</p>
</li>
<li>
<p>E’s cousin, her husband and young daughter stayed with us from Thursday. During their stay, it has become clear that C is a serial nuzzler.</p>
</li>
<li>
<p>I couldn’t upgrade a legacy JavaScript application to <a href="https://reactjs.org/blog/2020/10/20/react-v17.html">React v17.0</a> as its test suite was written with <a href="https://enzymejs.github.io/enzyme/">Enzyme</a> (which <a href="https://github.com/enzymejs/enzyme/issues/2429">doesn’t officially support the latest version of React</a>). We decided to replace Enzyme with a combination of <a href="https://reactjs.org/docs/test-renderer.html">React Test Renderer</a> and <a href="https://testing-library.com/react">React Testing Library</a> with <a href="https://testing-library.com/docs/ecosystem-jest-dom">jest-dom</a>.</p>
<p>We ported any <a href="https://jestjs.io/docs/snapshot-testing">snapshot tests</a> like the following:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">matches its snapshot</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">mount</span><span class="p">(<</span><span class="nc">Component</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">).</span><span class="nx">toMatchSnapshot</span><span class="p">()</span>
<span class="p">})</span>
</code></pre></div> </div>
<p>To use React Test Renderer instead:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-test-renderer</span><span class="dl">'</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">matches its snapshot</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">create</span><span class="p">(<</span><span class="nc">Component</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">.</span><span class="nx">toJSON</span><span class="p">()).</span><span class="nx">toMatchSnapshot</span><span class="p">()</span>
<span class="p">})</span>
</code></pre></div> </div>
<p>Any tests that asserted the presence of specific components went from:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">contains a child component</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">mount</span><span class="p">(<</span><span class="nc">Component</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ChildComponent</span><span class="p">)).</span><span class="nx">toExist</span><span class="p">()</span>
<span class="p">})</span>
</code></pre></div> </div>
<p>To:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-test-renderer</span><span class="dl">'</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">contains a child component</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">create</span><span class="p">(<</span><span class="nc">Component</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">.</span><span class="nx">root</span><span class="p">.</span><span class="nx">findByType</span><span class="p">(</span><span class="nx">ChildComponent</span><span class="p">)).</span><span class="nx">toBeTruthy</span><span class="p">()</span>
<span class="p">})</span>
</code></pre></div> </div>
<p>We rewrote tests that asserted on various attributes of <a href="https://www.w3.org/TR/html-aria/#docconformance">elements with ARIA roles</a> from:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">adds the specified class name to the image</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">mount</span><span class="p">(<</span><span class="nc">Component</span> <span class="na">className</span><span class="p">=</span><span class="s">"another-class-name"</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="dl">'</span><span class="s1">.some-img-class</span><span class="dl">'</span><span class="p">)).</span><span class="nx">toHaveClassName</span><span class="p">(</span><span class="dl">'</span><span class="s1">another-class-name</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div> </div>
<p>To:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">render</span><span class="p">,</span> <span class="nx">screen</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@testing-library/react</span><span class="dl">'</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">adds the specified class name to the image</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">render</span><span class="p">(<</span><span class="nc">Component</span> <span class="na">className</span><span class="p">=</span><span class="s">"another-class-name"</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">screen</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="dl">'</span><span class="s1">img</span><span class="dl">'</span><span class="p">)).</span><span class="nx">toHaveClass</span><span class="p">(</span><span class="dl">'</span><span class="s1">another-class-name</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div> </div>
<p>The remaining tests all used CSS selectors to assert attributes of elements without ARIA roles so we went from:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">adds the specified class name to the container</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">mount</span><span class="p">(<</span><span class="nc">Component</span> <span class="na">className</span><span class="p">=</span><span class="s">"another-class-name"</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="dl">'</span><span class="s1">.some-div-class</span><span class="dl">'</span><span class="p">)).</span><span class="nx">toHaveClassName</span><span class="p">(</span><span class="dl">'</span><span class="s1">another-class-name</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div> </div>
<p>To use <a href="http://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector"><code class="language-plaintext highlighter-rouge">querySelector</code></a> <a href="https://testing-library.com/docs/react-testing-library/api#container-1">though this is not recommended</a>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@testing-library/react</span><span class="dl">'</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">adds the specified class name to the container</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">container</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">render</span><span class="p">(<</span><span class="nc">Component</span> <span class="na">className</span><span class="p">=</span><span class="s">"another-class-name"</span> <span class="p">/>)</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">container</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.some-div-class</span><span class="dl">'</span><span class="p">)).</span><span class="nx">toHaveClassName</span><span class="p">(</span><span class="dl">'</span><span class="s1">another-class-name</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div> </div>
</li>
<li>
<p>Since <a href="/2021/10/17/weeknotes-95-104/">improving my CSS knowledge</a>, I’ve been using <a href="https://tailwindcss.com">Tailwind CSS</a> for all new projects that require custom design work, e.g. implementing a design from a mock-up or wireframes.</p>
<p>At first, I mistook it for something like <a href="https://getbootstrap.com">Bootstrap</a>: an entire framework of pre-written styled components that I could reuse with little thought. However, it made much more sense when I saw it as an alternate way of writing CSS. Instead of writing CSS declarations in classes, you use Tailwind’s utility classes directly on your elements.</p>
<p>While I initially balked at all the inline classes, in reality I’m replacing code like this:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.bubble</span> <span class="p">{</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span>
<span class="nl">border-radius</span><span class="p">:</span> <span class="m">1.5rem</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.arrow</span> <span class="p">{</span>
<span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
<span class="nl">top</span><span class="p">:</span> <span class="m">-1rem</span><span class="p">;</span>
<span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">right</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">margin-left</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">margin-right</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.arrow</span> <span class="nt">img</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div> </div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"bubble"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"arrow"</span><span class="nt">><img</span> <span class="na">src=</span><span class="s">"arrow.svg"</span> <span class="na">width=</span><span class="s">"16"</span> <span class="na">height=</span><span class="s">"16"</span> <span class="na">alt=</span><span class="s">""</span> <span class="nt">/></div></span>
<span class="nt"><p></span>I want to turn people into dinosaurs.<span class="nt"></p></span>
<span class="nt"></div></span>
</code></pre></div> </div>
<p>With this:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"relative p-4 bg-white rounded-3xl"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"absolute inset-x-0 -top-4 mx-auto"</span><span class="nt">><img</span> <span class="na">class=</span><span class="s">"w-4 h-4"</span> <span class="na">src=</span><span class="s">"arrow.svg"</span> <span class="na">width=</span><span class="s">"16"</span> <span class="na">height=</span><span class="s">"16"</span> <span class="na">alt=</span><span class="s">""</span> <span class="nt">/></div></span>
<span class="nt"><p></span>I want to turn people into dinosaurs.<span class="nt"></p></span>
<span class="nt"></div></span>
</code></pre></div> </div>
<p>The corresponding CSS classes still exist but I can delegate their maintenance to Tailwind’s authors.</p>
</li>
<li>
<p>Our current television-watching rotation is as follows:</p>
<ul>
<li><a href="https://www.hbo.com/succession/season-3">Succession: Season 3</a></li>
<li><a href="https://tv.apple.com/gb/show/the-morning-show/umc.cmc.25tn3v8ku4b39tr6ccgb8nl6m">The Morning Show: Season 2</a></li>
<li><a href="https://www.channel4.com/programmes/the-great-british-bake-off">The Great British Bake Off</a></li>
<li><a href="https://www.channel4.com/programmes/handmade-britains-best-woodworker">Handmade: Britain’s Best Woodworker</a></li>
<li><a href="https://www.imdb.com/title/tt13623580/">Reservation Dogs</a></li>
<li><a href="https://www.channel4.com/programmes/the-other-two">The Other Two</a></li>
</ul>
</li>
</ul>
Weeknotes 105–107https://mudge.name/2021/11/06/weeknotes-105-107/2021-11-06T13:42:00+00:002021-11-06T13:42:00+00:00A future gourmand, skipping a generation, HTTP headers and a backlog of Starmix.<ul>
<li>
<p>C often asks me “where are you [meaning “am I”] going today?” even if it is the end of the day. I’ll ask him back “what did we do today?” and his answers vary in accuracy. More than once, he has regaled me of the time we saw a double rainbow on the way to the butcher to buy a cormorant.</p>
<p>I fear he is on the path to be the kind of gourmand who <a href="https://en.wikipedia.org/wiki/Ortolan_bunting">eats an Ortolan bunting with a linen napkin draped over his head</a>.</p>
</li>
<li>
<p>After sitting out an <a href="https://support.apple.com/kb/SP749?locale=en_GB">entire generation of MacBook Pro</a>, I ordered a new <a href="https://www.apple.com/uk/macbook-pro/">14” MacBook Pro</a>. Given I had to put my 15” Mid-2015 MacBook Pro in the fridge after <a href="https://support.apple.com/en-gb/HT207359"><code class="language-plaintext highlighter-rouge">kernel_task</code> desperately took up 500% CPU</a>, I am looking forward to its arrival.</p>
</li>
<li>
<p>I’ve <a href="/2020/01/26/weeknotes-13/">mentioned before how we play white noise</a> to help C sleep but after too many mornings finding “Rain Rain” crashed, I decided to pay up for “<a href="https://darknoise.app">Dark Noise</a>” instead. While I have no need for custom animations, it hasn’t crashed yet in the several weeks I have been using it.</p>
</li>
<li>
<p>On reflection, <a href="/2021/10/17/weeknotes-95-104/">our holiday</a> was a more intense version of our daily life: spending our time wrangling a two-year old but without the familiarity of home or support of nearby family. Lesson learnt, our next holiday will involve having more people around.</p>
</li>
<li>
<p>I’ve been looking into best practices around HTTP headers using sites such as <a href="https://observatory.mozilla.org">Mozilla Observatory</a> and <a href="https://securityheaders.com">Security Headers</a>. I fear I cannot put off getting to grips with <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy"><code class="language-plaintext highlighter-rouge">Content-Security-Policy</code> and its many directives</a> for much longer but I’m hoping <a href="https://infosec.mozilla.org/guidelines/web_security#content-security-policy">Mozilla’s web security guidelines</a> will be helpful.</p>
</li>
<li>
<p>As this was our first Hallowe’en in our new house and I wasn’t sure whether to expect trick-or-treaters, I bought a Haribo “party box” and a tub of “sweet shop favourites”. However, out of ignorance, we neither put on an outside light nor had any decorations so you’ll now find me working my way through tiny bags of Starmix every afternoon.</p>
</li>
</ul>
Weeknotes 95–104https://mudge.name/2021/10/17/weeknotes-95-104/2021-10-17T18:08:00+00:002021-10-17T18:08:00+00:00Numbering, a false start to a holiday, learning CSS, moving S3 static websites and an end to DIY.<ul>
<li>
<p id="numbering-mistake">It was a mistake to number my weeknotes by weeks rather than by notes.</p>
</li>
<li>
<p>We are on holiday for the first time since C’s first birthday over a year ago.</p>
<p>We had a false start as the “eco lodge” we booked for the week was littered with mouse droppings. We’re now in our second accommodation, E has made a fire and I’m feeling more relaxed.</p>
</li>
<li>
<p>C continues to bring home from nursery a variety of colds and, on one occasion, <a href="https://www.nhs.uk/conditions/hand-foot-mouth-disease/">hand, foot and mouth disease</a> (not to be confused with <a href="https://www.gov.uk/guidance/foot-and-mouth-disease">foot and mouth</a>).</p>
</li>
<li>
<p>I haven’t <a href="/2021/08/15/weeknotes-83-94/">played Marvel’s Spider-Man</a> as I enjoyed <a href="https://www.doublefine.com/games/psychonauts-2">Psychonauts 2</a> instead, released five years after I became its 14,463rd backer.</p>
</li>
<li>
<p>In order to bring my CSS knowledge up-to-date I’ve been working through <a href="https://css-for-js.dev/">Josh Comeau’s “CSS for JS” course</a>. I was hesitant at first but I’ve learnt an incredible amount in the first two modules. I realise I’ve never understood the different <a href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model">box models</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing">margin collapsing</a> or even how <a href="https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Positioning">positioned layout</a> works until now.</p>
<p>A lot of this has influenced my work on the new <a href="https://www.raspberrypi.com/">raspberrypi.com</a>.</p>
</li>
<li>
<p>C has two great loves: road vehicles and birds. We alternate between <a href="https://www.bbc.co.uk/programmes/m0009mbx">Grace’s</a> (formerly <a href="https://www.bbc.co.uk/programmes/p06m8m6y">Catie’s</a>) Amazing Machines and scrolling through the <a href="https://apps.apple.com/gb/app/collins-bird-guide/id868827305">Collins Bird Guide iOS app</a>.</p>
</li>
<li>
<p>I had to move a subdomain to another domain for a website served by <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html">Amazon S3’s static website hosting</a> and <a href="https://aws.amazon.com/cloudfront/">CloudFront</a>. I couldn’t find examples of redirecting from one domain to another without setting up a new distribution for the new domain and a new S3 bucket to <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html#redirect-endpoint-host">redirect all requests from the old domain</a>.</p>
<p>I avoided the new bucket and distribution by creating a new SSL certificate for both the old and new domain using <a href="https://aws.amazon.com/certificate-manager/">Amazon Certificate Manager</a> and then updating the existing CloudFront distribution to use that certificate and the new domain as an <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#CreatingCNAME">alias</a>.</p>
<p>With both domains now serving requests, I deployed the following Lambda@Edge function and associated it with CloudFront as a <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html"><code class="language-plaintext highlighter-rouge">viewer-request</code> function</a>. This way, there’s one bucket and one distribution, both domains serve traffic but requests to the old will be permanently redirected to the new:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">handler</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Records</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">cf</span><span class="p">.</span><span class="nx">request</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">host</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">host</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">value</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">host</span><span class="p">.</span><span class="nx">endsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">.old.domain</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="dl">'</span><span class="s1">301</span><span class="dl">'</span><span class="p">,</span>
<span class="na">statusDescription</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Moved Permanently</span><span class="dl">'</span><span class="p">,</span>
<span class="na">body</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">location</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">key</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Location</span><span class="dl">'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s2">`https://new.domain</span><span class="p">${</span><span class="nx">request</span><span class="p">.</span><span class="nx">uri</span><span class="p">}</span><span class="s2">`</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">request</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div> </div>
</li>
<li>
<p>I second <a href="https://www.twitter.com/cabel/status/1426269188348268548">Cabel Sasser’s recommendation to add a large Photos widget on your Home Screen</a> once you <a href="https://www.twitter.com/cabel/status/1426270646468714496">tell it which people you don’t want featuring</a>.</p>
</li>
<li>DIY improvements on our house have stopped after disconnecting useless telephone and aerial cables, replacing the thermostat, fixing the fridge door with <a href="https://sugru.com/">Sugru</a> and filling holes in the external walls with silicone.</li>
</ul>
Weeknotes 83–94https://mudge.name/2021/08/15/weeknotes-83-94/2021-08-15T12:54:00+00:002021-08-15T12:54:00+00:00The past twelve weeks in summary.<ul>
<li>
<p>We moved house.</p>
</li>
<li>
<p>Our <a href="https://www.bradford.gov.uk/recycling-and-waste/wheeled-bins-and-recycling-containers/what-goes-in-my-recycling-and-waste-bins-and-bags/">recycling bin is grey and our general waste bin is green</a> (<a href="https://frinkiac.com/caption/S12E18/437896">what a country!</a>) and both have the letters <a href="https://www.bradford.gov.uk">“C.B.M.D.C.”</a> stamped on them.</p>
</li>
<li>
<p>I’ve replaced most of the old light fittings and a vast number of various halogen lamps with simple <a href="https://www.mkelectric.com/en-gb/Pages/default.aspx">MK Electric</a> pendants with LED bulbs. The fluted brass dimmer switches have all gone, replaced with normal light switches that don’t produce a high-pitched whining noise.</p>
</li>
<li>
<p>We had just over a week of building work done shortly after moving in to remedy some issues flagged by our structural survey. The work consisted mostly of repointing and replacing some poorly designed coping stones which lacked any sort of drip groove. Any rainfall (and there’s a lot more here than in London) would then pour down the wall and into the living room bay window below.</p>
<p>It was worth doing just to expand my vocabulary of building jargon.</p>
</li>
<li>
<p>I didn’t realise that different screws had different characteristics until <a href="https://po-ru.com/2021/07/11/week-27-blood-sweat-and-skirting-boards">Paul Battley wrote about the benefits of Torx head screws</a>. I’ve needlessly stripped so many screw heads due to <a href="https://en.wikipedia.org/wiki/Cam_out">camming out</a>.</p>
</li>
<li>
<p>The initial rush of renovations (new lighting, filling holes with silicone, replacing leaky flush valves, repainting E’s office from a particularly upsetting shade of yellow) has slowed to a more manageable crawl as things that once seemed urgent have receded into being <em>fine for now</em>.</p>
</li>
<li>
<p>After nearly eight months at home or with my parents, C is once more going to a nursery for three days a week. All three of us are full of cold and I write this with a warm mug of <a href="https://www.boots.com/boots-max-strength-cold-and-flu-blackcurrant-flavour-10-sachets-10075291">Boots Pharmaceuticals Max Strength Cold & Flu Relief Blackcurrant Flavour</a> to hand.</p>
</li>
<li>
<p>A friend returned from an intermission while watching “<a href="https://www.imdb.com/title/tt5034838/">Godzilla vs. Kong</a>” in a <a href="https://www.amazon.co.uk/adlp/watchparty">Prime Video Watch Party</a> with ice cream in a cone, explaining that one can simply buy waffle cones (chocolate dipped or not) from the supermarket.</p>
</li>
<li>
<p>After stocking up on cones, I made a small batch of <a href="https://milkbarstore.com">Christina Tosi</a>’s cereal milk ice cream by steeping corn flakes in milk and then using that milk to <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2010/jun/17/how-to-make-ice-cream">make Felicity Cloake’s “perfect” ice cream</a>. The milk powder I found in our cupboard expired in 2016 and smelled strongly of cheese so I left it out of the recipe.</p>
</li>
<li>
<p>It took me <a href="/2020/04/26/weeknotes-26/">over a year</a> but I finally finished <a href="https://bethesda.net/game/doom">Doom Eternal</a>. It was gruelling and relentless—especially during the final boss battle—but I enjoyed <a href="https://youtu.be/1MZbLOYtM2Y">shooting a hole in a planet</a>. A friend bought me <a href="https://www.playstation.com/en-gb/games/marvels-spider-man/">Marvel’s Spider-Man</a> to play next which I’m hoping will be slightly less stressful.</p>
</li>
<li>
<p>E and I are watching the German science fiction series <a href="https://www.netflix.com/gb/title/80100172">Dark</a>, chasing it with the sitcom <a href="https://www.netflix.com/gb/title/80061132">Superstore</a> to lighten an otherwise grim evening of murder and <i>Zeitreise</i>.</p>
</li>
<li>
<p>I spend a lot of my time doodling commissions for C either on paper or on a blackboard in our kitchen. They consist mostly of vehicles (police cars, police vans, camper vans, concrete mixer trucks) but occasionally there will be a curveball. After enjoying <a href="https://youtu.be/dVL69qGmJUM">The Singing Kettle’s “My Boy’s a Cowboy”</a> which features a rousing “oompa oompa”, C demanded an “oompa bear”. I dutifully drew a Bavarian teddy bear in lederhosen, eyes squeezed tight into two <a href="https://en.wikipedia.org/wiki/Intersection_(set_theory)#Notation_and_terminology">set intersections</a>.</p>
</li>
<li>
<p>In two weeks time, it’ll be C’s second birthday.</p>
</li>
</ul>
Weeknotes 80–82https://mudge.name/2021/05/22/weeknotes-80-82/2021-05-22T13:16:00+00:002021-05-22T13:16:00+00:00Legally baht ’at, checklists, vaccinations and the Will of the Wisps.<ul>
<li>
<p>We exchanged contracts on both <a href="/2021/02/07/weeknotes-67/">the sale of our flat</a> and the purchase of a house in <a href="https://en.wikipedia.org/wiki/Ilkley">Ilkley</a>.</p>
</li>
<li>
<p>With under two weeks to go before we complete, I’m struggling to get my head around having a place of our own again. You will mostly find me methodically working through checklists of utility companies, local councils and broadband providers in <a href="https://trello.com">Trello</a>.</p>
</li>
<li>
<p>E and I both have our first <a href="https://www.nhs.uk/conditions/coronavirus-covid-19/coronavirus-vaccination/">Coronavirus vaccinations</a> tomorrow. I look forward to catching the end of <a href="https://xkcd.com/2459/">May 2021</a>.</p>
</li>
<li>
<p>After completing “<a href="/2021/05/02/weeknotes-79/">Ori and the Blind Forest</a>”, I invested in a <a href="https://store.nintendo.co.uk/en_gb/-nintendo-switch-pro-controller-000000000002510466.html">Nintendo Switch Pro controller</a> and finished “<a href="https://www.orithegame.com">Ori and the Will of the Wisps</a>” too.</p>
<p>It was even more glorious than the first game, clearly taking a lot of inspiration from another beloved <a href="https://en.wikipedia.org/wiki/Metroidvania">Metroidvania</a>, “<a href="/2019/01/02/2018-yearnotes/">Hollow Knight</a>”.</p>
<p>I enjoyed <a href="https://youtu.be/gIdHTL18kTU">“On the Level” with Chris McEntee about some of Ori’s game design</a> though it does spoil part of the game.</p>
<p>I hoped the Pro controller would help with the <a href="https://www.nhs.uk/conditions/repetitive-strain-injury-rsi/">repetitive strain injury</a> playing the Switch gives me but sadly it continues to wreak some amount of havoc on my wrists.</p>
</li>
<li>
<p>I completed one more orbit around the sun.</p>
</li>
</ul>
Weeknotes 79https://mudge.name/2021/05/02/weeknotes-79/2021-05-02T12:36:00+00:002021-05-02T12:36:00+00:00Watermarking PDFs, alarming conditionals and other Computation Clubs.<ul>
<li>
<p>With no more “<a href="/2021/04/18/weeknotes-77/">Worship the Sun</a>” to play, I bought “<a href="https://www.orithegame.com/blind-forest/">Ori and the Blind Forest</a>” for the Nintendo Switch to meet my need for platforming games that border on the cruel.</p>
</li>
<li>
<p>I implemented a feature for a <a href="https://rubyonrails.org">Ruby on Rails</a> web application to watermark PDFs as they are downloaded. I used <a href="https://prawnpdf.org">Prawn</a> to generate a single A4 PDF with the user’s name and email address printed diagonally across the page, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Prawn</span><span class="o">::</span><span class="no">Document</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">page_size: </span><span class="s1">'A4'</span><span class="p">,</span> <span class="ss">margin: </span><span class="mi">0</span><span class="p">)</span> <span class="k">do</span>
<span class="n">font_size</span> <span class="mi">36</span>
<span class="n">fill_color</span> <span class="s1">'000000'</span>
<span class="n">rotate</span> <span class="mf">35.26</span><span class="p">,</span> <span class="ss">origin: </span><span class="p">[</span><span class="mf">297.64</span><span class="p">,</span> <span class="mf">420.945</span><span class="p">]</span> <span class="k">do</span>
<span class="n">transparent</span> <span class="mf">0.1</span> <span class="k">do</span>
<span class="n">text</span> <span class="s1">'Generated for Alice Bloggs <alice@example.com>'</span><span class="p">,</span> <span class="ss">align: :center</span><span class="p">,</span> <span class="ss">valign: :top</span>
<span class="n">text</span> <span class="s1">'Generated for Alice Bloggs <alice@example.com>'</span><span class="p">,</span> <span class="ss">align: :center</span><span class="p">,</span> <span class="ss">valign: :center</span>
<span class="n">text</span> <span class="s1">'Generated for Alice Bloggs <alice@example.com>'</span><span class="p">,</span> <span class="ss">align: :center</span><span class="p">,</span> <span class="ss">valign: :bottom</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>I then <a href="http://qpdf.sourceforge.net/files/qpdf-manual.html#ref.overlay-underlay">overlay</a> this onto whatever PDF document they are downloading from <a href="https://guides.rubyonrails.org/active_storage_overview.html">Active Storage</a> using <a href="http://qpdf.sourceforge.net">QPDF</a>.</p>
<p>As QPDF is a command-line tool, I needed a way to shell out to it in a safe way that could be used on-demand. Thankfully, <a href="https://github.com/minimagick/minimagick/blob/master/lib/mini_magick/shell.rb">MiniMagick does something very similar</a> shelling out to <a href="https://imagemagick.org/index.php">ImageMagick</a>’s various commands, e.g. <a href="https://imagemagick.org/script/convert.php"><code class="language-plaintext highlighter-rouge">convert</code></a>, <a href="https://imagemagick.org/script/mogrify.php"><code class="language-plaintext highlighter-rouge">mogrify</code></a>, <a href="https://imagemagick.org/script/command-line-tools.php">etc.</a></p>
<p>Having <a href="https://github.com/thoughtbot/paperclip/pull/324">seen ImageMagick commands cause an application to hang forever</a> (bonus trivia: that’s my first ever pull request on GitHub) and being <a href="https://devcenter.heroku.com/articles/request-timeout">subject to Heroku’s 30 second timeout</a>, I wanted to ensure the watermarking process had a strict time limit. Despite it being <a href="https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/">Ruby’s most dangerous API</a>, I took a leaf from MiniMagick’s book and used <a href="https://ruby-doc.org/stdlib-3.0.1/libdoc/timeout/rdoc/Timeout.html#timeout-method"><code class="language-plaintext highlighter-rouge">Timeout.timeout</code></a> like so:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">generate_pdf</span><span class="p">(</span><span class="n">infile</span><span class="p">,</span> <span class="n">overlayfile</span><span class="p">,</span> <span class="n">outfile</span><span class="p">)</span>
<span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span>
<span class="s1">'qpdf'</span><span class="p">,</span> <span class="n">infile</span><span class="p">.</span><span class="nf">path</span><span class="p">,</span> <span class="s1">'--overlay'</span><span class="p">,</span> <span class="n">overlayfile</span><span class="p">.</span><span class="nf">path</span><span class="p">,</span> <span class="s1">'--from='</span><span class="p">,</span> <span class="s1">'--repeat=1'</span><span class="p">,</span> <span class="s1">'--'</span><span class="p">,</span> <span class="n">outfile</span><span class="p">.</span><span class="nf">path</span>
<span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">_stdin</span><span class="p">,</span> <span class="n">_stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span>
<span class="k">break</span> <span class="k">if</span> <span class="no">Timeout</span><span class="p">.</span><span class="nf">timeout</span><span class="p">(</span><span class="n">timeout</span><span class="p">)</span> <span class="p">{</span> <span class="n">wait_thr</span><span class="p">.</span><span class="nf">value</span> <span class="p">}.</span><span class="nf">success?</span>
<span class="k">raise</span> <span class="no">WatermarkingError</span><span class="p">,</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span>
<span class="k">rescue</span> <span class="no">Timeout</span><span class="o">::</span><span class="no">Error</span>
<span class="no">Process</span><span class="p">.</span><span class="nf">kill</span><span class="p">(</span><span class="s1">'KILL'</span><span class="p">,</span> <span class="n">wait_thr</span><span class="p">.</span><span class="nf">pid</span><span class="p">)</span>
<span class="k">raise</span> <span class="no">WatermarkingError</span><span class="p">,</span> <span class="s1">'qpdf timed out'</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>At the end of this process, I have a <a href="https://ruby-doc.org/stdlib-3.0.1/libdoc/tempfile/rdoc/Tempfile.html"><code class="language-plaintext highlighter-rouge">Tempfile</code></a> with the watermarked PDF which I need to send to the user. Rails’ <a href="http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file"><code class="language-plaintext highlighter-rouge">send_file</code></a> seems a good choice as it’ll handle setting the appropriate headers for the file type and avoid issues with cache headers in old versions of Internet Explorer. However, if you are trying to clean up your temporary files on every request rather than letting the Ruby Garbage Collector handle it, you can’t <a href="https://ruby-doc.org/stdlib-3.0.1/libdoc/tempfile/rdoc/Tempfile.html#unlink-method">unlink</a> the file in your controller. If you do, by the time your file is actually read and served, it will have been deleted and your users will end up with an empty response.</p>
<p>Thankfully, <a href="https://github.com/rack/rack/blob/master/lib/rack/tempfile_reaper.rb">Rack comes with a Tempfile Reaper middleware</a> which allows you to clean <code class="language-plaintext highlighter-rouge">Tempfile</code>s created during a request by adding them to <code class="language-plaintext highlighter-rouge">request.env[Rack::RACK_TEMPFILES]</code>. This is <a href="https://guides.rubyonrails.org/rails_on_rack.html#inspecting-middleware-stack">part of Rails’ default middleware stack</a> so we can use it to remove <code class="language-plaintext highlighter-rouge">Tempfile</code>s only when they have been read.</p>
</li>
<li>
<p>As <a href="https://www.nhs.uk/conditions/coronavirus-covid-19/coronavirus-vaccination/coronavirus-vaccine/">people aged 40 and over can now get the COVID-19 vaccine</a>, I attempted to <a href="https://www.nhs.uk/conditions/coronavirus-covid-19/coronavirus-vaccination/book-coronavirus-vaccination/">book my sister’s first dose</a>. She hadn’t yet been contacted by her GP or the NHS so I looked for the next available appointment at the nearest vaccination centre and saw that there were slots within the next few days. However, demand is obviously high and the appointments disappeared from the site before I could book one.</p>
<p>On subsequent visits to the same site, I was then presented with the following message:</p>
<blockquote>
<p>You were unable to go to/missed your 1st appointment to get the coronavirus vaccination.</p>
<p>This means you need to book both of your appointments again.</p>
</blockquote>
<p>As we had never made an appointment, I assume this is due to some overly broad condition in the website, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">!</span><span class="n">users_first_time_using_site</span> <span class="o">&&</span> <span class="n">user_has_not_had_first_dose</span>
<span class="n">display_scary_message</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>Thankfully, she is now booked in for her first dose next weekend.</p>
</li>
<li>
<p>I attended last night’s <a href="https://bstn.cc">Boston Computation Club</a> to watch <a href="https://bstn.cc/artifacts/chrisPatuzzo/assembly.pdf">Chris Patuzzo’s talk “Assembly Programming for the M1</a>”. It was great to see Chris present his own work and to see him tease “Worship the Sun” at the end.</p>
<p>I learned that the club was inspired by <a href="https://london.computation.club">London Computation Club</a> and they found us due to <a href="https://github.com/computationclub/computationclub.github.io/wiki">our write-ups over the years</a> particularly <a href="https://github.com/computationclub/computationclub.github.io/wiki#types-and-programming-languages">those regarding Benjamin C. Pierce’s “Types and Programming Languages”</a>.</p>
</li>
</ul>
Weeknotes 78https://mudge.name/2021/04/29/weeknotes-78/2021-04-29T12:43:00+00:002021-04-29T12:43:00+00:00Conveyancing, ice packs on my laptop and a norny.<ul>
<li>
<p>Belated notes this week.</p>
</li>
<li>
<p>I haven’t been sharing updates on <a href="/2021/02/07/weeknotes-67/">our decision to leave London</a> but we are simultaneously selling our flat and buying a house closer to our family.</p>
<p>Bundles of paper secured with treasury tags have been handed to conveyancers, driving licences scanned while waiting, masked, in an empty waiting room. Queries sent and answered, PDFs <a href="https://eclecticlight.co/2020/12/11/how-effective-and-safe-is-previews-redaction-tool/">redacted</a> and, in a particularly stressful moment this week, hot water cylinder pipework “brought up to standard” during a service.</p>
<p>I am cautiously pessimistic when it comes to anything relating to property. I tell myself it is so I am prepared for the worst: should everything fall through, I will feel no different. However, the slightest hiccup (or <a href="https://english.stackexchange.com/questions/91910/are-there-regional-distinctions-in-how-hiccup-hiccough-is-spelled">is that hiccough</a>?) stresses me out no end so I’m not sure my strategy is working.</p>
</li>
<li>
<p>While <a href="/2021/04/18/weeknotes-77/">playing Chris Patuzzo’s “Worship the Sun”</a>, my mid-2015 MacBook Pro began to struggle with certain parts of the game. <a href="https://apple.stackexchange.com/a/372928">Using <code class="language-plaintext highlighter-rouge">powermetrics</code></a>, I discovered my CPU die temperature was reaching over 80 degrees C and causing everything to slow down due to throttling.</p>
<p>Desperate to finish the game, I ended up placing ice packs that came with a <a href="https://www.japancentre.com/en/products/15986-diy-shoryu-kit-shoryu-ganso-ramen">DIY Shoryu Ganso Ramen kit</a> on top of the keyboard and underneath the base of my laptop. I’m not sure it made as much difference as using <a href="https://obsproject.com">OBS</a>’s “low CPU usage preset” when recording but it was enough to get me through.</p>
</li>
<li>
<p>C began referring to something in his room as “norny”. Baffled, we questioned him further to eventually discover he was talking about a wooden fire engine.</p>
<p>What sound does a fire engine make? “Nor nee nor nee nor nee”.</p>
</li>
</ul>
Weeknotes 77https://mudge.name/2021/04/18/weeknotes-77/2021-04-18T13:01:00+00:002021-04-18T13:01:00+00:00Duggee o’clock, wall repair, ten hours of Worship the Sun, terrific tedium and a hot tub made of tweed.<ul>
<li>
<p>Every evening there is a moment when the daylight has waned just enough to trigger the light sensor in <a href="/2020/12/28/weeknotes-61/">C’s “Hey Duggee” wooden puzzle</a>. I look forward to <a href="https://www.heyduggee.com/characters/">Norrie’s</a> nightly cheer of “Duggee!” as we settle down to watch <a href="https://www.bbc.co.uk/programmes/b006t1k5">MasterChef</a> or <a href="https://www.bbc.co.uk/programmes/m0007mmw">Interior Design Masters</a>.</p>
</li>
<li>
<p>I used an <a href="https://youtu.be/Lmmkgeo0RBU">Artex Easifix wall repair kit</a> to fix a hole in the wall made by my late grandfather. The damage was minor enough that I largely ignored it but I came to realise it was a constant reminder for my parents. A wall patch, two layers of filler and a coat of paint later and the damage is no longer visible.</p>
</li>
<li>
<p>With <a href="https://www.gov.uk/guidance/covid-19-coronavirus-restrictions-what-you-can-and-cannot-do#april-whats-changed">the recent changes to restrictions</a>, we hosted some friends and their 3 year old for an outdoor pizza party. It has been over a year since I last saw them in person.</p>
<p>C hasn’t played with or really seen many children his age since he last went to nursery before Christmas. When he saw my friends’ little boy walk into the garden, he instantly lit up and walked slowly toward him, arms outstretched, head bowed as he went in to nuzzle against the boy’s chest.</p>
</li>
<li>
<p><em>Exactly</em> ten hours of recording later, I finished <a href="/2021/04/11/weeknotes-76/">playing Chris Patuzzo’s “Worship the Sun”</a>. Squeezing recording sessions into lunchtimes and the occasional evening meant I overdid it on my screen time quota for the week and my left wrist is certainly letting me know.</p>
<p>I thoroughly enjoyed the whole experience and was reminded of past favourites such as <a href="https://www.nintendo.co.uk/Games/Super-Nintendo/Super-Mario-World-752133.html">Super Mario World</a>, <a href="https://store.steampowered.com/app/620/Portal_2/">Portal 2</a> and, of course, <a href="http://the-witness.net">The Witness</a>.</p>
<p>While I’m sad there is no more to explore, Chris did hint there is one more thing to discover but it has so far evaded both <a href="https://tomstu.art">Tom</a> and me.</p>
</li>
<li>
<p>I enjoyed <a href="https://jacobian.org/2021/apr/7/embrace-the-grind/">Jacob Kaplan-Moss’ “Embrace the Grind”</a> and it made me consider when “doing the work” made a difference in my own experience.</p>
<blockquote>
<p>I often have people newer to the tech industry ask me for secrets to success. There aren’t many, really, but this secret — being willing to do something so terrifically tedious that it appears to be magic — works in tech too.</p>
</blockquote>
</li>
<li>
<p>I have listened (and gleefully sung along) to <a href="https://youtu.be/AdVgPCM5wEk">Brian David Gilbert’s “Just One Day - 2winz²”</a> more times than I can count.</p>
</li>
</ul>
Weeknotes 76https://mudge.name/2021/04/11/weeknotes-76/2021-04-11T13:34:00+00:002021-04-11T13:34:00+00:00Rushing, playing Worship the Sun and a very first haircut.<ul>
<li>
<p>I am rushing these notes before C wakes from his afternoon nap and we go to visit E’s sister for her birthday.</p>
</li>
<li>
<p>For the past five days, I’ve been recording myself playing <a href="https://tuzz.tech/blog/taking-the-plunge">Chris Patuzzo’s unfinished indie puzzle/platforming game, “Worship the Sun”</a>.</p>
<p>I’ve never done anything like this before but it seems a good way to show Chris how I play the game, explaining my thinking when I get stuck or mutter under my breath when I fail to make a particularly tricky jump.</p>
<p>Meanwhile, <a href="https://tomstu.art">Tom</a> has been doing the same and comparing our experiences has been amusing. In parts, Tom has mused over his current understanding of the game’s internal logic while I desperately try to glitch my way onto a floating platform, audibly mashing the space bar as I do so.</p>
</li>
<li>
<p>I’ve been using <a href="https://obsproject.com">OBS Studio</a> to record myself playing the game. While it was initially fiddly to grant it sufficient permission to record my screen and get the resolution right, it has been a breeze to use since. I am jealous of Tom’s green screen for that full “like and subscribe” YouTube influencer experience.</p>
</li>
<li>
<p>After finally conceding that he was dangerously close to sporting a mullet, E gave C his very first haircut. It was an emotional experience but he is thankfully much less “business in the front, party in the back.”</p>
</li>
</ul>
Weeknotes 75https://mudge.name/2021/04/05/weeknotes-75/2021-04-05T10:47:00+00:002021-04-05T10:47:00+00:00Wasps, requiring Ruby 3.0, buses of all sizes, the finished Tree Delta-based importer and a surprising lullaby.<ul>
<li>
<p>Last year, my dad found a large wasps’ nest in the attic. To my astonishment, with the aid of a beekeeper hat and various toxic powders, he removed it.</p>
<p>One night this week while preparing C’s room for his night-time routine, I found a large, dozy wasp clinging to a window blind and sent E in to dispatch it. Sufficiently suspicious, I sent my dad back into the attic with a torch to see if they had returned.</p>
<p>They haven’t.</p>
</li>
<li>
<p>Working on a new Rails application, I wondered what the current popular choice for handling pagination with Active Record is (only being familiar with the venerable <a href="https://github.com/mislav/will_paginate"><code class="language-plaintext highlighter-rouge">will_paginate</code></a> and <a href="https://github.com/kaminari/kaminari">Kaminari</a>). <a href="https://medium.com/@scottm">Scott</a> mentioned <a href="https://github.com/ddnexus/pagy">Pagy</a> and I gave it a try.</p>
<p>Given <a href="https://github.com/mudge/re2#re2-">how I can be about compatibility in my gems</a>, I was surprised to see that <a href="https://github.com/ddnexus/pagy/blob/master/CHANGELOG.md#changes-1">Pagy 4 requires Ruby 3.0</a>. This is because <a href="https://bugs.ruby-lang.org/issues/16746">it uses Ruby 3.0’s “endless method” definition syntax which was initially proposed on April Fool’s Day 2020</a>, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">square</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">=</span> <span class="n">x</span> <span class="o">*</span> <span class="n">x</span>
</code></pre></div> </div>
<p>Perhaps next year we’ll see libraries using <a href="https://bugs.ruby-lang.org/issues/17768">downward assignment</a>.</p>
</li>
<li>
<p>We had a barbecue with E’s family in their back garden on Saturday and we baked <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2013/apr/17/how-make-perfect-bakewell-tart">Felicity Cloake’s bakewell tart</a> for the occasion.</p>
<p>It has been a long time since I have seen them and C delighted in exploring a box of old toy cars that E’s dad and his brothers played with when they were young. We attempted to explain the difference between cars, vans and lorries as he inspected each one.</p>
<p>This joy was dwarfed by the discovery that real, honest-to-goodness buses could be spotted at the junction at the end of the road. Seeing three double-deckers at once caused C to breathlessly shout “BU!” at the top of his voice.</p>
</li>
<li>
<p>I finished implementing <a href="/2021/03/28/weeknotes-74/">last week’s Tree Delta-based synchronisation system</a> ending up with the following classes:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">FromRoot</code>: the root of the tree of nodes that represent the current state of the database, the key method being <code class="language-plaintext highlighter-rouge">FromRoot#children</code> which returns an array of…</li>
<li><code class="language-plaintext highlighter-rouge">FromProductNode</code>: a node representing a single top-level product with its <code class="language-plaintext highlighter-rouge">parent</code> set to the <code class="language-plaintext highlighter-rouge">FromRoot</code>, its <code class="language-plaintext highlighter-rouge">identity</code> as a <code class="language-plaintext highlighter-rouge">ProductIdentity</code>, its <code class="language-plaintext highlighter-rouge">value</code> as a <code class="language-plaintext highlighter-rouge">ProductValue</code> and its <code class="language-plaintext highlighter-rouge">children</code> as an array of…</li>
<li><code class="language-plaintext highlighter-rouge">FromCategoryNode</code>: a node representing a category which can have its <code class="language-plaintext highlighter-rouge">parent</code> set to either a <code class="language-plaintext highlighter-rouge">FromProductNode</code> or another <code class="language-plaintext highlighter-rouge">FromCategoryNode</code>, its <code class="language-plaintext highlighter-rouge">identity</code> as a <code class="language-plaintext highlighter-rouge">CategoryIdentity</code>, its <code class="language-plaintext highlighter-rouge">value</code> as a <code class="language-plaintext highlighter-rouge">CategoryValue</code> and its <code class="language-plaintext highlighter-rouge">children</code> as either other <code class="language-plaintext highlighter-rouge">FromCategoryNode</code>s or a…</li>
<li><code class="language-plaintext highlighter-rouge">FromDocumentNode</code>: a node representing a document and its file upload, its <code class="language-plaintext highlighter-rouge">parent</code> is a <code class="language-plaintext highlighter-rouge">FromCategoryNode</code>, its <code class="language-plaintext highlighter-rouge">identity</code> is a <code class="language-plaintext highlighter-rouge">DocumentIdentity</code> and its <code class="language-plaintext highlighter-rouge">value</code> is a <code class="language-plaintext highlighter-rouge">DocumentValue</code> with no <code class="language-plaintext highlighter-rouge">children</code>.</li>
</ul>
<p>There is also a set of corresponding <code class="language-plaintext highlighter-rouge">ToRoot</code>, <code class="language-plaintext highlighter-rouge">ToProductNode</code>, <code class="language-plaintext highlighter-rouge">ToCategoryNode</code>, etc. classes that also use <code class="language-plaintext highlighter-rouge">ProductIdentity</code>, <code class="language-plaintext highlighter-rouge">CategoryValue</code>, etc. so they can be compared by <a href="https://github.com/whichdigital/tree_delta">Tree Delta</a>.</p>
<p>By returning dedicated type-specific <code class="language-plaintext highlighter-rouge">Value</code> objects (e.g. <code class="language-plaintext highlighter-rouge">DocumentValue</code>, <code class="language-plaintext highlighter-rouge">CategoryValue</code>), I’m able to define how Tree Delta compares them but also expose extra methods to the importer, e.g. so two documents can be compared without re-downloading the full uploaded file but there is a way to get the full file if needed.</p>
<p>The final piece of the puzzle was to implement my own <code class="language-plaintext highlighter-rouge">ProductOperation</code>, <code class="language-plaintext highlighter-rouge">CategoryOperation</code> and <code class="language-plaintext highlighter-rouge">DocumentOperation</code> classes to wrap <a href="https://github.com/whichdigital/tree_delta/blob/master/lib/tree_delta/operation.rb">Tree Delta’s own</a>. These each implement a single public <code class="language-plaintext highlighter-rouge">apply</code> method responsible for translating an operation such as “create a node with this <code class="language-plaintext highlighter-rouge">identity</code>, <code class="language-plaintext highlighter-rouge">parent</code> and <code class="language-plaintext highlighter-rouge">value</code>” into a database operation in my application. This means my top-level <code class="language-plaintext highlighter-rouge">Importer#import</code> method boils down to:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">import</span>
<span class="no">TreeDelta</span>
<span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">from: </span><span class="no">FromRoot</span><span class="p">.</span><span class="nf">new</span><span class="p">,</span> <span class="ss">to: </span><span class="no">ToRoot</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">client</span><span class="p">))</span>
<span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">operation</span><span class="o">|</span> <span class="no">Operation</span><span class="p">.</span><span class="nf">from</span><span class="p">(</span><span class="n">operation</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span><span class="nf">each</span><span class="p">(</span><span class="o">&</span><span class="ss">:apply</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>Due to a sequence of events that is difficult to explain, C now asks me to sing <a href="https://youtu.be/qM5W7Xn7FiA">2 Unlimited’s 1993 hit “No Limit”</a> while putting him to bed.</p>
</li>
</ul>
Weeknotes 74https://mudge.name/2021/03/28/weeknotes-74/2021-03-28T12:46:00+00:002021-03-28T12:46:00+00:00The fate of the AirPod, Calky, an ancient robot in the consultation room, a SOAP integration and ASCII trees.<ul>
<li>
<p>After drying in the sun, <a href="/2021/03/21/weeknotes-73/">the AirPod that went through the washing machine</a> is working fine.</p>
</li>
<li>
<p>Count me among <a href="https://twitter.com/search?q=https%3A%2F%2Fjvns.ca%2Fblog%2Flearn-how-things-work%2F&src=typeahead_click">the many others</a> recommending <a href="https://jvns.ca/blog/learn-how-things-work/">Julia Evans’ “Get better at programming by learning how things work”</a>.</p>
</li>
<li>
<p>C has developed a fondness for a large, solar-powered calculator. He calls it “Calky”.</p>
</li>
<li>
<p>My <a href="https://support.apple.com/en-gb/guide/notes/not9474646a9/mac">notes</a> for this week’s notes are blank.</p>
</li>
<li>
<p>C’s left eyelid became inflamed. As it didn’t improve after 24 hours, I ran the gauntlet of trying to see a GP in person.</p>
<p>My quest started with an 8 am call to the local GP (“all appointments are gone already, you’ll need to go to a pharmacy”), then to a pharmacy (“we can’t prescribe anything for children under 2, you’ll need to see a GP”), to the GP in person (“call at 2 pm for an emergency appointment”), to a call with <a href="https://111.nhs.uk">111</a> (“see a GP within 12 hours”), a call with the GP (“a doctor will call you this afternoon”) and ended with an in-person appointment at 4 pm.</p>
<p>The GP was lovely even when C identified a robot in the consultation room (actually an ancient, giant pair of weighing scales) and repeatedly attempted to pick them up.</p>
<p>He was prescribed some eye-drops (the application of which is a twice-daily adventure in itself) but is recovering well.</p>
</li>
<li>
<p>I’ve been working on an integration with a SOAP API for the first time in years. I’m using <a href="https://www.savonrb.com">Savon</a> to implement a client in Ruby to consume a list of documents organised into a hierarchy of categories and subcategories.</p>
<p>I need to keep the documents and their categorisation synchronised with the API and have been experimenting with <a href="https://github.com/whichdigital/tree_delta">Which? Digital’s “Tree Delta” gem</a>. If I can represent both copies of the document hierarchy as comparable trees of nodes, I can use the gem to produce the minimum set of operations to keep them in sync.</p>
</li>
<li>
<p>The <a href="https://github.com/whichdigital/tree_delta/blob/master/spec/black_box/add_and_delete_nodes_spec.rb">tests for Tree Delta</a> are wonderful, using <a href="https://github.com/tuzz/ascii_tree">Chris Patuzzo’s “ASCII Tree” gem</a> to parse trees out of ASCII art, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">AsciiTree</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s1">'
( a )
/ \
b c
/ \ / \
d e f g
'</span><span class="p">)</span>
</code></pre></div> </div>
<p>Having spent a lot of time trying to make testing various SOAP API calls as easy as possible without having to use a large number of XML fixtures, this dedication to making tests easy to read is an inspiration.</p>
</li>
</ul>
Weeknotes 73https://mudge.name/2021/03/21/weeknotes-73/2021-03-21T11:11:00+00:002021-03-22T08:33:00+00:00Rabbits, livecheck, how to stop clobbering files, relative font sizing, seemingly innocuous bugs and a single AirPod.<ul>
<li>
<p>For the past few weeks, we have woken to see rabbits in the garden. Sometimes a solitary one, sometimes a pair and, once, three happily munching on the lawn. Given C’s familiarity with <a href="https://www.bbc.co.uk/cbeebies/shows/peter-rabbit">Peter Rabbit</a>, we’ll lift him to the window and watch as he whispers “babu”.</p>
</li>
<li>
<p>I upgraded my standing desk from two piles of books about magic tricks and movie tie-ins to a <a href="https://vivo-us.com/collections/stand-up-desk-risers/products/desk-v000v">mechanical standing desk converter</a>.</p>
</li>
<li>
<p>I <a href="https://github.com/Homebrew/homebrew-cask/commits?author=mudge">occasionally contribute Homebrew Casks</a> and have been adding <a href="https://github.com/Homebrew/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/livecheck.md"><code class="language-plaintext highlighter-rouge">livecheck</code> stanzas</a>. This allows you to see when there are newer versions of formulae and casks upstream, e.g.</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>brew livecheck <span class="nt">--installed</span> <span class="nt">--newer-only</span>
</code></pre></div> </div>
</li>
<li>
<p><a href="https://github.com/thommay">Thom</a> told me about <a href="https://kitchenprojects.substack.com/?no_cover=true">Nicola Lamb’s recipe development and kitchen journal, Kitchen Projects</a>. I haven’t yet attempted any recipes but top of my list are <a href="https://kitchenprojects.substack.com/p/kitchen-project-16-xxl-cheese-scones">XXL Cheese Scones</a> and <a href="https://kitchenprojects.substack.com/p/kitchen-projects-003-nata">Pasteis de Nata</a>.</p>
</li>
<li>
<p>When importing database dumps into a local server for development, I typically run something like the following where <code class="language-plaintext highlighter-rouge">agiantdumpfile.sql</code> might be hundreds of megabytes in size:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>mysql <span class="nt">-u</span> root <span class="nt">-p</span> somedatabase < agiantdumpfile.sql
</code></pre></div> </div>
<p>All too often, I accidentally get the file redirection the wrong way around, e.g.</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>mysql <span class="nt">-u</span> root <span class="nt">-p</span> somedatabase <span class="o">></span> agiantdumpfile.sql
</code></pre></div> </div>
<p>Which instantly obliterates <code class="language-plaintext highlighter-rouge">agiantdumpfile.sql</code>, replacing it with an empty file.</p>
<p>I discovered <a href="http://zsh.sourceforge.net/Intro/intro_16.html">zsh’s <code class="language-plaintext highlighter-rouge">noclobber</code> option</a> which prevents this from happening:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>setopt noclobber
<span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'foo'</span> <span class="o">></span> bar
<span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'baz'</span> <span class="o">></span> bar
<span class="go">zsh: file exists: bar
</span></code></pre></div> </div>
<p>There’s a <a href="https://unix.stackexchange.com/questions/452865/are-there-any-disadvantages-of-setting-noclobber">great discussion on the Unix & Linux Stack Exchange about why <code class="language-plaintext highlighter-rouge">noclobber</code> isn’t the default setting</a>.</p>
</li>
<li>
<p>I had to fix a client’s website that didn’t respect <a href="https://support.google.com/chrome/answer/96810?co=GENIE.Platform%3DDesktop&hl=en#fontsize">Chrome’s font size setting</a>. While the Zoom function worked, increasing or decreasing the default font size did nothing to the text across the site.</p>
<p>Looking in the CSS, I discovered over 100 <a href="http://developer.mozilla.org/en-US/docs/Web/CSS/font-size"><code class="language-plaintext highlighter-rouge">font-size</code></a>s set in absolute pixels, overriding the browser settings.</p>
<p>To fix this without changing the appearance of the site for users who haven’t adjusted their settings, I converted all font sizes to be relative to the common browser default of <code class="language-plaintext highlighter-rouge">16px</code> by first setting the base font size as a percentage:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">html</span> <span class="p">{</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">68.75%</span><span class="p">;</span> <span class="c">/* 11/16 = 0.6875 */</span>
<span class="p">}</span>
</code></pre></div> </div>
<p>I then expressed all other font sizes in <a href="http://developer.mozilla.org/en-US/docs/Web/CSS/length#rem"><code class="language-plaintext highlighter-rouge">rem</code></a>s relative to that base <code class="language-plaintext highlighter-rouge">11px</code>:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">p</span> <span class="p">{</span>
<span class="c">/* font-size: 14px; */</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">1.27rem</span><span class="p">;</span> <span class="c">/* 14/11 = 1.272727273 */</span>
<span class="p">}</span>
<span class="nt">h1</span> <span class="p">{</span>
<span class="c">/* font-size: 20px; */</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">1.82rem</span><span class="p">;</span> <span class="c">/* 20/11 = 1.818181818 */</span>
<span class="p">}</span>
</code></pre></div> </div>
</li>
<li>
<p>I’ve also been working on a static site generated by <a href="https://www.gatsbyjs.com">Gatsby</a> and ran into strange jumbled up components (such as you might expect when you don’t set an appropriate <a href="https://reactjs.org/docs/lists-and-keys.html"><code class="language-plaintext highlighter-rouge">key</code></a>) when using <a href="https://www.gatsbyjs.com/docs/adding-app-and-website-functionality/">client-side functionality with React</a>.</p>
<p><a href="https://www.joshwcomeau.com/react/the-perils-of-rehydration/">Josh Comeau sums up the issue and a workaround perfectly</a> when it comes to <a href="https://www.gatsbyjs.com/docs/conceptual/react-hydration/">hydrating server-side React</a>.</p>
</li>
<li>
<p>I thoroughly enjoyed <a href="https://blog.teddykatz.com/2019/11/05/github-oauth-bypass.html">Teddy Katz’ “Bypassing GitHub’s OAuth flow”</a> and <a href="https://blog.teddykatz.com/2021/03/17/github-actions-write-access.html">“Stealing arbitrary GitHub Actions secrets”</a> as an insight into how serious security flaws are discovered via seemingly innocuous bugs.</p>
<p>Thankfully, <a href="https://github.com/presidentbeef/brakeman/issues/1432">Brakeman checks for “HTTP Verb Confusion”</a>.</p>
</li>
<li>
<p>Listening to <a href="https://www.adam-buxton.co.uk/podcasts/7-bfk9m-4l8kp-blcga-jwabs-blbb8-b9mjx-fj3gr-j4lyp-9jlhe-a4bhm-c67yy-szhgm-m8pcd-l376z-zjb32-wh9dr-za87b-c8l3j-byyhb-segba-hlsc6-5m648-aef2d-2n3wg-tpcha-fsb4a-stlxm-7hb5z-ekrdd-6hdmt-e2zpx">Adam Buxton interview fellow UEA alumnus Kazuo Ishiguro</a>, discussing science fiction, AI and the selfishness of apology was utterly fascinating.</p>
</li>
<li>
<p>My sister just handed me a single <a href="https://www.apple.com/uk/airpods/">AirPod</a> and said “I just found this in the washing machine.”</p>
</li>
</ul>
Weeknotes 72https://mudge.name/2021/03/14/weeknotes-72/2021-03-14T13:47:00+00:002021-03-14T13:47:00+00:00Very hot dough, all the toy cars, re2 1.3.0 and how do you solve a problem like WordPress?<ul>
<li>
<p>Happy Mother’s Day! <a href="/2020/03/22/weeknotes-21/">Or is that Mothering Sunday?</a></p>
</li>
<li>
<p>To celebrate, we made pizzas in our brand new <a href="https://uk.ooni.com/collections/ovens/products/ooni-karu">Ooni Karu 12 pizza oven</a>.</p>
<p class="center"><img src="/i/pizza.jpg" width="375" height="375" alt="" /></p>
<p>I used <a href="https://uk.ooni.com/blogs/recipes/cold-prove-pizza-dough?_pos=1&_sid=e98ca93aa&_ss=r">Ooni’s cold-proof pizza dough</a> and <a href="https://uk.ooni.com/blogs/recipes/classic-pizza-sauce?_pos=1&_sid=7b03e0a9b&_ss=r">pizza sauce recipe</a> for our first bake after watching <a href="https://youtu.be/joGKYTGbVw8">multiple</a> <a href="https://youtu.be/YzXtNBwXZEU">videos</a> reassuringly narrated with a lilting Scottish accent.</p>
<p>Despite the typical northern English drizzle, I’d say it was a success with even C happily working his way through an individual <a href="https://uk.ooni.com/blogs/recipes/pizza-margarita-classic-woodfired-recipe">margherita</a>.</p>
</li>
<li>
<p>What better way to chase a variety of pizzas fired in a 500 degree Celsius oven than with a slab of <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2013/may/16/how-bake-perfect-victoria-sponge-cake">traditional Victoria sponge cake</a>?</p>
</li>
<li>
<p>C’s enthusiasm for cars has reached a fever pitch with him insatiably demanding “<strong>more!</strong>” toy cars despite completely emptying my collection from when <a href="https://youtu.be/VKHFZBUTA4k">I were a lad</a>.</p>
</li>
<li>
<p>I <a href="https://github.com/mudge/re2/releases/tag/v1.3.0">released version 1.3.0 of the re2 gem</a> which makes it easier to install on <a href="https://support.apple.com/en-gb/HT211814">Apple Silicon Macs</a> using <a href="https://brew.sh">Homebrew</a>.</p>
<p>As Homebrew installs re2 into <code class="language-plaintext highlighter-rouge">/opt/homebrew</code> on Apple Silicon, I <a href="https://github.com/mudge/re2/commit/d1c4ad271716046575c8750ed5fbf35d4033aa5a">added that</a> to the default locations the gem searches when trying to find the <a href="https://github.com/google/re2">underlying re2 library</a>. Previously, you’d have to <a href="https://github.com/mudge/re2/issues/50">specify this yourself at install time</a>, e.g.</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gem <span class="nb">install </span>re2 <span class="nt">--</span> <span class="nt">--with-re2-dir</span><span class="o">=</span>/opt/homebrew
</code></pre></div> </div>
<p>But now the following should work instead:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gem <span class="nb">install </span>re2
</code></pre></div> </div>
</li>
<li>
<p>I’ve been torn about how best to handle <a href="https://wordpress.org">WordPress</a> for a client.</p>
<p>At <a href="https://www.altmetric.com">Altmetric</a>, I had always hoped to migrate our self-hosted WordPress install away to a third party such as <a href="https://wpengine.co.uk">WP Engine</a>, <a href="https://wpvip.com">WP VIP</a> or <a href="https://pantheon.io">Pantheon</a>. That way we could have pushed responsibility for keeping WordPress up, running and secure to someone else but still benefited from <a href="https://wpengine.com/support/development-workflow-best-practices/">version control and continuous deployment across multiple environments</a>.</p>
<p>However, such things do not come cheap unless you’re happy to let go of the continuous deployment side of things when something like <a href="https://wordpress.com">WordPress.com</a> seems hard to beat.</p>
<p>How then to balance the risk of hosting your own WordPress (with all its security implications) with trying to make it as cheap to maintain as possible? Especially considering that WordPress is decided non-<a href="https://12factor.net">Twelve-Factor</a>, storing its configuration in files and relying on being able to write to disk (e.g. for both configuration and caching).</p>
<p>Given that one extreme is to use some sort of managed service and simply have access to the WordPress admin panel (e.g. WordPress.com), what is the other? Is it possible to make WordPress Twelve-Factor and not spend all your time fighting it in the process?</p>
<p>I spiked a project which used <a href="https://github.com/johnpbloch/wordpress">John P. Bloch’s fork of WordPress</a> (which syncs with upstream every 15 minutes) and <a href="https://wpackagist.org">WordPress Packagist</a> to install WordPress, a theme and its plugins via <a href="https://getcomposer.org">Composer, the dependency manager for PHP</a>. There’s a <a href="https://composer.rarst.net/recipe/site-stack/">whole project example of this by Andrey Savchenko</a> for reference.</p>
<p>My <code class="language-plaintext highlighter-rouge">composer.json</code> looked like the following:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"repositories"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"composer"</span><span class="p">,</span><span class="w">
</span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://wpackagist.org"</span><span class="p">,</span><span class="w">
</span><span class="nl">"only"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"wpackagist-plugin/*"</span><span class="p">,</span><span class="w">
</span><span class="s2">"wpackagist-theme/*"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"require"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"johnpbloch/wordpress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^5.7"</span><span class="p">,</span><span class="w">
</span><span class="nl">"wpackagist-theme/twentytwentyone"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"wpackagist-plugin/akismet"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"wpackagist-plugin/w3-total-cache"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"wpackagist-plugin/amazon-s3-and-cloudfront"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.5"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"extra"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"installer-paths"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"public/app/plugins/{$name}/"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"type:wordpress-plugin"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"public/app/themes/{$name}/"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"type:wordpress-theme"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"wordpress-install-dir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public/wp"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div> </div>
<p>This installs WordPress into <code class="language-plaintext highlighter-rouge">public/wp</code>, plugins into <code class="language-plaintext highlighter-rouge">public/app/plugins</code> and a theme into <code class="language-plaintext highlighter-rouge">public/app/themes</code>. I then have my typical <a href="https://wordpress.org/support/article/editing-wp-config-php/"><code class="language-plaintext highlighter-rouge">wp-config.php</code></a> in <code class="language-plaintext highlighter-rouge">public</code>, pulling configuration from <a href="http://www.php.net/manual/en/reserved.variables.environment.php"><code class="language-plaintext highlighter-rouge">$_ENV</code></a> and disabling all automatic updates and file editing/modification by WordPress and its plugins. We use <a href="https://wordpress.org/plugins/w3-total-cache/">W3 Total Cache</a> to cache using <a href="https://redis.io">Redis</a> and <a href="https://wordpress.org/plugins/amazon-s3-and-cloudfront/">WP Offload Media Lite</a> to store any uploads in <a href="https://aws.amazon.com/s3/">Amazon S3</a> (or an <a href="https://deliciousbrains.com/s3-compatible-storage-provider-minio/">S3-compatible alternative</a>). The goal here is to make WordPress as <a href="https://12factor.net/processes">stateless</a> as possible without doing anything <em>too</em> out of the ordinary.</p>
<p>That’s all well and good but what about the actual app servers, database and Redis? Well, what about <a href="https://devcenter.heroku.com/articles/getting-started-with-php">Heroku’s PHP support</a>? By adding the following dependencies to our <code class="language-plaintext highlighter-rouge">composer.json</code>, we can tell Heroku which version of PHP we want and exactly <a href="https://make.wordpress.org/hosting/handbook/handbook/server-environment/">which extensions we need for WordPress</a>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"require"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"php"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.3.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-curl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-dom"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-exif"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-fileinfo"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-json"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-mbstring"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-mysqli"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-redis"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-sodium"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-openssl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-pcre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-imagick"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-xml"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ext-zip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div> </div>
<p>Our <code class="language-plaintext highlighter-rouge">Procfile</code> instructs Apache to serve our application out of <code class="language-plaintext highlighter-rouge">public</code>, respecting the <a href="https://wordpress.org/support/article/htaccess/#basic-wp">default WordPress <code class="language-plaintext highlighter-rouge">.htaccess</code></a> we have there:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">web</span><span class="pi">:</span> <span class="s">vendor/bin/heroku-php-apache2 public/</span>
</code></pre></div> </div>
<p>As Heroku terminate SSL, we have to tell WordPress requests are using HTTPS based on the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto"><code class="language-plaintext highlighter-rouge">X-Forwarded-Proto</code> header</a> in <code class="language-plaintext highlighter-rouge">wp-config.php</code>, e.g.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nb">strpos</span><span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTP_X_FORWARDED_PROTO'</span><span class="p">],</span> <span class="s1">'https'</span><span class="p">)</span> <span class="o">!==</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'HTTPS'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'on'</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div> </div>
<p>While Heroku don’t provide their own MySQL, we can lean on <a href="https://aws.amazon.com/rds/mariadb/">Amazon RDS for MariaDB</a> and <a href="https://devcenter.heroku.com/articles/amazon-rds">connect to it from Heroku</a> while using <a href="https://devcenter.heroku.com/articles/heroku-redis">Heroku Redis</a> for caching.</p>
<p>This way, we can benefit from <a href="https://devcenter.heroku.com/articles/pipelines">Heroku’s pipelines</a> and even <a href="https://devcenter.heroku.com/articles/github-integration-review-apps">review apps</a> to spin up new WordPress environments and test changes as you would for a Rails or Node.js application.</p>
<p>Missing out on automatic updates is the big problem here but I wonder if leaning on something like <a href="https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/">GitHub Dependabot</a> solves this as it does for other web applications, e.g. set aside time every Monday to review, test and deploy any updates to WordPress, your themes and any plugins.</p>
<p>I’m still not 100% decided (particularly as my client has a lot of existing content and uploads to migrate) but I’m tempted to try it.</p>
</li>
</ul>
Weeknotes 71https://mudge.name/2021/03/07/weeknotes-71/2021-03-07T11:55:00+00:002021-03-07T11:55:00+00:00Getting busted by Nintendo, who is fantastic when gymnastics is what you need, a greenfield Rails application and being spooked by Facebook.<ul>
<li>
<p>My <a href="/2021/02/28/weeknotes-70/">Joy-Con diagnosis</a> came back from Nintendo: “liquid damage main board”. Turns out C dunking it in a glass of water might not have been the best for its delicate electronics.</p>
</li>
<li>
<p>C surprised us one morning by saying “go go” when “<a href="https://www.bbc.co.uk/cbeebies/shows/go-jetters">Go Jetters</a>” came on the TV during breakfast (after family favourite “<a href="https://www.bbc.co.uk/cbeebies/shows/hey-duggee">Hey Duggee</a>” but before family least-liked “<a href="https://www.bbc.co.uk/cbeebies/shows/peter-rabbit">Peter Rabbit</a>”).</p>
<p>E and I now sing the theme song on demand.</p>
</li>
<li>
<p>I’ve been working on a brand new <a href="https://rubyonrails.org">Rails</a> application at work and it has been a long time since I had to build something customer-facing from scratch.</p>
<p>What test frameworks should I use? Should I still use <a href="https://cucumber.io">Cucumber</a>? What front-end framework should I be using? Should I <a href="https://getbootstrap.com/docs/4.1/getting-started/theming/">theme Bootstrap</a>? What <a href="https://github.com/rubocop/rubocop">Rubocop</a> configuration should I use? Should I use <a href="https://github.com/testdouble/standard">Standard</a>?</p>
<p>In the past, I’ve tried to reduce the amount of code I had to write (e.g. perhaps unthinkingly in the name of “<a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">Don’t repeat yourself</a>”), extracting as much as possible so I could whip up <a href="https://guides.rubyonrails.org/routing.html#crud-verbs-and-actions">resourceful controllers</a> with the fewest lines of code. Now though, I’m less concerned about optimising for that initial writing and more concerned about making the code easier to read, change and <a href="https://programmingisterrible.com/post/139222674273/how-to-write-disposable-code-in-large-systems">delete</a>.</p>
<p>The gamble I’m taking is that it might take a little longer to write at first but it’ll be much easier to understand and change in future.</p>
<p>Perhaps I’m getting more cynical about my ability to predict the <a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction">right abstraction</a>. As <a href="https://sandimetz.com">Sandi Metz</a> said:</p>
<blockquote>
<p>Prefer duplication over the wrong abstraction.</p>
</blockquote>
<p>I’m reminded of <a href="https://leanpub.com/wewut">Jay Fields’ “Working Effectively with Unit Tests”</a>:</p>
<blockquote>
<p>[Test] code and production code is written, maintained, and reviewed in
drastically different ways. Production code collaborates to provide a
single running application, and it’s generally wise to avoid duplicating
concepts within that application. Tests do not, or at least should not
collaborate; it’s universally accepted that inter-test dependency is an
anti-pattern. If we think of tests as tiny, independent universes, then
code that appears in one test should not necessarily be considered
inadvisable duplication if it appears in another test as well.</p>
</blockquote>
<p>I wonder if it is also useful to think of separate parts of your application as “tiny, independent universes” too.</p>
</li>
<li>
<p>I had to sign up for a Facebook account in order to fix a problem with Instagram embeds and, despite providing only my name and a work email address (which no one else would have in their contacts), Facebook suggested I add people I know in real life.</p>
<p>I was spooked until I realised <a href="https://nakedsecurity.sophos.com/2019/12/23/facebook-will-stop-mining-contacts-with-your-2fa-number/">Facebook use the phone number you give for 2-factor authentication to look you up in others’ contacts</a>.</p>
</li>
</ul>
Weeknotes 70https://mudge.name/2021/02/28/weeknotes-70/2021-02-28T14:37:00+00:002021-02-28T16:54:00+00:00Fixing Joy-Con drift, a surprise encounter with my favourite type of duck, parsing Data URLs and a wardrobe malfunction.<ul>
<li>
<p>After having <a href="/2020/01/26/weeknotes-13/">a drifting left Joy-Con for over a year</a>, I finally sent <a href="https://www.nintendo.co.uk/Support/Nintendo-Switch/Troubleshooting/Joy-Con-Control-Sticks-Are-Not-Responding-or-Respond-Incorrectly-1908347.html">it to Nintendo for repair</a>.</p>
<p>With the Switch out of action, I signed up for a free trial of <a href="https://www.apple.com/uk/apple-arcade/">Apple Arcade</a> and have so far downloaded “<a href="https://whatthegolf.com">What the Golf?</a>, “<a href="https://www.monsterexpedition.com">A Monster’s Expedition (Through Puzzling Exhibitions)</a>” and “<a href="https://simogo.com/work/sayonara-wild-hearts/">Sayonara Wild Hearts</a>”.</p>
<p>I’ve only played “What the Golf?” so far and it is wonderful.</p>
</li>
<li>
<p>My mum and I took C to a nearby park to feed the ducks (<a href="https://www.theguardian.com/world/2019/oct/18/feed-the-ducks-bread-sign-sparks-heated-online-debate">oats, to avoid controversy</a>) and saw a raft of glorious <a href="https://en.wikipedia.org/wiki/Mandarin_duck">Mandarin ducks</a>.</p>
<p class="center"><img src="/i/mandarin-duck.jpg" width="375" height="375" alt="" /></p>
<p>Along with the <a href="https://www.rspb.org.uk/birds-and-wildlife/wildlife-guides/bird-a-z/red-kite/">red kites</a> we’ve seen in the garden, they certainly make a change from <a href="https://en.wikipedia.org/wiki/Feral_parakeets_in_Great_Britain">London’s feral parakeets</a>.</p>
</li>
<li>
<p>I had to parse a CSV file sent as a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs">Data URL</a> this week and converting a <a href="https://tools.ietf.org/html/rfc2397">Request for Comments</a> into a regular expression is a great joy in life:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">REGEXP</span> <span class="o">=</span> <span class="sr">%r{
data:
(?<mediatype>
(?<mimetype> .+? / .+? )?
(?<parameters> ( ; .+? = .+? )* )
)?
(?<extension>;base64)?
,
(?<data>.*)
}x</span><span class="p">.</span><span class="nf">freeze</span>
</code></pre></div> </div>
<p>You can find a <a href="https://gist.github.com/mudge/1f958ec848b6c31e287baa7a60a88064">full Ruby implementation on GitHub</a>.</p>
</li>
<li>
<p>I learnt about <a href="https://guides.rubyonrails.org/active_record_migrations.html#reverting-previous-migrations">Active Record’s ability to rollback migrations using the <code class="language-plaintext highlighter-rouge">revert</code> method</a> which came in very handy when amending a unique index:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require_relative</span> <span class="s1">'20210211104247_add_unique_index_to_product_links'</span>
<span class="k">class</span> <span class="nc">ChangeUniqueIndexOnProductLinks</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">6.1</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">revert</span> <span class="no">AddUniqueIndexToProductLinks</span>
<span class="n">add_index</span><span class="p">(</span><span class="ss">:product_links</span><span class="p">,</span> <span class="p">[</span><span class="ss">:product_id</span><span class="p">,</span> <span class="ss">:reseller_id</span><span class="p">,</span> <span class="ss">:country_id</span><span class="p">],</span> <span class="ss">unique: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">name: </span><span class="s1">'by_product_reseller_country'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>An overgrown bush had begun to crowd the patio so my mum and I decided to cut it back. She disappeared into the garage and returned with a truly horrifying but <a href="/2021/02/14/weeknotes-68/">brilliantly effective single-purpose tool</a>: a telescopic branch cutter.</p>
</li>
<li>
<p>In the footsteps of <a href="https://www.theverge.com/tldr/2017/5/30/15711662/netflix-house-of-cards-season-5-running-playlist">other power couples</a>, E and I went for our first run together yesterday. My legs now ache and, about 5 minutes into the run, my shorts nearly fell off.</p>
</li>
</ul>
Weeknotes 69https://mudge.name/2021/02/21/weeknotes-69/2021-02-21T10:21:00+00:002021-02-21T14:22:00+00:00A lot of hours writing YAML, pancake day, channelling Elon Musk and product names that maximise embarrassment in Homebase.<ul>
<li>
<p>I spent a long time <a href="https://github.com/mudge/re2/pull/47">rewriting re2’s CI configuration to test against Ruby versions 1.8 through to 3.0 and all six ABI versions</a> of the <a href="https://github.com/google/re2">underlying Google re2 library</a> using <a href="https://github.com/features/actions">GitHub Actions</a>.</p>
<p>My <a href="https://github.com/mudge/re2-test-action">previous setup</a>, based on using the <a href="https://www.brightbox.com/docs/ruby/ubuntu/">Brightbox Ruby packages for Ubuntu</a> and the <a href="http://rubinius-binaries-rubinius-com.s3-us-west-2.amazonaws.com/index.txt">official Rubinius binary releases</a>, <a href="https://github.com/mudge/re2/runs/1927274557?check_suite_focus=true">stopped working</a> when I ran it for the first time in ten months.</p>
<p>Thankfully, <a href="https://github.com/rspec/rspec-core">RSpec’s <code class="language-plaintext highlighter-rouge">rspec-core</code></a> <a href="https://github.com/rspec/rspec-core/blob/main/.github/workflows/ci.yml">also uses GitHub Actions to test against legacy Ruby versions</a> and their <a href="https://github.com/rspec/docker-ci">custom Docker images</a> were the basis for <a href="https://github.com/mudge/re2-ci">my own</a>.</p>
<p>I hope that by switching to the <a href="https://github.com/ruby/setup-ruby">official Ruby <code class="language-plaintext highlighter-rouge">setup-ruby</code> GitHub Action</a>, it will prevent further <a href="https://en.wikipedia.org/wiki/Software_rot">bit rot</a> but I suspect the <a href="https://github.com/mudge/re2/blob/main/.github/workflows/tests.yml#L54">necessary</a> <a href="https://github.com/mudge/re2-ci/blob/main/1.8/Dockerfile#L21">workarounds</a> for legacy Ruby versions will only get worse over time (see <a href="https://jcs.org/2021/01/06/plaintext">Joshua Stein’s “Plaintext HTTP in a Modern World”</a>).</p>
<p>I hoped this work would mean I could add <a href="https://github.com/oracle/truffleruby">TruffleRuby</a> as a supported version of Ruby but alas, <a href="https://github.com/oracle/truffleruby/issues/2262">it doesn’t seem to work with Ubuntu on GitHub Actions</a>.</p>
</li>
<li>
<p>We celebrated <a href="https://en.wikipedia.org/wiki/Shrove_Tuesday">Pancake Day</a> which doubles as the anniversary of C first eating solid food. While he demonstrated what difference a year makes, we introduced the concept of savoury pancakes to my sceptical parents.</p>
</li>
<li>
<p>I dropped the ball from my <a href="https://www.kensington.com/en-gb/p/products/control/trackballs/expert-mouse-wired-trackball/">trackball</a> onto the sheet of smoked glass covering my desk, accidentally recreating the <a href="https://youtu.be/LMWwImDX3ks">Tesla Cybertruck bulletproof window demo</a>.</p>
</li>
<li>
<p>I enjoyed sending my mum to the hardware shop to buy a specific adhesive to fix a broken window blind: “<a href="https://trade.evo-stik.co.uk/products/grab-adhesives/sticks-sht">Sticks Like Sh*t</a>”.</p>
</li>
</ul>
Weeknotes 68https://mudge.name/2021/02/14/weeknotes-68/2021-02-14T13:35:00+00:002021-02-14T13:35:00+00:00Hoarding tools, the trap of doing jobs and a new buzzword.<ul>
<li>
<p>The kitchen tap has been dripping for a week.</p>
<p>The replacement cartridge valves we ordered arrived quickly but the 14mm socket needed to remove the existing cartridge was delayed by a week.</p>
<p>Impatient and sick of the constant drip-drip-drip of the tap, my mum and E separately went in search of the missing part, returning home with various bits and pieces from local hardware shops.</p>
<p>We have ended up with:</p>
<ul>
<li>A 14mm ½ inch drive socket.</li>
<li>A 14mm ¼ inch drive socket.</li>
<li>A set of hexagonal things I do not recognise but now think might be Allen keys for a socket set.</li>
<li>A whole new socket set that goes up to 14mm but has sockets too shallow to remove a cartridge valve.</li>
<li>A ½ inch drive ratchet handle.</li>
</ul>
</li>
<li>
<p>The tap is now fixed.</p>
</li>
<li>
<p><a href="https://twitter.com/kentbeck/status/250733358307500032?lang=en">Kent Beck famously said</a>:</p>
<blockquote>
<p>for each desired change, make the change easy (warning: this may be hard), then make the easy change</p>
</blockquote>
<p>I would add the corollary:</p>
<blockquote>
<p>for each desired DIY job, find the tools to make the job easy (warning: this may be hard), then do the easy job.</p>
</blockquote>
</li>
<li>
<p>While hoarding the aforementioned tools, I was also attempting to complete multiple lengthy client questionnaires from our conveyancer, <a href="https://www.gov.uk/applying-for-probate/apply-for-probate">finish applying for probate</a>, mend C’s bedroom door (which doesn’t fully close) and retrieve two table football balls from my dad’s home cinema subwoofer.</p>
<p>While completing the probate application with my mum, I realised I had fallen into a familiar trap: trying to be useful to those around by busying myself with odd jobs (both DIY and administrative).</p>
<p>In reality:</p>
<ul>
<li>I was stressing myself out.</li>
<li>I was absent from the people I was trying (and failing) to help.</li>
</ul>
</li>
<li>
<p>I’ve given up on the table football balls in the subwoofer.</p>
</li>
<li>
<p><a href="https://twitter.com/Stew/status/1358766649105649678">Euan’s latest creation</a> is absolutely wonderful.</p>
</li>
<li>
<p>Having taken responsibility for some projects written using <a href="https://www.gatsbyjs.com">Gatsby</a> at <a href="https://www.ghostcassette.com">work</a>, you will now find me using the term “<a href="https://jamstack.org">jamstack</a>” in emails.</p>
</li>
</ul>
Weeknotes 67https://mudge.name/2021/02/07/weeknotes-67/2021-02-07T18:14:00+00:002021-02-07T18:14:00+00:00Bidding our flat farewell, attempting to apply for probate and a new reason to tease me.<ul>
<li>
<p>Following <a href="/2021/01/31/weeknotes-66/">our decision</a>, E and I signed terms with an estate agent, booked in a photographer and opened a file with a conveyancer. We <a href="/2020/12/21/weeknotes-59-and-60/">left our flat in a hurry</a> so we decided to return to prepare it for sale and bid it fare well.</p>
<p class="center"><img src="/i/flat.jpg" width="375" height="282" /></p>
<p>With E as a temporary driver on my dad’s oversized car, we drove down and set to work removing all the clutter essential for living (particularly with a toddler) but unsightly to prospective buyers. E filled two suitcases with belongings as I pulled up weeds and removed an old planter full of dead flowers from our patio. It was a task that was literally years overdue.</p>
<p>I cannot unsee what I found on the underside of long-neglected bags of compost.</p>
<p>E ordered what may well be our last <a href="https://yardsalepizza.com">Yard Sale Pizza</a> and I slept poorly, my mind racing with remaining jobs to do. By late morning, we’d done all we could, leaving a gooseneck kettle and a toaster on the kitchen worktop.</p>
</li>
<li>
<p>I’ve been helping <a href="https://www.moneysavingexpert.com/family/guide-to-probate/">apply for probate</a> and, my goodness, for the sake of your surviving relatives: please maintain a single, up-to-date list of all your assets. Every time we think we might be getting somewhere, a slip of undated paper is found that throws everything into doubt.</p>
</li>
<li>
<p>During the latest attempt, I received a notification on my phone that my beverage had reached my “perfect temperature”. That’s right, I indulged in an <a href="https://ember.com">Ember mug</a>.</p>
<p>This was mostly due to <a href="https://allconsuming.show/listen/ember">Noah Kalina and Adam Lisagor’s review</a>. As you can tell from <a href="https://youtu.be/8pPCMjqI_uw">James Hoffmann’s comments</a>, this is obviously an absurd luxury item (let’s not even mention <a href="https://www.independent.co.uk/news/uk/politics/rishi-sunak-smart-mug-ember-coffee-week-minimum-wage-jobs-a9607341.html">Rishi Sunak’s unfortunate photo op</a>):</p>
<blockquote>
<p>I’ll be honest, owning one of these will mean that your colleagues, friends and loved ones will mock you mercilessly for owning an incredibly expensive smart mug that holds coffee at a particular temperature of your choosing.</p>
<p>[…]</p>
<p>You should absolutely tease anyone who spent that much money on one of these… <em>but they’re also having a kind of a good time with it.</em></p>
</blockquote>
</li>
</ul>
Weeknotes 66https://mudge.name/2021/01/31/weeknotes-66/2021-01-31T15:35:00+00:002021-02-01T16:37:54+00:00Project managing life decisions and what even is pudding?<ul>
<li>
<p>My sister and I baked <a href="https://thegreatbritishbakeoff.co.uk/recipes/all/liam-charles-mega-choccy-stack/">Liam Charles’ “Mega-Choccy Stack”</a> from the latest series of <a href="https://www.channel4.com/programmes/junior-bake-off">Junior Bake Off</a>. We didn’t have any double cream so I used a sachet of <a href="https://www.birdscustard.co.uk/#">Bird’s</a> Dream Topping instead.</p>
<p class="center"><img src="/i/choccystack.jpg" width="375" height="337" /></p>
</li>
<li>
<p>I’ve created a <a href="https://www.trello.com">Trello</a> board called “Leaving London”.</p>
</li>
<li>
<p>I don’t know how to write about our decision to leave a city I have called home for 12 years.</p>
<p><a href="https://tomstu.art/weeknotes-30-on-pause">Others have written about the peculiar feeling that your life is on pause</a> but I hadn’t really thought about it until we started listing all the things we need to do to sell our flat.</p>
<blockquote>
<p>At the time of writing my life is currently happening!</p>
</blockquote>
<p>Perhaps I haven’t really believed this. That I could live <a href="/2021/01/17/weeknotes-64/">five minutes at a time</a> until “real life” could resume.</p>
<p>That, yes, we have outgrown our flat and, yes, want to be closer to family and, yes, we’re both working remotely and it seems to be going well. None of that ever connected with: right, let’s sell our flat, move our belongings and <a href="https://www.royalmail.com/personal/receiving-mail/redirection">redirect our mail</a>.</p>
<p>I originally moved to London on little more than a whim so perhaps I shouldn’t be surprised that leaving would be unceremonious too.</p>
</li>
</ul>
Weeknotes 65https://mudge.name/2021/01/24/weeknotes-65/2021-01-24T15:10:00+00:002021-01-24T15:10:00+00:00Separating out weeknotes, scaling a Rails application, plugging a memory leak and a little boat, no bigger than your hand.<ul>
<li>
<p>Taking inspiration from <a href="https://rowanmanning.com">Rowan</a> and <a href="https://alicebartlett.co.uk/blog/programming-note">Alice</a>, I’ve separated my weeknotes (and <a href="/2019/01/02/2018-yearnotes/">solitary yearnotes</a>) from my other, less frequent posts (e.g. “<a href="/2019/11/12/using-a-raspberry-pi-for-time-machine/">Using a Raspberry Pi for Time Machine</a>”) on the <a href="/">homepage</a>.</p>
<p>As well as the existing <a href="/index.atom">combined Atom feed</a>, there are now separate feeds for <a href="/posts.atom">posts</a> and <a href="/weeknotes.atom">weeknotes</a> if you only care to subscribe to one.</p>
</li>
<li>
<p>In preparation for a <a href="https://www.raspberrypi.org/blog/raspberry-pi-silicon-pico-now-on-sale/">launch</a> and an expected burst of traffic this week, I worked on automatically scaling a <a href="https://rubyonrails.org">Rails</a> application hosted on <a href="https://www.heroku.com">Heroku</a>.</p>
<p>Heroku provide their <a href="https://devcenter.heroku.com/articles/scaling#autoscaling">own autoscaling</a> but only for their <a href="https://devcenter.heroku.com/articles/dyno-types">“performance” tier of dynos</a> which are 10 times the price of their standard ones.</p>
<p>Thankfully, they recommend third-party add-ons such as <a href="https://railsautoscale.com">Rails Autoscale</a> if you’re not using performance dynos or if your app has variable response times (as most do). I hadn’t heard of Rails Autoscale before but <a href="https://www.youtube.com/watch?v=hkZgpc3BD4E">the demo</a> was very compelling. Its use of <a href="https://help.heroku.com/QB0BKTNJ/is-it-normal-for-requests-to-spend-a-long-time-in-request-queuing-as-reported-by-newrelic">Heroku “request queueing” time</a> rather than response time makes it especially useful when your app might have the occasional spike in response time without impacting its ability to serve requests to other clients.</p>
<p><a href="https://railsautoscale.com/how-many-dynos/">Adam McCrea’s “How Many Heroku Dynos Do You Need, and Which Size—An Opinionated Guide”</a> is a fantastic guide and led me to look into “<a href="https://railsautoscale.com/how-many-dynos/#heroku-routing-and-in-dyno-concurrency">in-dyno concurrency</a>” with <a href="https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#workers">multiple Puma workers</a> rather than relying solely on <a href="https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#threads">threads</a>.</p>
<p>However, as soon as I bumped the number of workers up for my application, a slow memory leak became much more pronounced with my app peaking at 131.5% of its memory quota.</p>
</li>
<li>
<p>Fortunately, I stumbled across <a href="https://www.spacevatican.org/2019/5/4/debugging-a-memory-leak-in-a-rails-app/">Frederick Cheung’s “Debugging a Memory Leak in a Rails App”</a> which not only described how to find memory leaks in running Rails applications using <a href="https://github.com/tmm1/rbtrace">rbtrace</a> and <a href="https://samsaffron.com/archive/2015/03/31/debugging-memory-leaks-in-ruby">Sam Saffron’s excellent guide</a> but described a <a href="https://github.com/rails/rails/issues/14301">potential memory leak in Rails’ <code class="language-plaintext highlighter-rouge">prepend_view_path</code></a>.</p>
<p>What did I find in a <a href="http://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-before_action"><code class="language-plaintext highlighter-rouge">before_action</code></a> in my Application Controller? Something like the following:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">prepend_subdomain_specific_templates</span>
<span class="n">prepend_view_path</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'app'</span><span class="p">,</span> <span class="s1">'views'</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="nf">subdomain</span><span class="p">))</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>As Frederick points out, prepending a <code class="language-plaintext highlighter-rouge">String</code> or <a href="https://ruby-doc.org/stdlib-2.7.2/libdoc/pathname/rdoc/Pathname.html"><code class="language-plaintext highlighter-rouge">Pathname</code></a> to the view path <a href="https://github.com/rails/rails/blob/6-1-stable/actionview/lib/action_view/path_set.rb#L84">causes Rails to initialize a new Action View <code class="language-plaintext highlighter-rouge">Resolver</code></a> for every request. This would be fine as Ruby should garbage collect the <a href="http://api.rubyonrails.org/classes/ActionView/Resolver.html"><code class="language-plaintext highlighter-rouge">Resolver</code></a>s when they are no longer used but Rails’ template caching means that each <code class="language-plaintext highlighter-rouge">Resolver</code> defines methods whose names are never collected, causing a slow but steady leak. If you’re calling <a href="http://api.rubyonrails.org/classes/ActionView/ViewPaths.html#method-i-prepend_view_path"><code class="language-plaintext highlighter-rouge">prepend_view_path</code></a> on a particularly heavily trafficked route, this leak will become more pronounced.</p>
<p>The fix in my situation was to initialize a <code class="language-plaintext highlighter-rouge">Resolver</code> per subdomain once and then re-use the same <code class="language-plaintext highlighter-rouge">Resolver</code> whenever possible, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">SUBDOMAIN_SPECIFIC_TEMPLATES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'subdomain1'</span> <span class="o">=></span> <span class="no">ActionView</span><span class="o">::</span><span class="no">OptimizedFileSystemResolver</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'app/views/subdomain1'</span><span class="p">),</span>
<span class="s1">'subdomain2'</span> <span class="o">=></span> <span class="no">ActionView</span><span class="o">::</span><span class="no">OptimizedFileSystemResolver</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'app/views/subdomain2'</span><span class="p">),</span>
<span class="s1">'subdomain3'</span> <span class="o">=></span> <span class="no">ActionView</span><span class="o">::</span><span class="no">OptimizedFileSystemResolver</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'app/views/subdomain3'</span><span class="p">)</span>
<span class="p">}.</span><span class="nf">freeze</span>
<span class="k">def</span> <span class="nf">prepend_subdomain_specific_templates</span>
<span class="n">prepend_view_path</span><span class="p">(</span><span class="no">SUBDOMAIN_SPECIFIC_TEMPLATES</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="nf">subdomain</span><span class="p">))</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>With the fix deployed, the resulting memory usage graph made me very happy indeed.</p>
<p class="center"><img src="/i/leak.png" width="500" height="70" alt="" /></p>
</li>
<li>
<p>I’ve been rightly chastised for <a href="/2020/08/23/weeknotes-43/">using the trope of interpreting children’s TV shows through the eyes of a jaded adult</a> before but I’m genuinely enjoying “<a href="https://www.bbc.co.uk/cbeebies/shows/in-the-night-garden">In the Night Garden…</a>”.</p>
<p>Watching C’s excitement build as we watch <a href="https://www.inthenightgarden.co.uk/about-the-show/igglepiggle">Igglepiggle</a> lie down in his boat, covering himself with its sail as we pan up from the rolling ocean to see the stars of the night sky bloom into flowers is a delight.</p>
<p>The <a href="http://www.bbc.co.uk/pressoffice/pressreleases/stories/2007/03_march/19/cbeebies_garden.shtml">creators’ intentions behind the series</a> is interesting too:</p>
<blockquote>
<p>“We became very aware of the anxiety surrounding the care of young children which manifested itself in all kind of directions – but the one big subject that came up again and again was bedtime. It’s the classic time for tension between children who want to stay up and parents who want them to go to bed.</p>
<p>“We wanted to explore the difference between being asleep and being awake from a child’s point of view: the difference between closing your eyes and pretending to be asleep and closing your eyes and sleeping.</p>
<p>“So this is a programme about calming things down whereas most children’s TV is about gee-ing everything up!”</p>
</blockquote>
</li>
</ul>
Weeknotes 64https://mudge.name/2021/01/17/weeknotes-64/2021-01-17T11:29:00+00:002021-01-17T11:29:00+00:00Playing old adventure games, extracting subdirectories from git repositories, programming robots, a photo book follow-up and more than five minutes ahead.<ul>
<li>
<p>After reading <a href="https://www.dropbox.com/s/ojb9zaplszjcp63/FULL%20THROTTLE%20-for%20all.pdf?dl=0">Duncan Jones’ script for “Full Throttle”</a>, I replayed the <a href="https://en.wikipedia.org/wiki/Full_Throttle_(1995_video_game)">original 1995 LucasArts adventure game</a> using <a href="https://www.scummvm.org">ScummVM</a> having <a href="https://wiki.scummvm.org/index.php?title=Datafiles#Full_Throttle">copied the files from my original CD-ROMs</a>.</p>
<p>I wanted to do the same with <a href="https://en.wikipedia.org/wiki/Broken_Sword">Broken Sword</a> 1 and 2 but struggled to find a computer in the house with a disc drive. I ended up using my grandad’s abandoned <a href="https://www.asus.com/uk/">Asus</a> laptop and <a href="https://en.wikipedia.org/wiki/Server_Message_Block">SMB</a> file sharing to get the <a href="https://wiki.scummvm.org/index.php?title=Datafiles#Broken_Sword:_The_Shadow_of_the_Templars">files</a> onto my Mac.</p>
</li>
<li>
<p>I’ve been thoroughly enjoying <a href="https://www.youtube.com/playlist?list=PLaDrN74SfdT7Ueqtwn_bXo1MuSWT0ji2w">Brian David Gilbert’s “Unraveled” series for Polygon</a> (now <a href="https://www.polygon.com/videos/2020/12/28/22195388/pokemon-edibility-unraveled">complete</a>), e.g. “<a href="https://youtu.be/Zb4CxTTFDfA">Which Dark Souls boss is the best manager?</a>”</p>
</li>
<li>
<p><a href="https://twitter.com/KushalP/status/1346958399972499457">Kushal’s tweet</a> introduced me to the term “<a href="https://en.wikipedia.org/wiki/Doomscrolling">doomscrolling</a>” and, perhaps it is the <a href="https://en.wikipedia.org/wiki/Frequency_illusion">Baader-Meinhof phenomenon</a> but I find myself doing it a <em>lot</em>.</p>
</li>
<li>
<p>I needed to extract a custom WordPress theme from a subdirectory in a git repository while preserving its git history. I first tried using <a href="https://git-scm.com/docs/git-filter-branch"><code class="language-plaintext highlighter-rouge">git filter-branch</code></a> but <a href="https://patchwork.kernel.org/project/git/patch/20190904223239.571-3-newren@gmail.com/">it told me</a> to use <a href="https://github.com/newren/git-filter-repo"><code class="language-plaintext highlighter-rouge">git filter-repo</code></a> instead.</p>
<p>I was able to extract the subdirectory as well as some top-level files and rewrite some hard-coded paths all with a single command:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git filter-repo <span class="nt">--path</span> .stylelintrc <span class="nt">--path</span> .stylelintignore <span class="nt">--path</span> .phpcs.xml <span class="nt">--path</span> package.json <span class="nt">--path</span> package-lock.json <span class="nt">--path</span> gulpfile.js <span class="nt">--path</span> .browserslistrc <span class="nt">--path</span> .eslintrc <span class="nt">--path</span> .prettierrc <span class="nt">--path</span> wp-content/themes/custom-theme/ <span class="nt">--path</span> assets/ <span class="nt">--path-rename</span> wp-content/themes/custom-theme/: <span class="nt">--path-rename</span> assets/: <span class="nt">--replace-text</span> ../replacements.txt
</code></pre></div> </div>
</li>
<li>
<p>We got our very first robot vacuum cleaner and watching it navigate takes me back to 2004 and <a href="https://github.com/mudge/lego_bulldozer">my adventures programming LEGO MINDSTORMS</a> with <a href="http://bricxcc.sourceforge.net/nqc/">Not Quite C</a>.</p>
</li>
<li>
<p>I finally managed to solve <a href="/2021/01/10/weeknotes-63/">my photo book and iCloud Photos syncing problems</a>:</p>
<ol>
<li>I restored my working (but outdated) photo library from a backup.</li>
<li>I <a href="https://support.apple.com/en-gb/HT208243">logged out of iCloud</a> altogether on my Mac.</li>
<li>I <a href="https://support.apple.com/en-gb/HT208682#macos">logged back into iCloud</a>.</li>
<li>I tried to enable iCloud Photos.</li>
<li>I encountered a <a href="https://discussions.apple.com/docs/DOC-13744">baffling error that “iCloud Photos is only available on macOS Standard, macOS Extended and APFS formatted volumes”</a>.</li>
<li>I <a href="https://support.apple.com/en-gb/HT201314">restarted into macOS Recovery</a>.</li>
<li>I ran <a href="https://support.apple.com/en-gb/guide/disk-utility/dskutl1040/mac">First Aid</a> on my Big Sur volumes.</li>
<li>I restarted.</li>
<li>I successfully enabled iCloud Photos, accepting its warning that I might run out of storage and watched as it began to re-upload all ~37,000 photos and videos in my library.</li>
<li>Days passed.</li>
<li>The update completed!</li>
</ol>
<p>I now have my latest photos <em>and</em> my Mimeo Photos project is intact.</p>
</li>
<li>
<p>I’ve been reading <a href="https://en.wikipedia.org/wiki/Northern_Lights_(novel)">Philip Pullman’s “Northern Lights”</a> (after enjoying the <a href="https://www.bbc.co.uk/programmes/m000b1v2">BBC TV adaptation</a>) and this quote from Roger felt a bit close to home:</p>
<blockquote>
<p>There en’t been nothing good more than about five minutes ahead. Like I can see now, this bath’s nice, and there’s a nice warm towel there, about five minutes away. And once I’m dry, maybe I’ll think of summing nice to eat, but no further ahead than that. And when I’ve eaten, maybe I’ll look forward to a kip in a comfortable bed. But after that, I dunno, Lyra. There’s been terrible things we seen, en’t there? And more a coming, more’n likely. So I think I’d rather not know what’s in the future. I’ll stick to the present.</p>
</blockquote>
</li>
</ul>
Weeknotes 63https://mudge.name/2021/01/10/weeknotes-63/2021-01-10T15:16:00+00:002021-01-10T15:16:00+00:00Arctic scenes, pointless defiance, networking problems and photo books.<ul>
<li>
<p>I haven’t seen snow like this in <a href="/2019/01/02/2018-yearnotes/">years</a>.</p>
<p class="center"><img src="/i/lane.jpg" width="375" height="282" alt="" /></p>
</li>
<li>
<p>I’m going to try to keep this quick as I want to try out my <a href="/2021/01/03/weeknotes-62/">new running shoes</a> before C wakes up. The feeling of having eaten too much at every meal is becoming far too familiar.</p>
</li>
<li>
<p>C has various place mats, posters and books that all feature <a href="https://en.wikipedia.org/wiki/Cupcake">cupcakes</a> for the letter C. In an act of pointless defiance, I am insisting on calling them “buns” instead.</p>
</li>
<li>
<p>My parents’ house is shaped like one long corridor so I bought them a <a href="https://shop.bt.com/learnmore/bt-branded-products-and-services/bt-whole-home-wi-fi/">BT Whole Home Wi-Fi</a> system a few years ago. Now there are three of us working from home (not to mention my dad’s binge-watching of any Netflix show that has more than three seasons), we’re especially vulnerable to any problems with the internet.</p>
<p>The day after upgrading their router to the <a href="https://shop.bt.com/products/bt-smart-hub-2-097683-DYNK.html">hub</a> BT insisted on sending them, we started to have problems. I spent that evening meticulously re-arranging the various nodes in the mesh network, trying to ensure a reading of “Excellent Connection” for each one (with mixed success).</p>
<p>The very next day at 8:30 am, all of our internet cut out unexpectedly.</p>
<p>C had pushed the router off a side table, turning the whole thing off.</p>
</li>
<li>
<p>For Christmas, I put together and printed a photo album of C’s 2020 using <a href="https://www.mimeophotos.com">Mimeo Photos</a> as <a href="https://www.mimeophotos.com/blog/apple-picture-books">they used to be the provider of Apple’s official photo books</a>.</p>
<p>I’ve been very happy with the result but <a href="/2020/12/28/weeknotes-61/">during my upgrade to Big Sur</a>, I downloaded my photo library afresh from <a href="https://support.apple.com/en-gb/HT204264">iCloud Photo Library</a> after experiencing <a href="https://daringfireball.net/2018/10/icloud_photo_library_start_over">syncing problems</a> with a restored backup. I clicked on my Mimeo project before all my original photos had finished downloading and it threw a <a href="http://support.mimeophotos.com/en/articles/2965789-some-photos-are-missing-warning">“Some Photos Are Missing” warning</a>, immediately gutting the project of all its photos.</p>
<p>Thankfully, I still had a <a href="https://www.shirt-pocket.com/SuperDuper/SuperDuperDescription.html">backup</a> of my entire photo library including the full project and, sure enough, <a href="https://support.apple.com/en-us/HT204414">loading that into Photos.app</a> worked fine but there was seemingly no way to transfer that project over to my new library.</p>
<p>This led me to take the following steps:</p>
<ol>
<li>Disable iCloud Photos.</li>
<li>Drag my entire photo library to the <a href="https://www.imore.com/apple-ditches-trash-can-name-uk-macos-catalina-update">Bin</a>.</li>
<li>Restore my working (but now out-of-date) photo library from my backup.</li>
<li>Without opening my Mimeo project, delete the entire contents of my library.</li>
<li>Re-enable iCloud Photos so that my library is re-downloaded, hopefully fixing my project.</li>
</ol>
<p>Step 4 took over three hours to complete.</p>
<p>It didn’t work.</p>
</li>
</ul>
Weeknotes 62https://mudge.name/2021/01/03/weeknotes-62/2021-01-03T16:55:00+00:002021-01-03T16:55:00+00:00A snowman, this time last year, reconsidering exercise, dental instruments, optionally rotating eyes and colour schemes.<ul>
<li>
<p>I built a snowman.</p>
<p class="center"><img src="/i/snowman.jpg" width="375" height="500" alt="" /></p>
</li>
<li>
<p>It’s been great to read <a href="https://alicebartlett.co.uk/blog/yearnotes-2020">people’s</a> <a href="https://tomstu.art/yearnotes-0-regular-reflection">yearnotes</a>.</p>
<p><a href="/2020/01/05/weeknotes-10/">This time last year</a> I was still on “parental leave” and anxious about finding a new client. By the beginning of March, I was ready to spend most of the year commuting over two hours a day to west London. In the end, I only spent one week doing that commute before everyone was working remotely. It meant I’ve been able to spend much more time with C than I might have hoped but I still can’t reconcile “C the baby” with “C the little boy who loves to climb stairs”.</p>
</li>
<li>
<p>It’s the end of my second year working for <a href="https://www.ghostcassette.com">myself</a> and I’ve been exceptionally lucky enough to work with <a href="https://spyscape.com" title="SPYSCAPE">some</a> <a href="https://gofreerange.com" title="Go Free Range">great</a> <a href="https://www.raspberrypi.org" title="Raspberry Pi">clients</a>.</p>
<p>I have even less of an idea than usual about what the future might hold so I’m extremely grateful to have the flexibility to work from home. It’s even more appealing now that I finally have a desk with a window after working in an unlit hallway for nine months.</p>
</li>
<li>
<p>On top of an unusually sedentary year, Christmas and the <a href="https://atp.fm/410">Accidental Tech Podcast review of Apple Fitness+</a> have pushed me further into reconsidering my exercise regime (which currently consists of nothing).</p>
<p>I’ve ordered <a href="https://ringfitadventure.nintendo.com">Ring Fit Adventure</a> and a new pair of running shoes in an attempt to cajole me into some form of rigorous movement at least once a day.</p>
</li>
<li>
<p>For a friend’s birthday on New Year’s Day, E and I took part in an online escape room over <a href="https://jitsi.org">Jitsi</a>.</p>
<p>There were four of us playing and a single “games master” (no, not <a href="https://en.wikipedia.org/wiki/GamesMaster">that one</a>) who acted as a sort of human text adventure game: they described the scene and responded to our commands to look at things, use objects, etc. As we slowly solved puzzles and revealed things in the scene, they presented an updating top-down view of the room.</p>
<p>It was a surprising amount of fun though it was difficult to coordinate our decision what to do next. At one point, we freed a character trapped in a cupboard and my friend instructed the games master to remove all of their teeth with dental instruments.</p>
</li>
<li>
<p>While working with <a href="https://create-react-app.dev">Create React App</a>, I noticed it uses the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion"><code class="language-plaintext highlighter-rouge">prefers-reduced-motion</code> CSS media feature</a>.</p>
<p>Some may argue the <a href="https://www.ghostcassette.com">rotating eyes on my company logo</a> are vital but I thought it best to <a href="https://github.com/ghostcassette/ghostcassette.github.io/commit/7f92087068b02d9c425179934d0cdb0cd677b836">disable them if someone has requested that non-essential motion is disabled</a>.</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@media</span> <span class="p">(</span><span class="n">prefers-reduced-motion</span><span class="p">:</span> <span class="n">no-preference</span><span class="p">)</span> <span class="p">{</span>
<span class="nc">.eye</span> <span class="p">{</span>
<span class="nl">animation</span><span class="p">:</span> <span class="n">playing</span> <span class="m">23s</span> <span class="n">linear</span> <span class="n">infinite</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div> </div>
<p>This way, the eyes only rotate if you haven’t explicitly asked for reduced motion, i.e. if <code class="language-plaintext highlighter-rouge">prefers-reduced-motion</code> is set to <code class="language-plaintext highlighter-rouge">reduce</code>, the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/animation"><code class="language-plaintext highlighter-rouge">animation</code> property</a> isn’t set at all.</p>
</li>
<li>
<p>As it is a new year and I <a href="/2020/12/28/weeknotes-61/">recently formatted my laptop</a>, it’s time for a new Terminal colour scheme! Having used <a href="https://ethanschoonover.com/solarized/">Solarized</a> and <a href="https://www.nordtheme.com">Nord</a> in the past, <a href="https://twitter.com/cassarani">Leo</a> introduced me to <a href="http://chriskempson.com/projects/base16/">Base16</a>.</p>
<p>It took me a while to understand that I had to combine both <a href="https://github.com/chriskempson/base16-shell">Base16 Shell</a> (to set my shell colours) and <a href="https://github.com/chriskempson/base16-vim/">Base16 Vim</a> (to use those colours in Vim).</p>
<p>With both installed and configured, I could run the following in the shell and have both the Terminal and vim update:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>base16_tomorrow
</code></pre></div> </div>
<p>However, after having <a href="/2019/12/01/weeknotes-5/">pointlessly sped up my zsh startup time</a> over a year ago, I am now avoiding <a href="https://github.com/chriskempson/base16-shell/blob/master/profile_helper.sh">Base16 Shell’s <code class="language-plaintext highlighter-rouge">profile_helper.sh</code></a> and instead use the following in my <code class="language-plaintext highlighter-rouge">.zshrc</code> (where <code class="language-plaintext highlighter-rouge">.base16_theme</code> is a symlink to <code class="language-plaintext highlighter-rouge">~/.config/base16-shell/scripts/base16-tomorrow.sh</code>):</p>
<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> ~/.base16_theme
</code></pre></div> </div>
<p>And the following in my <code class="language-plaintext highlighter-rouge">.vimrc</code>:</p>
<div class="language-vim highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> base16colorspace<span class="p">=</span><span class="m">256</span>
<span class="k">colorscheme</span> base16<span class="p">-</span>tomorrow
</code></pre></div> </div>
<p>If I want to choose a different theme, I need only update the <code class="language-plaintext highlighter-rouge">.base16_theme</code> symlink and the <code class="language-plaintext highlighter-rouge">colorscheme</code> in my <code class="language-plaintext highlighter-rouge">.vimrc</code>.</p>
</li>
<li>
<p>Happy new year!</p>
</li>
</ul>
Weeknotes 61https://mudge.name/2020/12/28/weeknotes-61/2020-12-28T13:33:00+00:002020-12-28T13:33:00+00:00The annual cleansing ritual, first words, festive soldering, learning Go and a lot of Christmas specials.<ul>
<li>
<p>As 2020 draws to a close, I performed the annual cleansing ritual of formatting my laptop and upgrading to <a href="https://www.apple.com/uk/macos/big-sur/">macOS Big Sur</a>.</p>
<p>Following <a href="https://tomstu.art">Tom</a>’s recommendation, I bought a <a href="https://www.samsung.com/uk/memory-storage/portable-ssd/portable-ssd-t5-500gb-blue-mu-pa500b-eu/">Samsung T5 500GB portable SSD</a> and used <a href="https://www.shirt-pocket.com/SuperDuper/SuperDuperDescription.html">SuperDuper!</a> to take a copy of my entire hard drive. I then upgraded to Big Sur, <a href="https://support.apple.com/en-gb/HT204904">restarted my Mac into macOS Recovery</a>, used Disk Utility to erase my internal hard drive and reinstall macOS from scratch.</p>
<p>While I don’t have an <a href="https://github.com/tuzz/zz">automation tool to my name</a>, almost everything I need to set up my computer can be found in my <a href="https://github.com/mudge/dotfiles">dotfiles</a> (aside from my private SSH and GPG keys). After copying over my music and photo libraries, I was back up and running within an hour.</p>
</li>
<li>
<p>We enjoyed Christmas day with the family with its equal parts of rich food and busying myself with the construction of various toys given to C (e.g. a tricycle and a repair garage with 32 pieces of track).</p>
</li>
<li>
<p>While we’ve been staying with my parents, C has started saying words. His first was “apple” and his vocabulary has now expanded to include “ball” and “dada” which, confusingly, is his attempt at “grandad”.</p>
</li>
<li>
<p>C was particularly fond of a <a href="https://www.heyduggee.com/products/hey-duggee-sound-puzzle/">Hey Duggee wooden puzzle</a> from my parents but managed to break it after dropping it only once.</p>
<p>From the outside, it seemed the circuit board attached to the inside of the puzzle had simply come loose but, as the entire unit was glued shut, I had to take a handsaw to it to investigate further.</p>
<p>After much sawing and clumsy prying with various screwdrivers, I managed to get inside to discover the integrated circuit that powered the device had detached as it was stuck in a blob of glue.</p>
<p class="center"><img src="/i/ic.jpg" width="375" height="375" alt="" /></p>
<p>After asking <a href="https://twitter.com/cassarani">Leo</a> for advice, he encouraged me to look up surface-mount technology (SMT) soldering and give it a go. <a href="/2020/12/21/weeknotes-59-and-60/">Calling upon my father-in-law’s soldering iron once more</a>, we gave it our best shot and got it (mostly) working! Only one of the characters (sorry, <a href="https://www.heyduggee.com/characters/">Tag</a>) no longer speaks when you fit their piece in the puzzle but five out of six isn’t bad.</p>
</li>
<li>
<p>Leo has also been teaching me <a href="https://golang.org">Go</a> as I’ve been working on a custom HTTP <a href="https://golang.org/pkg/net/http/httputil/#ReverseProxy">reverse proxy</a> to complete requests on behalf of a desktop application whose HTTP client has not been <a href="https://curl.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html">configured to follow redirects</a>.</p>
<p>Despite having done the excellent <a href="https://tour.golang.org/welcome/1">A Tour of Go</a>, I find that new languages only really stick when I try to ship my own project.</p>
<p>Writing automated tests using the <a href="https://golang.org/pkg/net/http/httptest/">httptest package</a> was great fun and Leo’s recommendation to <a href="https://www.youtube.com/watch?t=376&v=8hQG7QlcLBk&feature=youtu.be">watch Mitchell Hashimoto’s “Advanced Testing with Go”, focussing on subtests and table driven tests</a> was invaluable.</p>
<p>It was also useful to learn about the <a href="https://github.com/golang-standards/project-layout">standard Go project layout</a> even though my project is so small I’ve stuck everything in <code class="language-plaintext highlighter-rouge">main.go</code>.</p>
</li>
<li>
<p>I’ve been working my way through various Christmas specials: <a href="https://www.bbc.co.uk/iplayer/episode/m000qf09/mortimer-whitehouse-gone-fishing-gone-christmas-fishing">Mortimer & Whitehouse: Gone Christmas Fishing</a>, <a href="https://www.bbc.co.uk/iplayer/episode/m000qryp/motherland-christmas-special">Motherland: Christmas Special</a>, <a href="https://www.bbc.co.uk/iplayer/episode/m000qqt4/the-great-british-sewing-bee-2020-specials-1-celebrity-christmas-special">The Great British Sewing Bee: Celebrity Christmas Special</a> and, of course, <a href="https://www.channel4.com/programmes/the-great-british-bake-off-festive-specials/on-demand/70300-001">The Great Christmas Bake Off 2020</a>.</p>
<p>I’ve still got <a href="https://www.bbc.co.uk/iplayer/episode/m000qqsy/the-repair-shop-at-christmas">The Repair Shop at Christmas</a> and the <a href="https://www.bbc.co.uk/iplayer/episode/m000qq50/worzel-gummidge-saucy-nancy">new episode of Worzel Gummidge</a> to go.</p>
</li>
</ul>
Weeknotes 59 and 60https://mudge.name/2020/12/21/weeknotes-59-and-60/2020-12-21T12:10:00+00:002020-12-21T12:10:00+00:00Decamping to the north, two significant DIY projects and things learnt while continuing the migration to Active Storage.<ul>
<li>
<p>After <a href="/2020/12/06/weeknotes-58/">last week</a>, we decided to decamp to my parents’ house for the foreseeable future.</p>
<p>The day after we made the decision, C came home from nursery with a cold and E caught it soon after. We waited for it to pass but, by Friday, our anxiety led to a last minute <a href="https://www.gov.uk/get-coronavirus-test">coronavirus test</a> and a nervous two day wait for the result. Just as we were about to give up travelling that weekend, the text message arrived: a negative result.</p>
<p>Within seconds, E ordered our first (and perhaps only) <a href="https://www.pret.co.uk/en-GB/christmas-menu">Pret</a> of 2020 while we loaded up the car. Complaining about the inexplicable popularity of cranberries in every festive sandwich, we drove north without stopping, listening to <a href="https://www.adam-buxton.co.uk/podcasts/7-bfk9m-4l8kp-blcga-jwabs-blbb8-b9mjx-fj3gr-j4lyp-9jlhe-a4bhm-c67yy-szhgm-m8pcd-l376z-zjb32-wh9dr-za87b-c8l3j-byyhb-segba-hlsc6-5m648">Adam Buxton interview Sir Paul McCartney</a> along the way.</p>
<p><a href="https://www.gov.uk/guidance/tier-4-stay-at-home">I’m so glad we did</a>.</p>
</li>
<li>
<p>With my parents available to look after C, I spent the day I usually look after him alone constructing a climbing frame.</p>
<p class="center"><img src="/i/slide-under-construction.jpg" width="375" height="500" alt="" /></p>
<p>The instructions said it would take two people four hours to complete. It took me around seven hours to screw, bolt, drill and hammer together the finished product.</p>
<p class="center"><img src="/i/finished-slide.jpg" width="375" height="282" alt="" /></p>
<p>It’s no <a href="https://twitter.com/Stew/status/1340683776326856704">1960s cocktail cabinet</a> but it is the most complicated thing I have built.</p>
</li>
<li>
<p>During C’s giddy exploration of his new surroundings, he threw my parents’ 23 year old telephone base station on their kitchen tiles. It immediately stopped working, rendering all of their handsets useless.</p>
<p>Upon inspection, we realised the station had landed on its own cable, severing two of four wires within.</p>
<p>After speaking with <a href="/2020/02/23/weeknotes-17/">my father-in-law</a>, he lent me his soldering iron and some solder sleeves and I attempted to reattach each of the four wires.</p>
<p class="center"><img src="/i/soldering.jpg" width="375" height="282" alt="" /></p>
<p>I have never successfully repaired anything by soldering but this was my very first success!</p>
</li>
<li>
<p>I used <a href="https://www.rubydoc.info/gems/rake/12.0.0/Rake/MultiTask">Rake’s <code class="language-plaintext highlighter-rouge">multitask</code></a> for the first time:</p>
<blockquote>
<p>Same as a regular task, but the immediate prerequisites are done in parallel using Ruby threads.</p>
</blockquote>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">desc</span> <span class="s1">'Run all migrations in parallel'</span>
<span class="n">multitask</span> <span class="ss">migrate: </span><span class="sx">%i[migrate_issues migrate_books migrate_magazines]</span>
<span class="n">desc</span> <span class="s1">'Migrate issues'</span>
<span class="n">task</span> <span class="ss">migrate_issues: :environment</span> <span class="k">do</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="c1"># ...</span>
</code></pre></div> </div>
</li>
<li>
<p>My adventures with <a href="https://guides.rubyonrails.org/active_storage_overview.html">Rails’ Active Storage</a> continue and the use of <a href="https://guides.rubyonrails.org/routing.html#direct-routes">direct routes</a> stands out while reading <a href="https://github.com/rails/rails/blob/6-1-stable/activestorage/config/routes.rb#L18-L24">the source code</a>. This allows you to create powerful routing helpers that take arbitrary objects (rather than relying on <a href="https://guides.rubyonrails.org/routing.html#overriding-named-route-parameters">overriding <code class="language-plaintext highlighter-rouge">ActiveRecord::Base#to_param</code></a>):</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get</span> <span class="s1">'/downloads/:signed_id/*filename'</span> <span class="o">=></span> <span class="s1">'downloads#show'</span><span class="p">,</span> <span class="ss">as: :download_blob</span>
<span class="n">direct</span> <span class="ss">:download</span> <span class="k">do</span> <span class="o">|</span><span class="n">object</span><span class="p">,</span> <span class="n">options</span><span class="o">|</span>
<span class="n">signed_id</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">some</span><span class="p">.</span><span class="nf">custom</span><span class="p">.</span><span class="nf">signed_id</span>
<span class="n">filename</span> <span class="o">=</span> <span class="n">object</span><span class="p">.</span><span class="nf">another</span><span class="p">.</span><span class="nf">custom</span><span class="p">.</span><span class="nf">filename</span>
<span class="n">route_for</span><span class="p">(</span><span class="ss">:download_blob</span><span class="p">,</span> <span class="n">signed_id</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># So now you can call..</span>
<span class="n">download_path</span><span class="p">(</span><span class="n">object</span><span class="p">)</span>
<span class="n">download_url</span><span class="p">(</span><span class="n">object</span><span class="p">)</span>
</code></pre></div> </div>
</li>
<li>
<p>While I’ve since given up on <a href="https://guides.rubyonrails.org/active_storage_overview.html#public-access">Active Storage’s public access mode</a> as it doesn’t let you set a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition"><code class="language-plaintext highlighter-rouge">Content-Disposition</code></a> with <a href="https://aws.amazon.com/s3/">S3</a>, trying to make it work led me to discover <a href="https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#public_url-instance_method"><code class="language-plaintext highlighter-rouge">Aws::S3::Object#public_url</code></a> doesn’t respect the <a href="https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method">AWS client’s <code class="language-plaintext highlighter-rouge">:use_accelerate_endpoint</code></a> option to enable <a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html">Amazon S3 Transfer Acceleration</a>.</p>
<p>You can work around this by instead setting a custom <code class="language-plaintext highlighter-rouge">:endpoint</code> in your client configuration:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">amazon</span><span class="pi">:</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">S3</span>
<span class="na">endpoint</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://s3-accelerate.amazonaws.com"</span>
</code></pre></div> </div>
</li>
<li>
<p>I also disappeared down a bit of a rabbit-hole trying to debug why an <a href="https://guides.rubyonrails.org/active_storage_overview.html#transforming-images">Active Storage Variant</a> in my tests wasn’t being detected as <a href="https://api.rubyonrails.org/classes/ActiveStorage/Variant.html#method-i-processed"><code class="language-plaintext highlighter-rouge">processed</code></a>.</p>
<p>After much use of <a href="http://pry.github.io">Pry</a>, I discovered the bug was being caused by the fact Variant keys are generated by <a href="https://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html#method-i-generate"><code class="language-plaintext highlighter-rouge">ActiveSupport::MessageVerifier#generate</code></a> which uses <a href="https://ruby-doc.org/core-2.7.2/Marshal.html#dump-method"><code class="language-plaintext highlighter-rouge">Marshal.dump</code></a> to serialize a Variant’s transformations and, while my test variant seemed identical to the expected variant, there was a very subtle difference: the filename of the test variant was encoded with ASCII, not UTF-8:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">variant1</span><span class="p">.</span><span class="nf">variation</span><span class="p">.</span><span class="nf">transformations</span><span class="p">)</span>
<span class="s2">"</span><span class="se">\x04\b</span><span class="s2">{</span><span class="se">\b</span><span class="s2">:</span><span class="se">\v</span><span class="s2">formatI</span><span class="se">\"\b</span><span class="s2">png</span><span class="se">\x06</span><span class="s2">:</span><span class="se">\x06</span><span class="s2">ET:</span><span class="se">\x12</span><span class="s2">resize_to_fit[</span><span class="se">\a</span><span class="s2">0i</span><span class="se">\x01\x80</span><span class="s2">:</span><span class="se">\f</span><span class="s2">convertI</span><span class="se">\"\b</span><span class="s2">jpg</span><span class="se">\x06</span><span class="s2">;</span><span class="se">\x06</span><span class="s2">T"</span>
<span class="o">></span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">variant2</span><span class="p">.</span><span class="nf">variation</span><span class="p">.</span><span class="nf">transformations</span><span class="p">)</span>
<span class="s2">"</span><span class="se">\x04\b</span><span class="s2">{</span><span class="se">\b</span><span class="s2">:</span><span class="se">\v</span><span class="s2">formatI</span><span class="se">\"\b</span><span class="s2">png</span><span class="se">\x06</span><span class="s2">:</span><span class="se">\x06</span><span class="s2">EF:</span><span class="se">\x12</span><span class="s2">resize_to_fit[</span><span class="se">\a</span><span class="s2">0i</span><span class="se">\x01\x80</span><span class="s2">:</span><span class="se">\f</span><span class="s2">convertI</span><span class="se">\"\b</span><span class="s2">jpg</span><span class="se">\x06</span><span class="s2">;</span><span class="se">\x06</span><span class="s2">T"</span>
</code></pre></div> </div>
<p>The difference can be hard to spot: it’s the <code class="language-plaintext highlighter-rouge">\x06ET</code> after <code class="language-plaintext highlighter-rouge">png</code> instead of <code class="language-plaintext highlighter-rouge">\x06EF</code>.</p>
<p>This stems from the <a href="https://github.com/rails/rails/issues/25078">encoding of <code class="language-plaintext highlighter-rouge">Rails.root</code> being ASCII</a> (which is due to the underlying behaviour of <a href="https://ruby-doc.org/stdlib-2.7.2/libdoc/pathname/rdoc/Pathname.html"><code class="language-plaintext highlighter-rouge">Pathname</code></a>) and <a href="https://www.rubydoc.info/github/brynary/rack-test/master/Rack/Test/UploadedFile"><code class="language-plaintext highlighter-rouge">Rack::Test::UploadedFile</code></a> using that filename to determine the <a href="https://www.rubydoc.info/github/brynary/rack-test/master/Rack/Test/UploadedFile#original_filename-instance_method"><code class="language-plaintext highlighter-rouge">original_filename</code></a>.</p>
<p>Passing an explicit <code class="language-plaintext highlighter-rouge">:original_filename</code> fixes this:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rack</span><span class="o">::</span><span class="no">Test</span><span class="o">::</span><span class="no">UploadedFile</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'spec/fixtures/image.png'</span><span class="p">),</span>
<span class="ss">original_filename: </span><span class="s1">'image.png'</span>
<span class="p">)</span>
</code></pre></div> </div>
<p>I warned you it was a rabbit-hole.</p>
</li>
</ul>
Weeknotes 58https://mudge.name/2020/12/06/weeknotes-58/2020-12-06T14:22:00+00:002020-12-06T14:22:00+00:00Dealing with bad news, very specific tools for the job and migrating from Paperclip to Active Storage.<ul>
<li>
<p>While preparing some chocolate chip cookies, E asked, “there’s one left from the last batch, shall I <a href="/2020/05/17/weeknotes-29/">KitKat it</a>?”</p>
</li>
<li>
<p>I completed my first successful escape attempt from <a href="https://www.supergiantgames.com/games/hades/">Hades</a> thanks to repeatedly watching <a href="https://www.youtube.com/channel/UCSnGGntxMnAM6oDHML2qzaw">Haelian’s videos</a>, particularly the <a href="https://www.youtube.com/watch?v=EbMkJBxIVaI">top 6 easiest builds</a>.</p>
</li>
<li>
<p>After receiving some bad news, I went to make bread and discovered <a href="/2020/05/03/weeknotes-27/">our kitchen scales</a> didn’t work. Sitting on a flat worktop with nothing on the pan, the reading jumped around of its own accord.</p>
<p>With a ratchet screwdriver in one hand and some <a href="https://wd40.co.uk/specialist/fast-drying-contact-cleaner/">WD-40® Specialist® Fast Drying Contact Cleaner</a> in the other, I took the whole thing apart.</p>
<p class="center"><img src="/i/scales.jpg" width="375" height="375" alt="" /></p>
<p>Prying up the circuit board with a plectrum and discovering water pooled underneath the screen, I thought about my 92 year old grandfather who was the subject of the morning’s bad news. Taken ill in hospital, my occasionally overzealous sense of DIY comes entirely from him.</p>
<p>Always one to be fixing or building something throughout my childhood (or, on one occasion, inexplicably sawing the base off a beloved bin of my mother’s), he would invite me to help with whatever contraption he was working on. Even in his eighties, he drilled a hole in his walking stick so he could thread a loop through and secure it to his wrist, preventing it from ever falling from his grasp.</p>
</li>
<li>
<p>After <a href="/2020/10/11/weeknotes-50/">rain broke our car gate again</a>, I bought myself a four-way utility key which could lock and unlock the gate arms properly (rather than struggling with a pair of needle nose pliers). There’s something strangely satisfying about having a very specific tool for a job.</p>
<p>This might be why I own a spanner designed specifically for removing bath taps.</p>
</li>
<li>
<p>I finally had justification to migrate from <a href="https://thoughtbot.com/blog/closing-the-trombone">Thoughtbot’s long-deprecated Paperclip</a> to <a href="https://guides.rubyonrails.org/active_storage_overview.html">Rails’ Active Storage</a> for storing uploads in a web application.</p>
<p>While Thoughtbot maintain a <a href="https://github.com/thoughtbot/paperclip/blob/master/MIGRATING.md">migration guide</a>, I wanted to make sure I really understood the changes required before making them. Especially since I’m using <a href="https://aws.amazon.com/s3/">Amazon S3</a> and <a href="https://github.com/thoughtbot/paperclip/pull/2637">Paperclip has a bug that can truncate your uploads when using S3</a>.</p>
<p>While Paperclip concerns itself with processing uploads and storing them on disk or in a service like S3, it expects you to handle serving them by storing them somewhere publicly accessible (e.g. by <a href="https://www.rubydoc.info/gems/paperclip/Paperclip/Storage/S3">storing files with a <code class="language-plaintext highlighter-rouge">public-read</code> ACL by default</a>). As such, all my current uploads are stored in a public S3 bucket and I serve them via a <a href="https://aws.amazon.com/cloudfront/">CloudFront</a> distribution.</p>
<p>This notion of both storage and serving are entwined: uploads are stored with relatively human-readable paths, e.g. <code class="language-plaintext highlighter-rouge">/system/books/full_pdfs/00/00/01/original/MyPDF.pdf</code>, and are used to render any links, images, etc.</p>
<p>Looking at Active Storage, I was initially confused not to see any support for serving uploads via a CDN. When doing some test uploads to my existing S3 bucket, I was even more confused to see that a file like <code class="language-plaintext highlighter-rouge">MyPDF.pdf</code> was uploaded to the top-level of the bucket with a <a href="https://api.rubyonrails.org/classes/SecureRandom.html#method-c-base58">base58</a> filename like <code class="language-plaintext highlighter-rouge">77TMHrHJFvFDwodq8w7Ev2m7</code>.</p>
<p>This is because I was still thinking of storage and serving as being one and the same. Diving into <a href="https://guides.rubyonrails.org/active_storage_overview.html#linking-to-files">how Active Storage handles linking to uploads</a>, things started making a little more sense.</p>
<p>Instead of serving uploads using their “real” location in your storage service, e.g. a random key in an S3 bucket, Active Storage serves all uploads through its own controller with its own URLs. This way, the URL can contain the original filename, e.g. <code class="language-plaintext highlighter-rouge">/rails/blobs/lengthy-signed-id-here/MyPDF.pdf</code> and generate short-lived, signed URLs to uploads in an otherwise private S3 bucket. What’s more, the <a href="https://github.com/rails/rails/blob/6-0-stable/activestorage/app/controllers/active_storage/blobs_controller.rb">implementation of this controller for Rails 6.0</a> (and <a href="https://github.com/rails/rails/blob/6-1-stable/activestorage/app/controllers/active_storage/blobs/redirect_controller.rb">its replacement in the upcoming Rails 6.1</a>) is fantastically simple, effectively boiling down to:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">show</span>
<span class="vi">@blob</span> <span class="o">=</span> <span class="no">ActiveStorage</span><span class="o">::</span><span class="no">Blob</span><span class="p">.</span><span class="nf">find_signed!</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:signed_id</span><span class="p">])</span>
<span class="n">expires_in</span> <span class="no">ActiveStorage</span><span class="p">.</span><span class="nf">service_urls_expire_in</span>
<span class="n">redirect_to</span> <span class="vi">@blob</span><span class="p">.</span><span class="nf">service_url</span><span class="p">(</span><span class="ss">disposition: </span><span class="n">params</span><span class="p">[</span><span class="ss">:disposition</span><span class="p">])</span> <span class="c1"># or @blob.url(...) for Rails 6.1</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>The <a href="https://guides.rubyonrails.org/active_storage_overview.html#linking-to-files">official documentation explains this design decision</a>:</p>
<blockquote>
<p>Upon access, a redirect to the actual service endpoint is returned. This
indirection decouples the public URL from the actual one, and allows, for
example, mirroring attachments in different services for
high-availability.</p>
</blockquote>
<p>This decoupling means you rely on Rails to maintain your public URLs rather than using the underlying storage (as they rely on signed IDs to records in your database) but affords you the flexibility of moving storage services.</p>
<p>The difficult bit now is figuring out how to migrate all my existing Paperclip attachments to Active Storage without anyone noticing.</p>
<p>One useful thing I’ve discovered is that it is relatively easy to get a reference to a Paperclip attachment for a record even once you’ve replaced your call to <a href="https://www.rubydoc.info/gems/paperclip/Paperclip%2FClassMethods:has_attached_file"><code class="language-plaintext highlighter-rouge">has_attached_file</code></a> with <a href="https://api.rubyonrails.org/classes/ActiveStorage/Attached/Model.html#method-i-has_one_attached"><code class="language-plaintext highlighter-rouge">has_one_attached</code></a>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Book</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_one_attached</span> <span class="ss">:full_pdf</span> <span class="c1"># used to be has_attached_file :full_pdf</span>
<span class="k">end</span>
<span class="n">book</span> <span class="o">=</span> <span class="no">Book</span><span class="p">.</span><span class="nf">first</span>
<span class="n">book</span><span class="p">.</span><span class="nf">full_pdf</span>
<span class="c1"># => #<ActiveStorage::Attached::One:0xfeedface></span>
<span class="n">paperclip_attachment</span> <span class="o">=</span> <span class="no">Paperclip</span><span class="o">::</span><span class="no">Attachment</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:full_pdf</span><span class="p">,</span> <span class="n">book</span><span class="p">)</span>
<span class="c1"># => #<Paperclip::Attachment:0xdecafbad></span>
</code></pre></div> </div>
<p>You can then convert from a Paperclip attachment to an Active Storage one
(albeit by downloading and re-uploading the original file) like so:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">book</span><span class="p">.</span><span class="nf">full_pdf</span><span class="p">.</span><span class="nf">attach</span><span class="p">(</span>
<span class="ss">io: </span><span class="no">URI</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">paperclip_attachment</span><span class="p">.</span><span class="nf">url</span><span class="p">),</span>
<span class="ss">filename: </span><span class="n">paperclip_attachment</span><span class="p">.</span><span class="nf">original_filename</span><span class="p">,</span>
<span class="ss">content_type: </span><span class="n">paperclip_attachment</span><span class="p">.</span><span class="nf">content_type</span>
<span class="p">)</span>
</code></pre></div> </div>
<p>My hope is that I can write a <a href="https://guides.rubyonrails.org/v4.2/command_line.html#custom-rake-tasks">custom Rake task</a> that will copy all existing attachments to Active Storage before actually switching the application code over to using <code class="language-plaintext highlighter-rouge">has_one_attached</code> so that the database and S3 bucket are fully populated before the switch, e.g. something like the following:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># The real implementation still uses Paperclip so override for the purposes of this task</span>
<span class="no">Book</span> <span class="o">=</span> <span class="no">Class</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">ApplicationRecord</span><span class="p">)</span> <span class="p">{</span> <span class="n">has_one_attached</span> <span class="ss">:full_pdf</span> <span class="p">}</span>
<span class="no">Book</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">book</span><span class="o">|</span>
<span class="n">paperclip_attachment</span> <span class="o">=</span> <span class="no">Paperclip</span><span class="o">::</span><span class="no">Attachment</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:full_pdf</span><span class="p">,</span> <span class="n">book</span><span class="p">)</span>
<span class="k">next</span> <span class="k">unless</span> <span class="n">paperclip_attachment</span><span class="p">.</span><span class="nf">exists?</span>
<span class="n">book</span><span class="p">.</span><span class="nf">full_pdf</span><span class="p">.</span><span class="nf">attach</span><span class="p">(</span>
<span class="ss">io: </span><span class="no">URI</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">paperclip_attachment</span><span class="p">.</span><span class="nf">url</span><span class="p">),</span>
<span class="ss">filename: </span><span class="n">paperclip_attachment</span><span class="p">.</span><span class="nf">original_filename</span><span class="p">,</span>
<span class="ss">content_type: </span><span class="n">paperclip_attachment</span><span class="p">.</span><span class="nf">content_type</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>On a related note, I was wondering how easy it would be to run your own S3-compatible storage service given that <a href="https://en.wikipedia.org/wiki/Amazon_S3#S3_API_and_competing_services">quite a few competing services offer interoperability</a>. This led me to <a href="https://min.io">MinIO</a> which is an open source tool you can point at a location on disk and then <a href="https://docs.min.io/docs/minio-quickstart-guide.html">interact with it via the AWS CLI</a>.</p>
</li>
<li>
<p>Despite writing weeknotes for over a year, I only just discovered that Jekyll has “Live Reload” and “Open URL” <a href="https://jekyllrb.com/docs/configuration/options/#serve-command-options">serve command options</a>. No more will I manually open my browser to <code class="language-plaintext highlighter-rouge">localhost:4000</code> or repeatedly mash ⌘R:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>jekyll s <span class="nt">-lo</span>
</code></pre></div> </div>
</li>
</ul>
Weeknotes 56 and 57https://mudge.name/2020/11/28/weeknotes-56-and-57/2020-11-28T13:47:00+00:002020-11-28T13:47:00+00:00Cooking as a balm in troubled times, boil in the bag routine change, mistaken Twitter identity, meditating with DUPLO, sending cookies with the Fetch API, increasing errors by nearly 500,000%, trying to collect truly anonymous usage data and not thinking about a child's jaws.<ul>
<li>
<p>It has been a trying couple of weeks so we have been in need of uplifting television shows.</p>
<p>Our current rotation is “<a href="https://www.netflix.com/gb/title/80036165">Schitt’s Creek</a>”, “<a href="https://tv.apple.com/us/show/ted-lasso/umc.cmc.vtoh0mn0xn7t3c643xqonfzy">Ted Lasso</a>”, “<a href="https://www.bbc.co.uk/programmes/m000mfhl">Nadiya Bakes</a>” and “<a href="https://www.bbc.co.uk/programmes/m000pbhc">Nigella’s Cook, Eat, Repeat</a>”.</p>
<p>The latter two are a bit heavy-handed on the “<a href="https://youtu.be/T72TopWbXJg">everything’s alright, <em>everything</em>’s alright, it’s OK, it’s <em>fine</em></a>” but I don’t mind.</p>
</li>
<li>
<p>In an attempt to break the <a href="https://www.imdb.com/title/tt0107048/">Groundhog Day</a> of cooking at home and re-introduce some semblance of a date night, E and I ordered a <a href="https://perilladining.slerp.com/order">meal kit from Perilla</a>. It met our hopes of providing a change from something we would typically make ourselves without being a chore to prepare: the main course came in two vacuum sealed bags and only needed boiling in water for 12 minutes.</p>
<p>We sat at our dining table for the first time in months and realised how much we needed a break from our daily routine.</p>
</li>
<li>
<p>I got a <a href="https://www.raspberrypi.org/products/raspberry-pi-400/">Raspberry Pi 400</a> and was pleasantly surprised to find a print edition of the <a href="https://magpi.raspberrypi.org/books/beginners-guide-4th-ed">Raspberry Pi Beginner’s Guide</a> in the box. Leafing through it while booting the Pi 400 for the first time, I pictured myself at 8 years old opening this on Christmas Day.</p>
</li>
<li>
<p>I’ve had <a href="https://twitter.com/mudge">my Twitter account</a> for 14 years, initially only using it to send free group text messages. I am not <a href="https://twitter.com/dotMudge">the most famous “mudge”</a> and am constantly mistaken for <a href="https://en.wikipedia.org/wiki/Peiter_Zatko">Peiter “Mudge” Zatko</a>, so much so that I decided to <a href="https://help.twitter.com/en/safety-and-security/public-and-protected-tweets">“protect” my account</a> a few weeks ago.</p>
<p>I did so in the nick of time as <a href="https://uk.reuters.com/article/us-twitter-security/twitter-names-famed-hacker-mudge-as-head-of-security-idUSKBN27W2MB">Zatko is the new Head of Security for Twitter</a>.</p>
<p>Alas, now I’m receiving a whole new type of misdirected tweet: support and feature requests for Twitter itself.</p>
</li>
<li>
<p>When looking after C, one of my favourite things to do is build him the most elaborate tower out of <a href="https://www.lego.com/en-gb/themes/duplo">DUPLO</a> possible. I’ll incorporate <a href="https://en.wikipedia.org/wiki/Bogie">bogies</a>, animal heads and even the occasional <a href="https://www.lego.com/en-gb/product/submarine-adventure-10910">submarine</a>.</p>
<p>As I know he will eventually destroy it, it has become a sort of <a href="https://en.wikipedia.org/wiki/Mandala">mandala</a>.</p>
</li>
<li>
<p>In versions of <a href="https://www.mozilla.org/en-GB/firefox/new/">Mozilla Firefox</a> prior to <a href="https://www.mozilla.org/en-US/firefox/61.0/releasenotes/">61.0</a>, requests made with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">Fetch API</a> won’t send cookies unless you specify the <a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters"><code class="language-plaintext highlighter-rouge">credentials</code> option</a>.</p>
<p>I ran into this when <a href="https://guides.rubyonrails.org/security.html#csrf-countermeasures">Rails’ cross-site request forgery countermeasures</a> prevented my <a href="https://stripe.com/en-gb/payments/checkout">Stripe Checkout</a> integration from working properly in Firefox 52. Switching to the new default value of <code class="language-plaintext highlighter-rouge">same-origin</code> for <code class="language-plaintext highlighter-rouge">credentials</code> meant the Rails session cookie <em>was</em> correctly sent and the CSRF forgery protection would permit the request:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/some-endpoint</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
<span class="na">credentials</span><span class="p">:</span> <span class="dl">"</span><span class="s2">same-origin</span><span class="dl">"</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">X-CSRF-Token</span><span class="dl">"</span><span class="p">:</span> <span class="nx">csrfToken</span><span class="p">,</span>
<span class="p">},</span>
<span class="c1">// ...</span>
<span class="p">})</span>
</code></pre></div> </div>
</li>
<li>
<p>Concerned there were other bugs I had missed, I started using <a href="https://reactjs.org/docs/strict-mode.html">React’s Strict Mode</a> to perform stricter checks in development. In particular, I wanted to see how my components would behave when <a href="https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects">intentionally double-rendered</a>.</p>
</li>
<li>
<p>I added <a href="https://sentry.io/for/javascript/">JavaScript error monitoring with Sentry</a> which caused a 493,803% increase in “events” since the previous week when I was only <a href="https://sentry.io/for/ruby/">monitoring Ruby errors</a>.</p>
<p>Specifically, I started to see a <em>lot</em> of errors from a <a href="https://github.com/github/fetch/"><code class="language-plaintext highlighter-rouge">window.fetch</code> polyfill</a> bundled with the <a href="http://shopify.github.io/buy-button-js/">Shopify BuyButton.js</a> but almost all of them came from a download page.</p>
<p>This page shows a simple “Thank you” message to the user and immediately redirects them to a PDF using a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv"><code class="language-plaintext highlighter-rouge"><meta http-equiv="refresh"></code></a>. When trying to reproduce the error in <a href="https://www.apple.com/uk/safari/">Safari</a>, I noticed a bunch of different errors all due to the fact the redirect effectively <em>cancels</em> any JavaScript in progress.</p>
<p>To work around this, I copied <a href="https://stackoverflow.com/questions/156686/how-to-start-automatic-download-of-a-file-in-internet-explorer">a technique used by SourceForge and Audible</a> and used an invisible <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"><code class="language-plaintext highlighter-rouge"><iframe></code></a> with a <code class="language-plaintext highlighter-rouge">src</code> pointing to a file with a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition"><code class="language-plaintext highlighter-rouge">Content-Disposition</code></a> of <code class="language-plaintext highlighter-rouge">attachment</code>, e.g.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><iframe</span> <span class="na">src=</span><span class="s">"https://example.com/some.pdf"</span> <span class="na">class=</span><span class="s">"d-none"</span><span class="nt">></iframe></span>
</code></pre></div> </div>
<p>This immediately causes the browser to start downloading the PDF without cancelling any JavaScript running on the page.</p>
</li>
<li>
<p>I’ve been working on a web service to record anonymous usage statistics of a desktop application. A key goal of this project is to avoid storing <a href="https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/key-definitions/what-is-personal-data/">personal data</a> and exemplify <a href="https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/principles/data-minimisation/">data minimisation</a>.</p>
<p>In looking for other applications that do a similar thing, I dug into the <a href="https://docs.brew.sh/Analytics">anonymous aggregate user behaviour analytics collected by Homebrew</a> which <a href="https://github.com/Homebrew/brew/blob/ae0332a0f6fcf4846ef032ba217fb00ef66ccc48/Library/Homebrew/utils/analytics.rb">uses <code class="language-plaintext highlighter-rouge">curl</code> to report events</a> to the <a href="https://developers.google.com/analytics/devguides/collection/protocol/v1">Google Analytics Measurement Protocol</a>.</p>
<p>Reporting events to a third party over HTTP opens a whole can of worms, particularly as IP addresses alone can be considered personal data. As our reporting needs are very simple, I decided to implement my own service using <a href="https://rack.github.io">Rack</a> and <a href="https://redis.io">Redis</a>.</p>
<p>Specifically, requests to the service are stored as daily counts in <a href="https://redis.io/topics/data-types#sorted-sets">Sorted Sets</a> that expire after 90 days like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ZADD choice:2020-11-28 INCR 1 "Some choice"
EXPIRE choice:2020-11-28 7776000
</code></pre></div> </div>
<p>This way, we can see what the top ten most popular choices for a particular day are with <a href="http://redis.io/commands/zrevrange"><code class="language-plaintext highlighter-rouge">ZREVRANGE</code></a> like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ZREVRANGE choice:2020-11-28 0 10 WITHSCORES
</code></pre></div> </div>
<p>If we want to look at choices over a longer time period, we use <a href="http://redis.io/commands/zunionstore"><code class="language-plaintext highlighter-rouge">ZUNIONSTORE</code></a> to aggregate each of the dates in the range:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ZUNIONSTORE choice:past_week 8 choice:2020-11-20 choice:2020-11-21 choice:2020-11-22 choice:2020-11-23 choice:2020-11-24 choice:2020-11-25 choice:2020-11-26 choice:2020-11-27
</code></pre></div> </div>
<p>Then we use <code class="language-plaintext highlighter-rouge">ZREVRANGE</code> again to get the results:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ZREVRANGE choice:past_week 0 10 WITHSCORES
</code></pre></div> </div>
<p>Despite best intentions, it is very easy to accidentally store personal data without realising it. For example, even if you don’t log requests to your service in your application code, Heroku will log the IP addresses of every request made to your app, <a href="https://devcenter.heroku.com/articles/logging#log-history-limits">retaining the most recent 1,500 lines of logs for one week</a>.</p>
</li>
<li>
<p>I enjoyed <a href="https://rowanmanning.com/posts/yeehaw/">Rowan Manning’s “Yeehaw!”</a> about attempting side projects and tip #5 “When the Fun Stops, Stop” feels especially relevant when working alone. I’ve had several situations recently where I find myself down some blind alley of refactoring and have to force myself to <a href="https://git-scm.com/docs/git-restore"><code class="language-plaintext highlighter-rouge">git restore .</code></a> and refocus my efforts.</p>
</li>
<li>
<p>C is unhappily teething but please try not to think about how <a href="http://memento.muttermuseum.org/detail/child-skull">every child’s jaws are packed with teeth</a>.</p>
</li>
</ul>
Weeknotes 55https://mudge.name/2020/11/15/weeknotes-55/2020-11-15T10:52:00+00:002020-11-15T10:52:00+00:00Integrating Shopify BuyButton.js with Turbolinks, the M1 SoC, testing signed Stripe webhooks and cavorting.<ul>
<li>
<p>This week, I’ve been trying to make <a href="http://shopify.github.io/buy-button-js/">Shopify BuyButton.js</a> play nicely with <a href="https://github.com/turbolinks/turbolinks">Turbolinks</a>.</p>
<p>Integration with Turbolinks is often tricky because <a href="https://github.com/turbolinks/turbolinks#building-your-turbolinks-application">it retains the state of <code class="language-plaintext highlighter-rouge">window</code> and <code class="language-plaintext highlighter-rouge">document</code> across page changes and any other JavaScript objects will stay in memory</a>. In the case of BuyButton.js, this means any <a href="https://github.com/Shopify/buy-button-js/blob/v2.1.7/src/ui.js#L29-L36">UI state</a> and <a href="https://github.com/Shopify/buy-button-js/blob/v2.1.7/src/ui.js#L50-L53">bound event listeners</a> will persist as a user clicks around your site. This is further complicated by <a href="https://github.com/turbolinks/turbolinks#understanding-caching">Turbolinks’ caching</a> which means <a href="https://github.com/turbolinks/turbolinks#making-transformations-idempotent">any transformations may be applied multiple times</a>.</p>
<p>If you don’t take Turbolinks into consideration, you might see broken behaviour such as clicks on “Add to cart” not working, the entire UI failing to appear or multiple buy buttons appearing every time you use the browser’s back and forward buttons.</p>
<p>While I’m still working on this, I’ve had some success with the following strategy:</p>
<ol>
<li>
<p><a href="http://shopify.github.io/buy-button-js/#creating-a-shop-client">Create a Shop client</a> <em>once</em> to be re-used across all pages.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="nx">ShopifyBuy</span><span class="p">.</span><span class="nx">buildClient</span><span class="p">({</span>
<span class="na">domain</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-shop.myshopify.com</span><span class="dl">"</span><span class="p">,</span>
<span class="na">storefrontAccessToken</span><span class="p">:</span> <span class="dl">"</span><span class="s2">your-storefront-access-token</span><span class="dl">"</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div> </div>
</li>
<li>
<p><a href="http://shopify.github.io/buy-button-js/#initializing-the-library">Initialize the library</a> <em>once</em> to be re-used across all pages.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">ui</span> <span class="o">=</span> <span class="nx">ShopifyBuy</span><span class="p">.</span><span class="nx">UI</span><span class="p">.</span><span class="nx">init</span><span class="p">(</span><span class="nx">client</span><span class="p">);</span>
</code></pre></div> </div>
<p>This will <a href="https://github.com/Shopify/buy-button-js/blob/v2.1.7/src/ui.js#L273-L281">append a <code class="language-plaintext highlighter-rouge"><style></code> tag to the <code class="language-plaintext highlighter-rouge"><head></code> of your <code class="language-plaintext highlighter-rouge">document</code></a> and <a href="https://github.com/Shopify/buy-button-js/blob/v2.1.7/src/ui.js#L273-L281">add various event listeners to <code class="language-plaintext highlighter-rouge">document</code></a>. As the <code class="language-plaintext highlighter-rouge"><head></code> of your page will be <a href="https://github.com/turbolinks/turbolinks#navigating-with-turbolinks">merged when a user navigates between pages</a> this will retain the styles and it is <a href="https://github.com/turbolinks/turbolinks#observing-navigation-events">best practice to only bind event listeners once on <code class="language-plaintext highlighter-rouge">document</code></a>.</p>
</li>
<li>
<p><del>On <code class="language-plaintext highlighter-rouge">turbolinks:load</code>, <em>reset the inner state of the library</em> using <a href="https://github.com/Shopify/buy-button-js/blob/v2.1.7/src/ui.js#L92-L105"><code class="language-plaintext highlighter-rouge">destroyComponent</code></a> as you can no longer guarantee the various DOM elements the UI will have injected into your <code class="language-plaintext highlighter-rouge"><body></code> are still present.</del></p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">ui</span><span class="p">.</span><span class="nx">destroyComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">product</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">destroyComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">cart</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">destroyComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">collection</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">destroyComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">productSet</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">destroyComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">modal</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">destroyComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">toggle</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div> </div>
<p><em>Update:</em> In writing this, I discovered <a href="https://github.com/Shopify/buy-button-js/blob/v2.1.7/src/ui.js#L92-L105"><code class="language-plaintext highlighter-rouge">destroyComponent</code></a> doesn’t work if you don’t pass the ID of a component’s model. It uses <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice"><code class="language-plaintext highlighter-rouge">Array.prototype.splice()</code></a> to mutate an array while looping over it. This means the position of each component in the array is changing as the loop advances so some components will not be destroyed, e.g.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">];</span>
<span class="o">></span> <span class="nx">foo</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">e</span><span class="p">,</span> <span class="nx">index</span><span class="p">)</span> <span class="o">=></span> <span class="nx">foo</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="nx">index</span><span class="p">,</span> <span class="mi">1</span><span class="p">));</span>
<span class="o">></span> <span class="nx">foo</span>
<span class="p">[</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">4</span> <span class="p">]</span>
</code></pre></div> </div>
<p>We can fix this by destroying and resetting the components ourselves:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">product</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">cart</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">collection</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">productSet</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">modal</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">toggle</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">product</span> <span class="o">=</span> <span class="p">[];</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">cart</span> <span class="o">=</span> <span class="p">[];</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">collection</span> <span class="o">=</span> <span class="p">[];</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">productSet</span> <span class="o">=</span> <span class="p">[];</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">modal</span> <span class="o">=</span> <span class="p">[];</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">.</span><span class="nx">toggle</span> <span class="o">=</span> <span class="p">[];</span>
</code></pre></div> </div>
<p>Or, more concisely:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">type</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">[</span><span class="nx">type</span><span class="p">].</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">[</span><span class="nx">type</span><span class="p">]</span> <span class="o">=</span> <span class="p">[];</span>
<span class="p">});</span>
</code></pre></div> </div>
<p>We can do this when the <a href="https://github.com/turbolinks/turbolinks#preparing-the-page-to-be-cached"><code class="language-plaintext highlighter-rouge">turbolinks:before-cache</code> event fires as recommended in the documentation</a>:</p>
<blockquote>
<p>You can use this event to reset forms, collapse expanded UI elements, or tear down any third-party widgets so the page is ready to be displayed again.</p>
</blockquote>
<p>This will ensure any DOM elements injected by BuyButton.js won’t be cached so you don’t end up with duplicate “Add to cart” buttons that don’t do anything when clicked.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbolinks:before-cache</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">type</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">[</span><span class="nx">type</span><span class="p">].</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">component</span><span class="p">)</span> <span class="o">=></span> <span class="nx">component</span><span class="p">.</span><span class="nx">destroy</span><span class="p">());</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">components</span><span class="p">[</span><span class="nx">type</span><span class="p">]</span> <span class="o">=</span> <span class="p">[];</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div> </div>
</li>
<li>
<p>Following that, <a href="http://shopify.github.io/buy-button-js/#creating-a-component">create your components</a> as normal.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">ui</span><span class="p">.</span><span class="nx">createComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">product</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
<span class="na">handle</span><span class="p">:</span> <span class="dl">"</span><span class="s2">some-product-handle</span><span class="dl">"</span><span class="p">,</span>
<span class="na">node</span><span class="p">:</span> <span class="nx">element</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div> </div>
<p>To make it easy for Turbolinks to create the appropriate components based on the <code class="language-plaintext highlighter-rouge"><body></code> contents for every <code class="language-plaintext highlighter-rouge">turbolinks:load</code>, I embedded the necessary information in <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes">data attributes</a>.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="dl">"</span><span class="s2">[data-shopify-product-handle]</span><span class="dl">"</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">element</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="na">dataset</span><span class="p">:</span> <span class="p">{</span> <span class="nx">shopifyProductHandle</span> <span class="p">}</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">element</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">temporaryWrapper</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">div</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">temporaryWrapper</span><span class="p">);</span>
<span class="nx">ui</span><span class="p">.</span><span class="nx">createComponent</span><span class="p">(</span><span class="dl">"</span><span class="s2">product</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
<span class="na">handle</span><span class="p">:</span> <span class="nx">shopifyProductHandle</span><span class="p">,</span>
<span class="na">node</span><span class="p">:</span> <span class="nx">temporaryWrapper</span><span class="p">,</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div> </div>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">data-shopify-product-handle=</span><span class="s">"some-product-handle"</span><span class="nt">></div></span>
</code></pre></div> </div>
<p>I create a new <code class="language-plaintext highlighter-rouge"><div></code> for the component to use as calling <a href="https://github.com/Shopify/buy-button-js/blob/ba89d7fa2f4944da7f388a9b6d632d764b376e5f/src/component.js#L197-L202"><code class="language-plaintext highlighter-rouge">destroy</code> will remove the node from the DOM</a>.</p>
</li>
</ol>
<p><del>I still need to iron out some issues with Turbolinks caching but I’m optimistic.</del> <em>Update:</em> with the update to use <code class="language-plaintext highlighter-rouge">turbolinks:before-cache</code> above, all my caching issues are now resolved.</p>
</li>
<li>
<p>On Tuesday, <a href="https://www.apple.com/mac/m1/">Apple announced the M1 system on a chip</a>. While I’m not in a hurry to replace my <a href="https://marco.org/2017/11/14/best-laptop-ever">beloved</a> refurbished 2015 15-inch MacBook Pro, the pandemic and <a href="/2020/01/19/weeknotes-12/">my musculoskeletal woes</a> have made my working setup decidedly stationary. I’m seriously considering the <a href="https://www.apple.com/uk/mac-mini/">Mac mini</a> for the first time.</p>
</li>
<li>
<p>I’ve also been working on an integration with <a href="https://stripe.com/">Stripe</a> and have been amazed how good their <a href="https://stripe.com/docs/development">developer tools</a> are. From the ability to <a href="https://stripe.com/docs/cli/listen">test webhooks with their CLI</a>, their <a href="https://stripe.com/docs/development/checklist">go-live checklist</a> and <a href="https://stripe.com/docs/testing">extensive support for testing</a>, it has been surprisingly pleasant to work with.</p>
<p>If you need to test handling their webhooks but are <a href="https://stripe.com/docs/webhooks/signatures">verifying their signatures</a>, I found the following useful in my <a href="https://relishapp.com/rspec/rspec-rails/docs/controller-specs">controller specs</a> (assuming you’re storing your signing secret in <a href="https://guides.rubyonrails.org/configuring.html#custom-configuration"><code class="language-plaintext highlighter-rouge">Rails.configuration.stripe_webhook_signing_secret</code></a>):</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sign_stripe_webhook</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
<span class="n">timestamp</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">zone</span><span class="p">.</span><span class="nf">now</span>
<span class="n">signature</span> <span class="o">=</span> <span class="no">Stripe</span><span class="o">::</span><span class="no">Webhook</span><span class="o">::</span><span class="no">Signature</span><span class="p">.</span><span class="nf">compute_signature</span><span class="p">(</span>
<span class="n">timestamp</span><span class="p">,</span>
<span class="n">payload</span><span class="p">.</span><span class="nf">to_json</span><span class="p">,</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">stripe_webhook_signing_secret</span>
<span class="p">)</span>
<span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Stripe-Signature'</span><span class="p">]</span> <span class="o">=</span> <span class="no">Stripe</span><span class="o">::</span><span class="no">Webhook</span><span class="o">::</span><span class="no">Signature</span><span class="p">.</span><span class="nf">generate_header</span><span class="p">(</span><span class="n">timestamp</span><span class="p">,</span> <span class="n">signature</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>Then I can use it like so:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">it</span> <span class="s1">'returns a 200 OK with a valid Stripe event'</span> <span class="k">do</span>
<span class="n">event</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'id'</span> <span class="o">=></span> <span class="s1">'evt_1'</span><span class="p">,</span>
<span class="s1">'object'</span> <span class="o">=></span> <span class="s1">'event'</span><span class="p">,</span>
<span class="s1">'type'</span> <span class="o">=></span> <span class="s1">'checkout.session.completed'</span><span class="p">,</span>
<span class="s1">'data'</span> <span class="o">=></span> <span class="p">{}</span>
<span class="p">}</span>
<span class="n">sign_stripe_webhook</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
<span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">params: </span><span class="n">event</span><span class="p">,</span> <span class="ss">as: :json</span>
<span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_http_status</span><span class="p">(</span><span class="ss">:ok</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>Following <a href="/2020/11/08/weeknotes-54/">last week’s adventures with <code class="language-plaintext highlighter-rouge">brew bundle</code></a> and <a href="https://medium.com/@scottm">Scott</a>’s tip about <a href="https://medium.com/@scottm">Homebrew Bundle</a> being integrated with <a href="https://github.com/mas-cli/mas">mas</a>, I committed my <a href="https://github.com/mudge/dotfiles/blob/master/Brewfile"><code class="language-plaintext highlighter-rouge">Brewfile</code> to my dotfiles</a>.</p>
</li>
<li>
<blockquote>
<p>The days of spring will surely bring the birds and bees cavorting.</p>
<p>But since I am a gentleman, I’d much rather be <a href="https://youtu.be/Sx3ORAO1Y6s">jorting</a>.</p>
</blockquote>
</li>
</ul>
Weeknotes 54https://mudge.name/2020/11/08/weeknotes-54/2020-11-08T14:12:00+00:002020-11-08T14:12:00+00:00Handling numeric user input with React, cleaning up unused Homebrew dependencies, Command Line Tools woe, losing control of your eyes and comparing our son to icons in IKEA instruction manuals.<ul>
<li>
<p>I worked on two projects with <a href="https://blog.chrislowis.co.uk">Chris</a> for <a href="https://gofreerange.com">Go Free Range</a>, the first of which involved working with a <a href="https://reactjs.org">React</a> form to record numbers given by the user. We already had some <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select"><code class="language-plaintext highlighter-rouge"><select></code></a> elements to do this, e.g.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Form</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">unit</span><span class="p">,</span> <span class="nx">setUnit</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">handleUnitChange</span> <span class="o">=</span> <span class="p">({</span> <span class="na">target</span><span class="p">:</span> <span class="p">{</span> <span class="nx">value</span> <span class="p">}})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setUnit</span><span class="p">(</span><span class="nb">parseFloat</span><span class="p">(</span><span class="nx">value</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">select</span> <span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">unit</span><span class="si">}</span> <span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleUnitChange</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nt">option</span> <span class="na">value</span><span class="p">=</span><span class="s">"1"</span><span class="p">></span>m<span class="p"></</span><span class="nt">option</span><span class="p">></span>
<span class="p"><</span><span class="nt">option</span> <span class="na">value</span><span class="p">=</span><span class="s">"100"</span><span class="p">></span>cm<span class="p"></</span><span class="nt">option</span><span class="p">></span>
<span class="p"><</span><span class="nt">option</span> <span class="na">value</span><span class="p">=</span><span class="s">"1000"</span><span class="p">></span>mm<span class="p"></</span><span class="nt">option</span><span class="p">></span>
<span class="p"></</span><span class="nt">select</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
</code></pre></div> </div>
<p>This works fine. React is able to interpret our numeric value for <code class="language-plaintext highlighter-rouge">unit</code> and select the appropriate <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option"><code class="language-plaintext highlighter-rouge"><option></code></a> in our dropdown. When the user changes their selection, the corresponding value is passed to <code class="language-plaintext highlighter-rouge">handleUnitChange</code> and stored as a floating point number. This way, we keep our <code class="language-plaintext highlighter-rouge">unit</code> as a number throughout so it is ready to be passed to other functions without any coercion.</p>
<p>However, if we try to do the same thing with an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input"><code class="language-plaintext highlighter-rouge"><input type="text"></code></a>, we quickly run into trouble:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Form</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">unit</span><span class="p">,</span> <span class="nx">setUnit</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">handleUnitChange</span> <span class="o">=</span> <span class="p">({</span> <span class="na">target</span><span class="p">:</span> <span class="p">{</span> <span class="nx">value</span> <span class="p">}})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setUnit</span><span class="p">(</span><span class="nb">parseFloat</span><span class="p">(</span><span class="nx">value</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">unit</span><span class="si">}</span> <span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleUnitChange</span><span class="si">}</span> <span class="p">/></span>
<span class="p">);</span>
<span class="p">};</span>
</code></pre></div> </div>
<p>What happens when the user tries to empty the field (e.g. perhaps to enter a new value from scratch)? <code class="language-plaintext highlighter-rouge">handleUnitChange</code> will fire with a value of an empty string:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="nb">parseFloat</span><span class="p">(</span><span class="dl">""</span><span class="p">)</span>
<span class="kc">NaN</span>
</code></pre></div> </div>
<p>Perhaps then we try to avoid storing <code class="language-plaintext highlighter-rouge">NaN</code> and instead set <code class="language-plaintext highlighter-rouge">unit</code> to be <code class="language-plaintext highlighter-rouge">undefined</code> or <code class="language-plaintext highlighter-rouge">null</code> if it is <a href="https://developer.mozilla.org/en-US/docs/Glossary/Falsy">falsy</a>?</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">handleUnitChange</span> <span class="o">=</span> <span class="p">({</span> <span class="na">target</span><span class="p">:</span> <span class="p">{</span> <span class="nx">value</span> <span class="p">}})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setUnit</span><span class="p">(</span><span class="nx">value</span> <span class="p">?</span> <span class="nb">parseFloat</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">:</span> <span class="kc">undefined</span><span class="p">);</span>
<span class="c1">// or setUnit(value ? parseFloat(value) : null);</span>
<span class="p">}</span>
</code></pre></div> </div>
<p>If we do this, when the user clears the field, React will log a warning that we’re switching a component from being <a href="https://reactjs.org/docs/forms.html#controlled-components">controlled</a> to being <a href="https://reactjs.org/docs/uncontrolled-components.html">uncontrolled</a> because setting the <code class="language-plaintext highlighter-rouge">value</code> of a field to <code class="language-plaintext highlighter-rouge">undefined</code> or <code class="language-plaintext highlighter-rouge">null</code> has a special meaning: it tells React this field is now uncontrolled.</p>
<p>Given it is totally valid for the user to enter an empty value, we realised we needed to separate our concerns: use strings for the <code class="language-plaintext highlighter-rouge">input</code> value but convert the value to a number when we pass it elsewhere in our application.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Form</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">unit</span><span class="p">,</span> <span class="nx">setUnit</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="dl">"</span><span class="s2">1</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">handleUnitChange</span> <span class="o">=</span> <span class="p">({</span> <span class="na">target</span><span class="p">:</span> <span class="p">{</span> <span class="nx">value</span> <span class="p">}})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setUnit</span><span class="p">(</span><span class="nx">value</span><span class="p">);</span>
<span class="p">};</span>
<span class="nx">doSomeCalculation</span><span class="p">(</span><span class="nb">parseFloat</span><span class="p">(</span><span class="nx">unit</span><span class="p">));</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">unit</span><span class="si">}</span> <span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleUnitChange</span><span class="si">}</span> <span class="p">/></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">doSomeCalculation</span> <span class="o">=</span> <span class="p">(</span><span class="nx">unit</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">Number</span><span class="p">.</span><span class="nb">isNaN</span><span class="p">(</span><span class="nx">unit</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="p">};</span>
</code></pre></div> </div>
<p>Using an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number"><code class="language-plaintext highlighter-rouge"><input type="number"></code></a> helps a little with this as it prevents non-numeric inputs firing the <code class="language-plaintext highlighter-rouge">onChange</code> event but it seemed cleaner to deal with the problem in our logic than rely on browser behaviour.</p>
</li>
<li>
<p>The second project Chris and I worked on involved a <a href="https://www.python.org">Python</a> API written with <a href="https://flask.palletsprojects.com/en/1.1.x/">Flask</a>. Getting things running locally was quite a pain with my attempts to use <a href="https://pip.pypa.io/en/stable/">pip</a> to install <a href="https://numpy.org">NumPy</a> ending in compilation errors. In the end, switching to the previous stable <a href="https://www.python.org/downloads/release/python-386/">Python release of 3.8</a> (rather than the latest of <a href="https://www.python.org/downloads/release/python-390/">3.9</a>) using <a href="https://github.com/pyenv/pyenv">pyenv</a> solved my problems.</p>
<p>However, I was left with a bit of a mess of installed <a href="https://brew.sh">Homebrew</a> formulae and wanted to clean things up. Thankfully, I found <a href="https://superuser.com/a/1509213">an answer on the macOS Super User</a> with a great tip for removing unused Homebrew dependencies:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>brew bundle dump
<span class="gp">$</span><span class="w"> </span>brew bundle <span class="nt">--force</span> cleanup
</code></pre></div> </div>
<p>This will first dump all explicitly installed formulae and <a href="https://github.com/Homebrew/homebrew-cask">casks</a> into a <code class="language-plaintext highlighter-rouge">Brewfile</code> (which you could then edit) and then uninstall anything no longer required.</p>
</li>
<li>
<p>I ended up in a similar mess after installing <a href="https://developer.apple.com/xcode/">Xcode</a> and <a href="https://docs.brew.sh/Manpage#doctor-options"><code class="language-plaintext highlighter-rouge">brew doctor</code></a> reported that I had out-of-date Command Line Tools.</p>
<p>Attempting to use <a href="https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-DOWNLOADING_COMMAND_LINE_TOOLS_IS_NOT_AVAILABLE_IN_XCODE_FOR_MACOS_10_9__HOW_CAN_I_INSTALL_THEM_ON_MY_MACHINE_"><code class="language-plaintext highlighter-rouge">xcode-select --install</code></a> failed with a strange error about the software update not being found and Software Update itself couldn’t install the update properly.</p>
<p>It turns out that Xcode <em>includes</em> the Command Lines Tools itself even though I had a previous installation in <code class="language-plaintext highlighter-rouge">/Library/Developers/CommandLineTools</code> installed via <code class="language-plaintext highlighter-rouge">xcode-select</code>. I ended up completely removing both Xcode and the Command Line Tools and reinstalling the latter before Software Update could successfully pick up the right update.</p>
</li>
<li>
<p><a href="https://www.urbanautomaton.com">Simon</a> wrote a truly excellent blog post about Rails autoloading with <a href="https://github.com/fxn/zeitwerk">Zeitwerk</a> titled “<a href="https://www.urbanautomaton.com/blog/2020/11/04/rails-autoloading-heaven/">Rails autoloading — now it works, and how!</a>”. This is a follow-up to his <a href="https://www.urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/">2013 post “Rails autoloading — how it works, and when it doesn’t”</a> to which I often found myself referring.</p>
</li>
<li>
<p>Following <a href="/2020/11/01/weeknotes-53/">last week’s need for earplugs</a>, I lost some hearing in my left ear. I returned from a trip to the pharmacy with some <a href="https://en.wikipedia.org/wiki/Hydrogen_peroxide_-_urea">urea hydrogen peroxide</a> and watched the <a href="https://thegreatbritishbakeoff.co.uk">Great British Bake Off</a> sideways with the deeply unsettling sensation of fizzing in my ear canal.</p>
<p>I woke in the small hours of Thursday morning to find the room uncontrollably spinning around me. I staggered to the bathroom where I discovered I could no longer get my eyes to focus straight ahead without them constantly drifting to my right. With E’s help, I managed to speak to a doctor on the phone and go in to our local GP’s surgery to see someone in person that morning.</p>
<p>After waiting outside, the doctor greeted me at the door wrapped in plastic, said that they needed to “fire [their] gun” (a thermometer) at me and instructed me not to touch anything as we walked to a consultation room.</p>
<p>Thankfully, it’s a viral ear infection that’ll clear up by itself in a matter of days.</p>
</li>
<li>
<p>Now that C is confident enough walking, he happily roams our flat looking for things to prod, pull or combine in some way. We’ve already lost three bowls to his curiosity and I’ve had to rewire one plug. It also means that I took the step of lowering the mattress in his cot now that he more closely resembles the IKEA cartoon for “toddler” than the one for “baby”.</p>
</li>
</ul>
Weeknotes 53https://mudge.name/2020/11/01/weeknotes-53/2020-11-01T14:35:00+00:002020-11-01T14:35:00+00:00An unwelcome return, low spirits and removing a special case.<ul>
<li>
<p>A year ago—one week before <a href="/2020/09/06/weeknotes-45/">C was born</a>—wearing rubber gloves and a face mask, I pulled out all of our kitchen appliances and stuffed every crevice with steel wool. I covered the wool with electrical tape before carefully arranging boxes of mouse poison given to me by the council along the walls.</p>
<p>Last night, just as E and I were falling asleep, there was the sound of scratching mere metres from our heads. As soon as we moved to investigate, it stopped and I spent the rest of the night waiting to hear something else other than the pounding of my own heart.</p>
</li>
<li>
<p>With that and the <a href="https://www.gov.uk/guidance/new-national-restrictions-from-5-november">new national restrictions in the UK from 5th November</a>, my level of <a href="https://github.com/computationclub/computationclub.github.io/wiki/Types-and-Programming-Languages-Chapter-13-References#pub">“cheersy-cheers” spirit</a> is at an all-time low.</p>
</li>
<li>
<p><a href="https://youtu.be/wbfu39l0kxg">Brian David Gilbert’s “Earn $20K EVERY MONTH by being your own boss”</a> helped a little though.</p>
</li>
<li>
<p>I’m working on a publishing platform that has a lot of special cases for a single publication. That publication is now being handled by another codebase so I have the opportunity to remove it and simplify a lot of the existing software. However, removing it cleanly without risking breaking other functionality has proven very difficult.</p>
<p>I tried a few different strategies: trying to remove all of the view-related code first then trying to come at it by removing a whole feature and all its related dependencies at a time. However, I kept coming unstuck: finding myself having deleted a lot of code but with far too many tests failing and not enough confidence I was on the right track so I’d <a href="https://git-scm.com/docs/git-reset#Documentation/git-reset.txt---hard"><code class="language-plaintext highlighter-rouge">git reset --hard</code></a> and abandon the approach.</p>
<p>On Friday, I finally succeeded by being far more conservative in the amount I deleted at any one time. Rather than deleting an entire feature, I’d pick a small aspect of it (say, an admin-only view) and remove that, ensuring all the tests still passed before removing the next slice.</p>
<p>The result?</p>
<blockquote>
<p>39 additions and 2,786 deletions.</p>
</blockquote>
</li>
<li>
<p>Time to find some ear plugs.</p>
</li>
</ul>
Weeknotes 52https://mudge.name/2020/10/29/weeknotes-52/2020-10-29T10:32:00+00:002020-10-29T10:32:00+00:00A year of weeknotes, inconsequential quests, kernel extensions, using Terraform to manage multiple AWS accounts at once, testing feature flags and the secret fantasy of a future self.<ul>
<li>
<p>It has been a year since my <a href="/2019/11/04/weeknotes-1/">first weeknotes</a>.</p>
<p>I still haven’t finished that blog post on <a href="https://reactjs.org/docs/hooks-intro.html">React Hooks</a>.</p>
</li>
<li>
<p>Since reading <a href="https://hotpodnews.com/the-problem-with-the-inconsequential-quest/">Caroline Crampton’s “The Problem with the Inconsequential Quest”</a>, I’ve found it hard to listen to recommended podcasts that fit the description, e.g. <a href="https://underunderstood.com/podcast/episode/amazons-next-top-model/">Underunderstood’s “Amazon’s Next Top Model”</a>:</p>
<blockquote>
<p>I’m sure readers will be very familiar with the form: a host chooses something that is of seemingly little importance and investigates it with a thoroughness and journalistic rigour that seems completely out of proportion to the original question. The resulting episode or series documents this journey in detail and ultimately reveals a conclusion that surprises and delights listeners.</p>
</blockquote>
</li>
<li>
<p>Instead of writing these notes on Sunday while C napped, I attempted to remap the extra buttons on <a href="https://www.kensington.com/en-gb/p/products/control/trackballs/expert-mouse-wireless-trackball/">my trackball</a> to go back and forwards in my browser. I tried <a href="https://www.usboverdrive.com">USB Overdrive</a>, <a href="http://plentycom.jp/en/steermouse/">SteerMouse</a> and <a href="https://sensible-side-buttons.archagon.net">Sensible Side Buttons</a> but balked at the need for <a href="https://developer.apple.com/support/kernel-extensions/">kernel extensions</a> (and sadly Sensible Side Buttons doesn’t work because one of the buttons is interpreted as a middle-click).</p>
<p>I spent an hour trying to clean up the various bits of software I’d tried as <code class="language-plaintext highlighter-rouge">kextstat</code> revealed USB Overdrive’s kernel extension lingered after uninstallation. In the end, I had to use <code class="language-plaintext highlighter-rouge">kextcache</code> to clear “staged” extensions in <a href="https://support.apple.com/en-gb/HT201314">Recovery Mode</a>.</p>
<p>I’ve actually made things worse than when I installed the <a href="https://www.kensington.com/en-gb/software/trackballworks-customization-software/">official Kensington TrackballWorks driver</a>.</p>
</li>
<li>
<p>I’ve been doing quite a lot of work with <a href="https://aws.amazon.com">Amazon Web Services</a> for a client and was very grateful to have automated it with <a href="https://www.terraform.io">Hashicorp Terraform</a> as I needed to quickly destroy and recreate <a href="https://aws.amazon.com/s3/">S3</a> buckets connected to <a href="https://aws.amazon.com/cloudfront/">CloudFront</a> distributions with <a href="https://aws.amazon.com/certificate-manager/">ACM</a> certificates.</p>
<p>I’ve found it useful to manage both our production and staging environment using a single Terraform repository by leveraging <a href="https://aws.amazon.com/organizations/">AWS Organizations</a> and the ability for an administrator in the root organization to temporarily assume the role of an administrator in any of the member accounts. By default, <a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_accounts_access.html">AWS will create a role called <code class="language-plaintext highlighter-rouge">OrganizationAccountAccessRole</code></a> to do exactly this.</p>
<p>To manage <a href="https://www.terraform.io/docs/configuration/resources.html">Terraform resources</a> in different AWS accounts we can then use an “<a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs#assume-role">Assume Role</a>” block in an <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs#assume-role">AWS provider</a> like so:</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
<span class="nx">alias</span> <span class="p">=</span> <span class="s2">"root"</span>
<span class="nx">region</span> <span class="p">=</span> <span class="s2">"eu-west-1"</span>
<span class="p">}</span>
<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
<span class="nx">alias</span> <span class="p">=</span> <span class="s2">"production"</span>
<span class="nx">region</span> <span class="p">=</span> <span class="s2">"eu-west-1"</span>
<span class="nx">assume_role</span> <span class="p">{</span>
<span class="nx">role_arn</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::PRODUCTION-ID:role/OrganizationAccountAccessRole"</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
<span class="nx">alias</span> <span class="p">=</span> <span class="s2">"staging"</span>
<span class="nx">region</span> <span class="p">=</span> <span class="s2">"eu-west-1"</span>
<span class="nx">assume_role</span> <span class="p">{</span>
<span class="nx">role_arn</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::STAGING-ID:role/OrganizationAccountAccessRole"</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div> </div>
<p>Then you specify the appropriate provider alias in each resource, e.g.</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_s3_bucket"</span> <span class="s2">"staging"</span> <span class="p">{</span>
<span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">staging</span>
<span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"bucket-staging"</span>
<span class="p">}</span>
<span class="k">resource</span> <span class="s2">"aws_s3_bucket"</span> <span class="s2">"production"</span> <span class="p">{</span>
<span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">production</span>
<span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"bucket-production"</span>
<span class="p">}</span>
</code></pre></div> </div>
<p>You can use something like <a href="https://github.com/99designs/aws-vault">AWS Vault</a> to run your Terraform commands as an administrator in your root account and it’ll assume the appropriate role for each resource automatically:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>aws-vault <span class="nb">exec </span>root-account-administrator <span class="nt">--</span> terraform plan
</code></pre></div> </div>
</li>
<li>
<p>I’ve been working on a new feature in a <a href="https://rubyonrails.org">Rails</a> application and needed to hide it behind a <a href="https://martinfowler.com/articles/feature-toggles.html">feature flag</a> so I wasn’t blocking other features that needed deploying. Now that I have two possible sets of behaviour in various parts of the application, I wanted to test them both with <a href="https://rspec.info">RSpec</a>.</p>
<p>The feature flag lives in a bit of <a href="https://guides.rubyonrails.org/configuring.html#custom-configuration">custom Rails configuration</a>, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'COOL_NEW_FEATURE'</span><span class="p">]</span>
</code></pre></div> </div>
<p>Then various parts of the application switch on the value of that flag:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span>
<span class="c1"># do something cool</span>
<span class="k">else</span>
<span class="c1"># do something less cool</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>I wanted to change the value of that configuration before running some examples and reset it back to its original value after the test, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">context</span> <span class="s1">'with the cool new feature enabled'</span> <span class="k">do</span>
<span class="n">around</span> <span class="k">do</span> <span class="o">|</span><span class="n">example</span><span class="o">|</span>
<span class="n">original_value</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">example</span><span class="p">.</span><span class="nf">run</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span> <span class="o">=</span> <span class="n">original_value</span>
<span class="k">end</span>
<span class="c1"># Examples here...</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>However, adding this across several places in the test suite seemed rather noisy but we can leverage <a href="https://relishapp.com/rspec/rspec-core/v/3-9/docs/metadata/user-defined-metadata">user-defined metadata</a> with <a href="https://relishapp.com/rspec/rspec-core/v/3-9/docs/hooks/filters">hook filtering</a> to clean this up:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in spec/rails_helper.rb</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
<span class="c1"># ...</span>
<span class="n">config</span><span class="p">.</span><span class="nf">around</span><span class="p">(</span><span class="ss">:each</span><span class="p">,</span> <span class="ss">:cool_new_feature</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">example</span><span class="o">|</span>
<span class="n">original_value</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span> <span class="o">=</span> <span class="n">example</span><span class="p">.</span><span class="nf">metadata</span><span class="p">[</span><span class="ss">:cool_new_feature</span><span class="p">]</span>
<span class="n">example</span><span class="p">.</span><span class="nf">run</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">cool_new_feature</span> <span class="o">=</span> <span class="n">original_value</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>Then in our specs, we simply tag examples with the appropriate metadata:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">context</span> <span class="s1">'with cool new feature enabled'</span><span class="p">,</span> <span class="ss">cool_new_feature: </span><span class="kp">true</span> <span class="k">do</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="n">context</span> <span class="s1">'with cool new feature disabled'</span><span class="p">,</span> <span class="ss">cool_new_feature: </span><span class="kp">false</span> <span class="k">do</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>I haven’t had any epiphanies after writing these notes every week for a year but I still think about the <a href="/2020/02/10/weeknotes-15/">Paul Murray quote I shared in weeknotes 15</a>:</p>
<blockquote>
<p>Although technology has the capability now to record entire lifetimes, meaning that every moment may be pulled from the foaming sea of oblivion to the dry land of perfect recall, the mythic power of the photograph nevertheless relates to the future, and not to the past. Every recording conceals the secret fantasy of a future self who will observe it; this future self is himself the simulacrum, the persona ficta. He exists beyond time, beyond action, beyond need; his only function is to witness the continuum of the past, as he might observe the steps that brought him to godhood. Through this fantasy, time is transformed from the condition of loss into a commodity that may be acquired and stockpiled; rather than disappear ceaselessly into the past, life accumulates, each moment becoming a unit of a total self that is the culmination of our experiences in a way that we—biological composites who profligately shed our cells, our memories and our possessions—can never be.</p>
</blockquote>
</li>
</ul>
Weeknotes 51https://mudge.name/2020/10/18/weeknotes-51/2020-10-18T09:33:00+00:002020-10-18T09:33:00+00:00Pizza crusts, London Computation Club wiki, what to do with an inherited codebase and collective disbelief.<ul>
<li>
<p><a href="/2020/09/27/weeknotes-48/">Two weeks ago</a> I wrote about choosing between two 12” pizzas and one 18” pizza by comparing their surface area but <a href="https://tomstu.art">Tom</a> pointed out this only works if you care about sheer quantity of pizza.</p>
<p>If you care more about pizza crust (especially when <a href="https://www.theguardian.com/global-development/2014/mar/02/pizza-hut-2880-calorie-monstrosity-worlds-burgeoning-food-crisis">studded with burgers</a>), you should instead compare the circumference:</p>
<ul>
<li>“<a href="https://www.wolframalpha.com/input/?i=Circumference+of+12+inch+diameter+circle">circumference of 12 inch diameter circle</a>” = 37.7 inches * 2 = 75.4 inches</li>
<li>“<a href="https://www.wolframalpha.com/input/?i=Circumference+of+18+inch+diameter+circle">circumference of 18 inch diameter circle</a>” = 56.6 inches</li>
</ul>
</li>
<li>
<p>I added <a href="https://github.com/computationclub/computationclub.github.io/wiki/iCE2Tetris">a page about Leo’s astounding “iCE2Tetris” project</a> to the <a href="https://github.com/computationclub/computationclub.github.io/wiki">London Computation Club wiki</a> this week.</p>
<p>According to the git history, I have made 637 changes to the wiki since 2015, adding 8,228 lines and removing 1,911.</p>
<p>While I’d go <a href="/2014/11/26/data-structures-as-functions/" title="Data Structures as Functions, published in 2014">years</a> <a href="/2016/03/26/the-difficulty-with-the-success-of-software-development-teams/" title="What Is a Successful Software Development Team?, published in 2016">between</a> <a href="/2019/01/02/cross-compiling-rust-for-a-raspberry-pi-on-travis-ci/" title="Cross-Compiling Rust for a Raspberry Pi on Travis CI, published in 2019">posts</a> on this site, you could often find me writing up <a href="https://github.com/computationclub/computationclub.github.io/wiki#meetings">club meetings</a> every few weeks.</p>
<p>The write-up I remember most is “<a href="https://github.com/computationclub/computationclub.github.io/wiki/Types-and-Programming-Languages-Chapter-15-Subtyping-–-Part-1">Types and Programming Languages Chapter 15: Subtyping</a>” as I left the meeting defeated and burnt out, ready to give up on the club altogether but the act of writing it up clarified my understanding and restored my energy for returning to the club.</p>
</li>
<li>
<p>I recently inherited a codebase and am the sole developer working on it. This means I’m in the rare position to make whatever change I see fit to make it easier to work on and so I decided to start with dependency updates.</p>
<p>Having seen it used before, I decided to <a href="https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/enabling-and-disabling-version-updates">set up GitHub Dependabot</a>. While finding the right documentation was tricky given that <a href="https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/">Dependabot is being integrated into GitHub itself</a>, once I had it set up I was pleasantly surprised how well it worked: raising PRs for upgrades, automatically rebasing them and even detecting when multiple dependencies need to be updated in tandem.</p>
<p>I have mixed feelings about updating dependencies now being an essential part of being a web developer but if you’re going to have to do it anyway, you might as well automate it.</p>
</li>
<li>
<p>The other change I’d like to make is to the code style itself: the formatting, linting, etc. Running <a href="https://github.com/rubocop-hq/rubocop">RuboCop</a> with the project’s current configuration produces over 1,000 violations so I needed some advice on how to approach this in a manageable way.</p>
<p>Thankfully, <a href="https://youtu.be/Cd1aLLwTNKc">Scott Matthewman’s “Calling in the cops”</a> is an excellent talk about this exact topic: how to sensibly approach linting a legacy codebase.</p>
</li>
<li>
<p>This led me to discover the <a href="https://github.com/rubocop-hq/rubocop-rails/issues/195"><code class="language-plaintext highlighter-rouge">Rails/FilePath</code> cop prefers using forward slashes in paths</a> and share in the <a href="https://twitter.com/tenderlove/status/842064491936280576">collective disbelief that Ruby’s <code class="language-plaintext highlighter-rouge">File.join</code> does not change separators based on platform</a>.</p>
</li>
</ul>
Weeknotes 50https://mudge.name/2020/10/11/weeknotes-50/2020-10-11T12:02:00+00:002020-10-11T12:02:00+00:00Baby-proofing, Richard Osman’s role in my life, carrying pliers, zeitgeist marketing, ergonomic experiments and escaping from Hades.<ul>
<li>
<p>After realising our <a href="/2020/06/07/weeknotes-32/">DNS sinkhole</a> was causing the <a href="https://store.playstation.com/en-gb/product/EP4338-CUSA00122_00-IPLAYER0FULL0000">BBC iPlayer app for the PlayStation 4</a> to crash (presumably because some essential telemetry was being blocked), I switched it over to use <a href="https://1.1.1.1/dns/">Cloudflare’s 1.1.1.1</a> and discovered two whole series of “<a href="https://www.bbc.co.uk/cbeebies/shows/hey-duggee">Hey Duggee</a>” <a href="https://www.netflix.com/gb/title/80989192">not on Netflix</a>. That’s my viewing for the foreseeable sorted.</p>
</li>
<li>
<p>As C grows more confident with his toddling, his love of opening cupboards and drawers to explore their contents has also increased. I began securing certain cupboards with “child safety locks”: plastic straps secured at either end with adhesive tape.</p>
<p>Having placed a lock on our fridge the wrong way round, I followed the instructions to remove it: heat up the adhesive with a hair-dryer and peel it off. While that did the trick, it also melted the <a href="https://en.wikipedia.org/wiki/Thermofoil">thermofoil</a> on our fridge door causing it to warp and peel away from the wood.</p>
<p>It turns out the lock is no match for C’s strength as he can easily pull the ruined door open.</p>
</li>
<li>
<p>My mum refers to C’s teeth as “tooshy pegs”.</p>
</li>
<li>
<p>Our evening schedule is dictated entirely by <a href="https://en.wikipedia.org/wiki/Richard_Osman">Richard Osman</a>.</p>
<p>First, there’s “<a href="https://www.bbc.co.uk/programmes/b00rhg2r">Pointless</a>” from 5:15 pm to 6 pm when C has his dinner. We switch over to BBC Two for “<a href="https://www.bbc.co.uk/programmes/b094mjv0">Richard Osman’s House of Games</a>”, the end of which marks the beginning of C’s bedtime routine.</p>
<p>After a hard day, I long for the “Answer Smash” siren.</p>
</li>
<li>
<p>Following <a href="https://zerodivisionerror.neocities.org">Maciej</a>’s recommendation, I’ve been reading <a href="https://en.wikipedia.org/wiki/The_Dark_Forest">Cixin Liu’s “The Dark Forest”</a>. <a href="/2019/12/01/weeknotes-5/">I read the previous book</a> back in December last year but didn’t continue with <a href="https://en.wikipedia.org/wiki/Remembrance_of_Earth%27s_Past">the series</a>. In looking for something new to read, I’m not sure why I overlooked this as I am thoroughly enjoying it so far.</p>
</li>
<li>
<p>Our flat has an automatic car gate that stops working whenever it rains. As this week has been particularly wet, one of our neighbours discovered you can unlock the gate arms (so they can be opened and closed manually) by turning a small bolt.</p>
<p>You can now find a pair of needle-nose pliers in my coat pocket wherever I go.</p>
</li>
<li>
<p>E and I watched “<a href="https://www.imdb.com/title/tt7146812/">Onward</a>” last night. While she wasn’t especially moved, I cried the most I have in years.</p>
</li>
<li>
<p>I enjoyed the first two episodes of <a href="https://allconsuming.show">Noah Kalina and Adam Lisagor’s “All Consuming” podcast</a> reviewing new direct-to-consumer products commonly advertised on the likes of Instagram, etc.</p>
<p>The subject of <a href="https://allconsuming.show/listen/nuggs">their first episode</a>, “the Tesla of chicken” <a href="https://eatnuggs.com/releases">NUGGS’ release notes</a> are quite the read:</p>
<blockquote>
<ul>
<li>Enables a close to indistinguishable chicken-like substrate (utilizing soy and wheat proteins)</li>
</ul>
</blockquote>
</li>
<li>
<p><a href="https://www.bloomberg.com/opinion/articles/2020-09-07/welcome-to-your-bland-new-world-of-consumer-capitalism">Ben Schott’s “Welcome to Your Bland New World”</a> (via <a href="https://alicebartlett.co.uk/blog/weaknotes-110">Alice Bartlett</a>) is another exploration of this “zeitgeist marketing”:</p>
<blockquote>
<p>All startups seek to disrupt and disintermediate a smug status quo, or
originate and dominate an entirely new niche. But what makes a brand a
bland is duality: claiming simultaneously to be unique in product,
groundbreaking in purpose, and singular in delivery, while slavishly
obeying an identikit formula of business model, look and feel, and tone
of voice.</p>
</blockquote>
</li>
<li>
<p>In need of a fun game to pass the time, I read <a href="https://www.eurogamer.net/articles/2020-09-17-hades-review-of-myth-and-mayhem">Eurogamer.net’s review of “Hades”</a> and bought it.</p>
<p>Having previously tried and failed to get into “<a href="https://dead-cells.com">Dead Cells</a>”, I was nervous about another “<a href="https://en.wikipedia.org/wiki/Roguelike">roguelike</a>” but something about it works for me. Perhaps the sense of progress with every failed run as the story and world unfolds breaks the monotony of dying over and over.</p>
<p>It has also inspired me to listen to <a href="https://www.hadestown.com">Anaïs Mitchell’s “Hadestown”</a> again.</p>
</li>
<li>
<p>While not going quite so far <a href="https://blog.scottlogic.com/2020/10/09/ergo-rabbit-hole.html">down the ergonomic keyboard rabbit hole as Steven Waterman</a>, I did buy the wired version of <a href="https://www.kensington.com/en-gb/p/products/control/trackball-products/expert-mouse-wireless-trackball/">Kensington’s Expert Mouse Trackball</a>. It is comically large with a 55 mm diameter trackball but if it <a href="https://www.nytimes.com/wirecutter/reviews/best-trackballs/">helps my ailing skeleton</a>, I’m willing to try it.</p>
</li>
</ul>
Weeknotes 49https://mudge.name/2020/10/04/weeknotes-49/2020-10-04T09:34:00+00:002020-10-04T09:34:00+00:00Paracetamol suspensions, 90s music and final shaping.<ul>
<li>
<p>C had his <a href="https://www.nhs.uk/conditions/vaccinations/NHS-vaccinations-and-when-to-have-them/">1 year vaccinations</a> which meant we had to <a href="https://www.gov.uk/government/publications/menb-vaccine-and-paracetamol">top him up with a cherry-flavoured paracetamol suspension</a> to keep his fever down. At the same time, he was fighting off his first cold caught from nursery which made for a grumpy, ruddy-cheeked boy.</p>
<p>I caught C’s cold so my <a href="https://www.lemsip.co.uk">Lemsip</a> intake has sky-rocketed.</p>
</li>
<li>
<p>E and I began watching HBO’s “<a href="https://www.hbo.com/watchmen">Watchmen</a>” TV series after <a href="https://www.imdb.com/title/tt7049682/awards">its success at the Emmy Awards</a>. E has neither read the <a href="https://en.wikipedia.org/wiki/Watchmen">comic</a> nor seen the <a href="https://www.imdb.com/title/tt0409459/">2009 Zack Snyder film</a> so I attempted to summarise the key plot points.</p>
<p>It got tricky when I had to explain the squid.</p>
</li>
<li>
<p>We celebrated our third wedding anniversary by exchanging gifts, making an extravagant dinner and indulging in various indie classics from the 90s including <a href="https://en.wikipedia.org/wiki/Version_2.0">Garbage’s “Version 2.0”</a>, <a href="https://en.wikipedia.org/wiki/Without_You_I%27m_Nothing_(Placebo_album)">Placebo’s “Without You I’m Nothing”</a> and <a href="https://en.wikipedia.org/wiki/If_You%27re_Feeling_Sinister">Belle and Sebastian’s “If You’re Feeling Sinister”</a>.</p>
</li>
<li>
<p><a href="https://blog.chrislowis.co.uk/2020/10/03/weeknotes.html">I worked on a project with Chris</a> all week, mostly pair-programming via <a href="https://tuple.app">Tuple</a>. As my other project work is quite solitary, this was a welcome change. It was great to get a chance to work with Chris for the first time after years of being involved with <a href="https://london.computation.club">London Computation Club</a> and <a href="https://lrug.org">London Ruby User Group</a> together.</p>
</li>
<li>
<p>Despite being put off Netflix’s “<a href="https://thechefshow.com">The Chef Show</a>” by its first episode with Gwyneth Paltrow, I watched the episode with Chad Robertson and Chris Bianco at <a href="https://tartinebakery.com">Tartine Bakery</a> and am warming to it a little. I assumed it was yet another entry in the genre of “<a href="/2020/03/08/weeknotes-19/">famous chef and their celebrity friends</a>” but, in this recent episode, Favreau seems genuinely humble and keen to learn from others.</p>
<p>It also gave me a detailed look at Robertson’s final shaping technique for his country loaf recipe which prompted me to give it a try:</p>
<p class="center"><img src="/i/final-shaping.gif" width="270" height="480" alt="" /></p>
<p>And here’s the result:</p>
<p class="center"><img src="/i/tartine-loaf.jpg" width="375" height="375" alt="" /></p>
</li>
</ul>
Weeknotes 48https://mudge.name/2020/09/27/weeknotes-48/2020-09-27T12:45:00+00:002020-09-27T12:45:00+00:00Mentor envy, calculating the surface area of pizza, separation, adding HTTP headers and how to use your entire body to express dissatisfaction.<ul>
<li>
<p>I thoroughly enjoyed <a href="https://www.annashipman.co.uk/jfdi/meeting-everyone.html">Anna Shipman’s “Meeting everyone on a new team”</a>:</p>
<blockquote>
<p>When I joined the Financial Times as Technical Director for FT.com, I inherited a team of around 50 engineers. One of the first things I did was meet each of them for a one-to-one.</p>
</blockquote>
<p>Though it did make me envious of Anna’s mentors:</p>
<blockquote>
<p>The idea was suggested to me by a mentor, who’d been advised to do it by his mentor, a <em>Rear Admiral</em> [emphasis added], who said this was something you should do whenever you have a team of fewer than 150 people.</p>
</blockquote>
</li>
<li>
<p>Torn between sharing two takeaway 12 inch pizzas or a single 18 inch one, E and I turned to <a href="https://www.wolframalpha.com">Wolfram Alpha</a>:</p>
<ul>
<li>“<a href="https://www.wolframalpha.com/input/?i=area+of+circle+with+diameter+12+inches">area of circle with diameter 12 inches</a>” = 36 square inches * 2 = 72 square inches</li>
<li>“<a href="https://www.wolframalpha.com/input/?i=area+of+circle+with+diameter+18+inches">area of circle with diameter 18 inches</a>” = 81 square inches</li>
</ul>
</li>
<li>
<p>I dropped C off at nursery for the first time. Wearing a face mask, I had to slip on blue, disposable shoe covers while waiting for him to settle.</p>
<p>At a certain point, one of the key workers said to the other “let’s do the separation now” and it was time for me to leave.</p>
</li>
<li>
<p>In order to test parts of a new project, I have to send an extra HTTP header with my requests. If you use Chrome or Firefox, <a href="https://bewisse.com/modheader/">ModHeader</a> seems a popular choice for doing this but, as a Safari user, I needed an alternative.</p>
<p>Instead of using a browser extension, I settled on using <a href="https://mitmproxy.org">mitmproxy</a>, a “free and open source interactive HTTPS proxy”. I use its “<a href="https://mitmproxy.readthedocs.io/en/v2.0.2/features/setheaders.html">Set Headers</a>” feature to add my extra header to requests matching a particular domain, e.g. to add <code class="language-plaintext highlighter-rouge">X-Special-Header: header-value</code> to any request to a <a href="https://devcenter.heroku.com/articles/github-integration-review-apps">Heroku review app</a>:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>mitmdump <span class="nt">--modify-headers</span> <span class="s1">'/herokuapp.com/X-Special-Header/header-value'</span>
</code></pre></div> </div>
<p>This works regardless of browser and with tools such as <a href="https://httpie.org">HTTPie</a>.</p>
</li>
<li>
<p>“<a href="https://thegreatbritishbakeoff.co.uk">The Great British Bake Off</a>” is back.</p>
<p>After <a href="/2019/11/16/weeknotes-3/">finding last year’s competition disappointing and upsetting</a>, my faith was somewhat restored by the much more joyful “<a href="https://www.channel4.com/programmes/junior-bake-off">Junior Bake Off</a>” and “<a href="https://www.channel4.com/programmes/bake-off-the-professionals">Bake Off: The Professionals</a>”.</p>
<p>After the first episode and <a href="https://youtu.be/JKhOFKCOH5M">Matt Lucas’ surprisingly accurate impression of Boris Johnson</a>, I’m cautiously optimistic.</p>
</li>
<li>
<p>I’m a big fan of mint chocolate and have come to the conclusion the only mint chocolate worth buying is “<a href="https://www.lindt.co.uk/lindt-excellence-dark-mint-intense-bar-100g-en-gb-03046920028752">Lindt Excellence Dark Mint Intense</a>”. Anything else just doesn’t cut it.</p>
</li>
<li>
<p>C has been developing a special move he deploys whenever you prevent him doing something he wants (e.g. pulling things out of the fridge, grabbing bottles of gel for unclogging drains) where he’ll suddenly stiffen his entire body, arching backwards as hard as he can. We’ve tried explaining this usually results in an outcome no one desires: flying headfirst backwards.</p>
<p>We call it the “protest banana”.</p>
</li>
</ul>
Weeknotes 47https://mudge.name/2020/09/21/weeknotes-47/2020-09-21T11:55:00+00:002020-09-21T11:55:00+00:00Settling in, potentially diseased garlic and being at a loss for notes.<ul>
<li>
<p>It was C’s “settling in” week at nursery this week. E took him in for a few hours every day to get used to the unfamiliar surroundings and his key worker. This meant ironing <a href="https://www.easy2name.com">name labels</a> into all of his clothes and that he was in someone else’s care for an hour or two for the first time.</p>
<p>I capitalised on this strange, guilty sense of freedom by going for a run for the first time in months. I didn’t get very far and spent a lot of the time trying to keep my running tights up. I also indulged in <a href="https://www.netflix.com/title/80186863" title="Umbrella Academy">some</a> <a href="https://www.netflix.com/title/80097140" title="Altered Carbon">science fiction</a>.</p>
</li>
<li>
<p><a href="/2020/04/19/weeknotes-25/">Five months ago</a>, I planted a clove of garlic. Today, I harvested it!</p>
<p class="center"><img src="/i/garlic.jpg" width="360" height="480" alt="" /></p>
<p>It certainly <em>looks</em> like a bulb of garlic but we now need to dry it out for a few days. I’m only slightly alarmed by the <a href="https://www.rhs.org.uk/advice/grow-your-own/vegetables/garlic">RHS Gardening website’s warning</a> that growing garlic from a supermarket “may carry disease”.</p>
</li>
<li>
<p>The official <a href="https://github.blog/2020-09-17-github-cli-1-0-is-now-available/">GitHub CLI 1.0 was released</a>. I’ve long used <a href="https://hub.github.com">hub</a> but <a href="https://mislav.net/2020/01/github-cli/">as its maintainer Mislav was hired by GitHub to build GitHub CLI</a>, I’m going to try switching over.</p>
<blockquote>
<p>The GitHub CLI that we are building is not exactly what I would have
chosen to create if I was the only person in charge of making it, but
this is A Good Thing. Before, I never really made an effort to understand
who the audience of hub was, but with an actual team we finally get to
explore that and hopefully build something that’s ambitious not in terms
of the number of features it offers or how much of the GitHub API it
covers, but in how well it helps people be productive with their daily
work.</p>
</blockquote>
</li>
<li>
<p>With an increasingly mobile C in nursery, E going back to work and me working with multiple clients for the first time, I’m at a bit of a loss for notes this week.</p>
</li>
</ul>
Weeknotes 46https://mudge.name/2020/09/13/weeknotes-46/2020-09-13T10:23:00+00:002020-09-13T10:23:00+00:00T'north, Netflix tips and tricks and coffee brewing theory.<ul>
<li>
<p>We’re back in London after two weeks up in t’north of England.</p>
<p>It had been six months since our families had properly seen C which is basically half his life so far. I got to rummage through old toys, uncovering an old <a href="https://www.lego.com/en-gb/themes/duplo">DUPLO</a> aeroplane and train set of mine. We discovered a local <a href="https://www.themilkchurn.com">Yorkshire gelateria</a> and revisited the <a href="http://theangelssharebakery.com/">bakery that provided bread for our wedding</a>, giving C his first taste of a <a href="http://theangelssharebakery.com/about/specialities/">Yorkshire Curd tart</a>.</p>
<p>It was the break we all needed before everything changes in the next few weeks as E returns to work and C starts nursery.</p>
</li>
<li>
<p>Part of the ritual of leaving guest accommodation is logging out of one’s Netflix account. Depending on the device, this may involve <a href="https://help.netflix.com/en/node/23876">an arcane sequence of button presses</a> to reveal a secret Settings screen:</p>
<blockquote>
<p>From within the Netflix app, use the arrows on your remote to enter the following sequence: <strong>Up, Up, Down, Down, Left, Right, Left, Right, Up, Up, Up, Up.</strong></p>
</blockquote>
<p>It is easier to remember if you think of it as a <a href="https://en.wikipedia.org/wiki/Konami_Code">variation of the Konami Code</a>.</p>
</li>
<li>
<p>Speaking of Netflix: during the trip, we signed my dad up for his own account so he could stop filling my viewing history with endless shows related to the drug trade. I’ve been tempted to delete my profile and start again but am trying to <a href="https://help.netflix.com/en/node/22205">hide the things he has watched instead</a>.</p>
<p>However, it can “take up to 24 hours for a hidden title to be removed from all your devices” and the interface seems remarkably flaky. I’m currently having to delete each and every episode of “Suits” and “Homeland” individually (I blame <a href="https://en.wikipedia.org/wiki/Eventual_consistency">eventual consistency</a>).</p>
<p>Hopefully I’ll soon be rewarded for my meticulous clicking with fewer recommendations for documentaries about Pablo Escobar and El Chapo.</p>
</li>
<li>
<p>One thing we’ve missed while being away was a <a href="https://youtu.be/KxwcQ1dapw8">damn fine cup of coffee</a>. Leo recommended <a href="https://youtu.be/AI4ynXzkSQo">James Hoffmann’s “The Ultimate V60 Technique”</a> which has immediately sent me down something of a “coffee brewing theory” rabbit-hole.</p>
</li>
</ul>
Weeknotes 45https://mudge.name/2020/09/06/weeknotes-45/2020-09-06T11:02:00+00:002020-09-06T11:02:00+00:00A birth story.<ul>
<li>
<p>“We’re going to pull the alarm now and a lot of people will come into the room but don’t worry.”</p>
<p>A year ago, I held an exhausted E’s hand as consultants, doctors and midwives gathered around C on a <a href="https://www.draeger.com/en_uk/Products/Resuscitaire">Resuscitaire</a>. It’d been two days since E’s waters had broken and what little sleep we’d had was punctuated by my clumsy timing of contractions. It was a time of being offered choices, desperately trying to understand <a href="https://www.cochranelibrary.com/cdsr/about-cdsr">Cochrane systematic reviews</a> and ultimately making decisions I was in no way equipped to make.</p>
<p>I listened to the mundane counting as they performed CPR, each new round of “1, 2, 3, 4” steadily filling me with dread.</p>
<p>Eventually, a mewling and a hubbub as C urinated on several members of staff. They began bickering among themselves as to what had happened to him with the most commanding person in the room repeatedly insisting “there was no meconium on the vocal cords”. As they finally beckoned me over to properly see C for the first time, the lead midwife jostled to show me a clear tube with something cloudy within, “see, look at the mucus!”</p>
<p>Bewildered, I did as I was told and followed as he was wheeled to the neonatal intensive care unit (the “NICU”). He was soon covered in dressings with teddy bears and the <a href="https://www.3m.co.uk/3M/en_GB/company-uk/">3M</a> logo on them. I quickly began misspelling the word “cannula”.</p>
<p>As I watched him through the plastic, I turned to the nurses and, masking my panic, asked “erm, is it OK he’s pulling his breathing tubes out?”</p>
<p>I returned to the labour ward to find E eating a bowl of bran flakes.</p>
<p>Later, after being told parents shouldn’t be present for the procedure, E and I would sit in a small room off the NICU as C had a lumbar puncture. I stared at a noticeboard, trying not to think about what I’d read about “bacterial meningitis” the night before.</p>
<p>Eventually, we’d make it home after the most careful drive of my life. There’d be a trip to the children’s ambulatory care unit to deal with C’s cannula which we were told had “tissued”. We’d watch as two people struggled to fit another and I distracted myself by staring at some morning TV show about how a teenager had gone blind from a diet that consisted exclusively of chips.</p>
<p>There’d be a <a href="https://www.nhs.uk/conditions/pregnancy-and-baby/baby-reviews/">red book</a> and a course of benzylpenicillin and gentamicin. I forget which one stings.</p>
<p>There’d be tearful debriefs with our community midwives. I’d grow resentful and then angry at how polarised the <a href="https://www.nhs.uk">NHS</a> and <a href="https://www.nct.org.uk">NCT</a> were and how that might have affected what happened. I’d reluctantly share our “birth story” with our NCT class and immediately regret it.</p>
<p>I’d think about sharing what had happened more publicly in an attempt to order my own thoughts and feelings but nothing would come of it.</p>
<p>Instead, after nine weeks, I’d <a href="/2019/11/04/weeknotes-1/">start writing weeknotes</a>.</p>
</li>
<li>
<p>Happy 1st birthday, C.</p>
<p class="center"><img src="/i/c.jpg" width="360" height="480" alt="" /></p>
</li>
</ul>
Weeknotes 44https://mudge.name/2020/08/30/weeknotes-44/2020-08-30T09:39:00+00:002020-08-30T09:39:00+00:00A young child's unsteady walk, deep cleaning and a holiday.<ul>
<li>
<p>In the past two weeks, C has begun insisting we hold his hands so he can toddle around. Having previously shown little interest in movement, this is quite a change of pace and has led to the discovery there is no greater fun in the world than pulling magnets off a radiator or mashing the buttons on a washing machine.</p>
</li>
<li>
<p>This newfound mobility has meant I routinely find tiny, greasy hand-prints in new and exciting places. <a href="https://atp.fm/381">John Siracusa would not approve</a> of C’s love of pressing his sticky palms against our TV screen.</p>
</li>
<li>
<p>Now that I have been able to help with childcare in these last few weeks before E returns to work, she has begun a methodical deep clean of our flat starting with our kitchen (grubby hand-prints notwithstanding). It has been a great opportunity to <a href="https://konmari.com">KonMari</a> the strange, useless things one accumulates over time, e.g. an expansive collection of reusable wine bottles that can only be filled at a shop that no longer offers refills.</p>
</li>
<li>
<p>I write this looking out over a rolling Yorkshire dale.</p>
<p>We’re staying in a self-catering lodge which was apparently thoroughly disinfected and—as if to really shock-and-awe any virus—had some kind of antibacterial fog bomb detonated within.</p>
<p>This isn’t our first time here as this was our wedding venue nearly three years ago. Upon arrival, we were greeted by a member of staff who had looked after us back then. She told us they stopped doing weddings shortly after ours so we were one of the last.</p>
</li>
<li>
<p>This is <a href="https://youtu.be/UHzF5KnoN20">the furthest we’ve travelled</a> since the pandemic began. Halfway through the journey, we stopped at a motorway service station and enjoyed a picnic of cold pizza and peanut butter sandwiches in our car.</p>
</li>
</ul>
Weeknotes 43https://mudge.name/2020/08/23/weeknotes-43/2020-08-23T15:23:00+00:002020-08-23T15:23:00+00:00New experiences, starting over and Daddy Pig holding a mirror up to society.<ul>
<li>
<p>I met a former colleague at a local pub and we sat out front to catch up. After taking my order, a member of staff asked to scan my wrist with an infrared thermometer. This was an entirely new experience and I was quite giddy at having my faculties critically assessed by a beige handheld device.</p>
</li>
<li>
<p>I finished my latest contract on Friday and, due to <a href="/2020/08/16/weeknotes-42/">last week’s preparation</a>, I write these notes on a freshly formatted laptop.</p>
<p>There’s something cathartic about starting afresh, clearing out my old <a href="https://support.1password.com/create-share-vaults/">1Password vaults</a> and asking whether each piece of software <a href="https://konmari.com">sparks joy</a> or not.</p>
</li>
<li>
<p>I went down a <a href="https://github.com/Homebrew/homebrew-cask/pull/88039">bit of a rabbit-hole</a> installing <a href="https://www.backblaze.com">Backblaze</a> via <a href="https://brew.sh">Homebrew</a>.</p>
</li>
<li>
<p>Now that I’ll be helping out with childcare more, I inevitably found myself watching <a href="https://www.peppapig.co.uk">Peppa Pig</a> this morning and was reminded of <a href="https://youtu.be/oZC790urhtw?t=136">its damning satire of knowledge work</a> at Daddy Pig’s office.</p>
</li>
<li>
<p><a href="/2019/01/02/2018-yearnotes/">Once more, I’m not sure what’s next</a> but I’m looking forward to a break and to celebrating C’s first birthday.</p>
</li>
</ul>
Weeknotes 42https://mudge.name/2020/08/16/weeknotes-42/2020-08-16T09:42:00+00:002020-08-16T09:42:00+00:00A void that cannot be filled with ergonomic equipment, tech brain and decapitating cardboard dogs.<ul>
<li>
<p>I made no notes this week.</p>
</li>
<li>
<p><a href="https://ahelwer.ca/post/2020-08-09-home-ergonomics/">Andrew Helwer’s “Taking my home work setup seriously”</a> is making me reconsider the suitability of my <a href="https://www.apple.com/uk/shop/product/MJ2R2Z/A/magic-trackpad-2-silver">Magic Trackpad</a> especially given that I’ve developed some sort of callus between my palm and my wrist.</p>
<p>Then again:</p>
<blockquote>
<p>Inside us all there is a void. People want to complete themselves and fill this void with spirituality, or hedonistic pursuits, or material things. If you’ll indulge a metaphor, this is not a void that can be filled - its nature is more akin to a black hole of the cosmic variety. Feeding it things - for example, expensive ergonomic equipment - will simply add to its mass and pull. Only if left alone might it slowly evaporate. You must learn to live with it.</p>
</blockquote>
</li>
<li>
<p><a href="https://pycnocline.substack.com/p/tech-brain">stephanie’s “tech brain”</a> provoked discussion among myself and some fellow software developers.</p>
<blockquote>
<p>what is tech brain? there are lots of things to point to, but if i had to come up with a thesis it would be that tech brain is a sort of constant willful reductionism: an addiction to easy answers combined with a wholesale cultural resistance to any kind of complexity.</p>
</blockquote>
<p><a href="https://twitter.com/eightbitraptor/status/1293901151591702528">Like</a> <a href="https://twitter.com/tomstuart/status/1293641281667764224">others</a>, I found myself nodding along but <a href="https://zerodivisionerror.neocities.org">Maciej</a> asked an interesting question: how much of this is <em>Twitter</em> brain rather than being specific to the tech industry?</p>
</li>
<li>
<p>I’m currently running a full backup of my laptop with <a href="https://www.shirt-pocket.com/SuperDuper/SuperDuperDescription.html">SuperDuper!</a>. Next week, in a ritual to mark moving on from a client I’ve been working with since March, I’ll wipe my computer clean and start afresh.</p>
</li>
<li>
<p>C’s current favourite activity is to prop himself up on a bookshelf and systematically pull all of the books onto the floor.</p>
<p>Sometimes he wants to read them (or, in the case of certain pop-up books, attempt to decapitate as many cardboard dogs as possible) and sometimes he just wants to see <del>the world burn</del> the shelf emptied.</p>
</li>
</ul>
Weeknotes 41https://mudge.name/2020/08/09/weeknotes-41/2020-08-09T10:12:00+00:002020-08-09T10:12:00+00:00Almost all the colours of the rainbow, what we wear on our feet, reminiscing with Radiohead, René Redzepi and technical tests.<ul>
<li>
<p>C has a set of “First Word” books that he often wants to read. They came in a little paper box so they can be kept together and each one is a different colour. Every time I put them away, I recite the mnemonic “<a href="https://en.wikipedia.org/wiki/ROYGBIV">Richard Of York Gave Battle In</a>” for there are only six.</p>
</li>
<li>
<p>As it has been unusually hot and humid, I had to forego <a href="/2020/03/22/weeknotes-21/">wearing socks</a> and even resort to flip-flops on Friday. This marks my first slipper-free day working from home since March.</p>
</li>
<li>
<p>As the word “flip-flop” is not quite onomatopoeic (or is that <a href="https://en.wikipedia.org/wiki/Echomimetic">echomimetic</a>?) enough for this household, we like to use the much more accurate phrase “snicker-snackers”.</p>
</li>
<li>
<p>Listening to <a href="https://www.adam-buxton.co.uk/podcasts/7-bfk9m-4l8kp-blcga-jwabs-blbb8-b9mjx-r3ftc">Adam Buxton interview Radiohead guitarist Ed O’Brien</a> reminded me of my deep and perhaps-in-retrospect-unhealthy love of <a href="https://www.radiohead.co.uk">the band</a> in my teens.</p>
</li>
<li>
<p>Buxton and O’Brien’s discussion of <a href="https://www.imdb.com/title/tt0816692/">Christopher Nolan’s 2014 film “Interstellar”</a> in relation to parenting reminded me of something I mentioned in <a href="/2019/11/10/weeknotes-2/">my second weeknotes</a>:</p>
<blockquote>
<p>It’s like the experience of being a parent is an exercise in relativity in a way because your sense of time is so different to theirs and it’s so cruel how fast it is for you as an adult and it’s so torturously slow for them.</p>
</blockquote>
</li>
<li>
<p>On a more joyous (but still poignant note), they also mentioned <a href="https://youtu.be/6SFNW5F8K9Y">Tom Petty, Jeff Lynne, Steve Winwood, Dhani Harrison and Prince’s performance of “While My Guitar Gently Weeps” during A Tribute to George Harrison in 2004</a>. Where <em>does</em> that guitar go?</p>
</li>
<li>
<p>We <a href="/2020/08/02/weeknotes-40/">continue to watch “Parts Unknown”</a> and, despite owning <a href="https://www.mendo.nl/product/noma-guide-fermentation/">one of his books</a> and seeing him on countless other cookery shows, I found the episode about <a href="https://noma.dk">Noma</a>’s <a href="https://en.wikipedia.org/wiki/René_Redzepi">René Redzepi</a> fascinating.</p>
<p>It’s one thing to hear the phrase “<a href="https://en.wikipedia.org/wiki/New_Danish_cuisine#New_Nordic_Cuisine">New Nordic Cuisine</a>” over and over but another to hear of Redzepi’s <a href="https://en.wikipedia.org/wiki/René_Redzepi#Early_life_and_education">childhood in Macedonia</a> foraging with his father and watching chefs present their dishes for the scrutiny of their peers:</p>
<blockquote>
<p>And that might well end up on the menu?</p>
</blockquote>
<blockquote>
<p>No. This is not about putting things on the menu. No, I mean, if somebody makes a masterpiece, its <em>their</em> masterpiece.</p>
</blockquote>
<blockquote>
<p><em>Really?</em></p>
</blockquote>
<blockquote>
<p>Yeah, yeah, of course.</p>
</blockquote>
<blockquote>
<p>Isn’t it your historical imperative as the chef to take his good work and innovation and put it on the menu and take credit for it as your own? I mean, that’s the way it’s been done for centuries.</p>
</blockquote>
<blockquote>
<p>This is not the point here.</p>
</blockquote>
<blockquote>
<p>The pursuit of enlightenment and knowledge is its own reward?</p>
</blockquote>
<blockquote>
<p>To me, yes.</p>
</blockquote>
</li>
<li>
<p>I did a technical test this week for a potential client and, in doing so, thought about the difficulty of trying to condense your experience and philosophy of programming into the solution of a canned exercise.</p>
<p>There’s often an expectation from interviewers this is possible: that a candidate can concisely convey their approach and a lot of what you need to know about them with these exercises which are typically blind of context.</p>
<p>Last year, I was doing a lot of recruitment for a client and saw many solutions to the same problem. It’s a variation on one I dare say a lot of Ruby programmers in London have seen: a checkout with various discount rules.</p>
<p>I grew to love the problem after seeing so many people focus on different aspects of it. With each new submission, I got a clearer and clearer idea of what <em>I</em> thought was interesting and what was unimportant. In the end, I was driven to write my own minimal solution: with nothing spare, focussed purely on my view of what the beating heart of the problem was.</p>
<p>In the end though, it was just my opinion. I believe code should be optimised for change (or <a href="https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to">deletion</a>) but predicting exactly <em>which</em> changes it needs to accommodate is extremely hard. In the case of a tech test which contains only business rules and no context, perhaps it borders on the impossible.</p>
</li>
</ul>
Weeknotes 40https://mudge.name/2020/08/02/weeknotes-40/2020-08-02T10:20:00+00:002020-08-02T10:20:00+00:00Not owning a microwave, uncanny turns of phrase, Parts Unknown and the therapeutic power of Taylor Swift.<ul>
<li>
<p>Following <a href="/2020/07/19/weeknotes-38/">the success of our trip north</a>, we met up with my parents for the first time since lockdown began, having a picnic together so they could see their grandson in person after months of only FaceTime calls.</p>
<p>C enjoyed the extra attention and I got to tell my dad off for ignoring social distancing advice.</p>
<p>We came back from the trip with my parents’ old microwave. I’d tell you this is the first time I’ve had a microwave in 11 years but I fear it’s like people who are at pains to tell you they don’t even own a TV.</p>
</li>
<li>
<p>On the radio one morning, someone described tourism as:</p>
<blockquote>
<p>The industry of <em>human</em> [emphasis added] happiness.</p>
</blockquote>
<p>Tough luck, other species.</p>
</li>
<li>
<p>Continuing the theme of uncanny speech, we caught a TV interview with a politician talking about easing lockdown for our country’s beloved “sandwich bars”.</p>
</li>
<li>
<p>E and I began watching “<a href="https://en.wikipedia.org/wiki/Anthony_Bourdain:_Parts_Unknown">Anthony Bourdain: Parts Unknown</a>” after multiple recommendations.</p>
<p>We’ve only watched the first few episodes (originally aired in 2013) but I’m enjoying it so far. I expected a food tourism show with a larger-than-life host (not unlike <a href="https://en.wikipedia.org/wiki/David_Chang">David Chang</a> in “<a href="/2020/03/08/weeknotes-19/">Ugly Delicious</a>”) but I was pleasantly surprised that Bourdain doesn’t dominate the show, instead preferring to let others speak in places like Myanmar and Koreatown.</p>
<p>The amount of truffle and foie gras on display in Quebec was a bit much though.</p>
<p>Seeing him travel the world and listening to his narration, it’s hard not to think about <a href="https://en.wikipedia.org/wiki/Anthony_Bourdain#Death">his death in 2018</a>.</p>
</li>
<li>
<p>This week was dominated by some stressful decisions so I don’t have too much to share. I spent a lot of it agonising over details and going for walks around the block (mostly to the sound of <a href="https://en.wikipedia.org/wiki/Folklore_(Taylor_Swift_album)">Taylor Swift’s “Folklore”</a>) in an attempt to clear my head.</p>
</li>
</ul>
Weeknotes 39https://mudge.name/2020/07/25/weeknotes-39/2020-07-25T10:48:00+00:002020-07-25T10:48:00+00:00Nurseries, unwinding with wooden blocks, things people say to you when you wear a baby carrier and a computation anniversary.<ul>
<li>
<p>Working from home with C in the house can be a strange experience. After a morning of calls about <a href="https://webpack.js.org/guides/tree-shaking/">tree shaking</a>, <a href="https://twitter.com/qrlitycontrol">QR codes</a> and <a href="https://ethereum.stackexchange.com/questions/3/what-is-meant-by-the-term-gas">gas fees</a>, I’ll leave my desk and upon seeing him for the first time in a few hours, I’ll ask E, “whose baby is that?”</p>
</li>
<li>
<p>As E will be going back to work soon, we’ve been trying to find a nursery for C to attend three days a week. We were lucky enough to get more than one place for him but hadn’t seen one of the options due to the lockdown. This week, we arranged to have a look around (wearing masks, of course) and, after much deliberation, made our decision and paid our deposit.</p>
<p>(It still doesn’t feel real.)</p>
</li>
<li>
<p>After a stressful day, there’s no better way to unwind than by meticulously organising the wooden blocks in C’s walker.</p>
<p class="center"><img src="/i/blocks.jpg" width="375" height="375" alt="" /></p>
</li>
<li>
<p>During one of my lunchtime walks with C, he was fascinated by a passing bin lorry. One of the dustmen noticed him watching and waved emphatically.</p>
</li>
<li>
<p>During another stroll, a man crossing the street cooed at C in his baby carrier and gleefully called for all to hear, “look at his chubby cheeks!”</p>
</li>
<li>
<p>On a call with a potential vendor this week, when discussing the user experience of a particular system, their representative said:</p>
<blockquote>
<p>Sure, if you want to cater to Joe Schmoe… from Kokomo.</p>
</blockquote>
</li>
<li>
<p>It was the third anniversary of the <a href="https://github.com/computationclub/computationclub.github.io/wiki/Bletchley-Park">London Computation Club trip to Bletchley Park</a>.</p>
<p class="center"><img src="/i/bletchley.jpg" width="512" height="384" alt="" /></p>
<p>Just look at us all: bright-eyed, bushy-tailed and sitting so close to one another.</p>
</li>
</ul>
Weeknotes 38https://mudge.name/2020/07/19/weeknotes-38/2020-07-19T10:10:00+00:002020-07-19T10:10:00+00:00Gold trees, some small sense of freedom, quartz glass platters and the villain in your history.<ul>
<li>
<p>After feeling <a href="/2020/07/12/weeknotes-37/">“out of sorts” last week</a> due to the monotony of life during the pandemic, I decided to go for a walk with C every lunchtime this week. I get a change of scenery and he gets to babble at dog walkers.</p>
<p>Now that <a href="https://alicebartlett.co.uk/blog/weaknotes-98">Alice Bartlett’s “Week 98: Trees”</a> has introduced me to <a href="https://www.treetalk.co.uk">TreeTalk</a>, I might alter my route to investigate one of only two <a href="https://www.rhs.org.uk/plants/14003/Prunus-lusitanica/Details">Portuguese laurel trees</a> in London.</p>
</li>
<li>
<p>E and I decided to make our first trip outside London since lockdown began. We drove north for two hours and E’s family drove south for two hours and we all met for a picnic in the grounds of a country house.</p>
<p>As it seems unlikely we’ll be able to spend more time together soon, it felt good to exercise some small amount of freedom after feeling hopelessly trapped.</p>
<p>It has given us the confidence to get away for C’s first birthday and be closer to family so we won’t have to celebrate it purely virtually as we did for both <a href="/2020/04/12/weeknotes-24/">E’s</a> and <a href="/2020/05/24/weeknotes-30/">my birthday</a> this year.</p>
</li>
<li>
<p>This week saw the <a href="https://github.blog/2020-07-16-github-archive-program-the-journey-of-the-worlds-open-source-code-to-the-arctic/">introduction of “Arctic Code Vault Badges” on GitHub</a>, meaning open source software I wrote made it into the <a href="https://archiveprogram.github.com">GitHub Archive Program</a>.</p>
<p>More specifically, it is stored on “<a href="https://www.piql.com">3,500-foot film reels</a>” and has been written “into <a href="https://www.microsoft.com/en-us/research/project/project-silica/">quartz glass platters using a femtosecond laser</a>” with the intent of being archived for “over 10,000 years”.</p>
<p>Let future historians judge the value of <a href="https://github.com/rbenv/rbenv/commit/b7e19b4953f6f9c3377781342e517c78ebf27bce">adding Solaris support to rbenv</a> or <a href="https://github.com/rails/rails/commit/58e21a4a0d4eefc395139e88c1f184b9eaf0b4c4">passing blocks to <code class="language-plaintext highlighter-rouge">ActiveSupport::OrderedHash#merge</code></a>.</p>
</li>
<li>
<p>I’m currently avoiding tweeting my weeknotes as my mentions are full of <a href="https://twitter.com/hexadecim8/status/1281224429863919619">people mistaking me</a> for <a href="https://en.wikipedia.org/wiki/Peiter_Zatko">Peiter Zatko</a>.</p>
</li>
<li>
<p>After adding <code class="language-plaintext highlighter-rouge">bamtech.sc.omtrdc.net</code> to my <a href="/2020/06/07/weeknotes-32/">DNS sinkhole</a>’s safelist, we watched <a href="https://www.disneyplus.com/movies/hamilton/3uPmBHWlO6HJ">“Hamilton” on Disney+</a> having previously seen it <a href="https://hamiltonmusical.com/london/home/">on stage in London</a> a year ago. It’s still <a href="https://twitter.com/mudge/status/983827873063690240">rather good</a>.</p>
</li>
</ul>
Weeknotes 37https://mudge.name/2020/07/12/weeknotes-37/2020-07-12T13:55:00+00:002020-07-12T13:55:00+00:00How I discovered adventure games, saving your life, converting cups of sifted flour into moon masses and feeling out of sorts.<ul>
<li>
<p>The <a href="https://twitter.com/DoubleFine">Double Fine Productions</a> remasters of classic <a href="https://en.wikipedia.org/wiki/LucasArts">LucasArts</a>’ adventure games “<a href="http://dott.doublefine.com">Day of the Tentacle</a>”, “<a href="http://fullthrottle.doublefine.com">Full Throttle</a>” and “<a href="http://grimremastered.com">Grim Fandango</a>” (as well as Double Fine’s own, more recent adventure game “<a href="http://www.brokenagegame.com">Broken Age</a>”) are all <a href="https://twitter.com/DoubleFine/status/1281618886744576003">free on the Mac App Store this weekend</a>.</p>
<p>When I was a child, my mum came back from <a href="https://en.wikipedia.org/wiki/PC_World_(retailer)">PC World</a> with a copy of <a href="https://en.wikipedia.org/wiki/The_Lion_King_(video_game)">“The Lion King” video game</a> on <a href="https://en.wikipedia.org/wiki/Floppy_disk">3½-inch floppy disks</a>. Sadly, I had broken my disk drive when the sliding metal shutter from a diskette got stuck inside. My mum returned to the shop and swapped it for “<a href="https://en.wikipedia.org/wiki/Day_of_the_Tentacle">Day of the Tentacle</a>” on <a href="https://en.wikipedia.org/wiki/CD-ROM">CD-ROM</a> and my love of adventure games began.</p>
<p>In the words of the mechanics from one of my personal favourites:</p>
<blockquote>
<p>We shoot you now like an arrow into the wind. May you pierce the heart of the wind itself, and drink the blood of flight.</p>
</blockquote>
</li>
<li>
<p>The <a href="http://adam-buxton.co.uk/podcasts/ep126-joe-cornish">latest episode of Adam Buxton’s podcast</a> where he discusses the recent death of his mother with Joe Cornish is worth your time.</p>
</li>
<li>
<p>Reading <a href="https://web.dev/one-line-layouts/">Una Kravets’ “Ten modern layouts in one line of CSS”</a> drives home how woefully out-of-date my CSS knowledge is.</p>
</li>
<li>
<p>I caught C as he toppled backwards in surprise while sat on his mat and, without pausing, E said: “tell your mum I saved your life.”</p>
</li>
<li>
<p><a href="https://www.hillelwayne.com/post/frink/">Hillel Wayne’s “The Frink Is Good, the Unit Is Evil”</a> introduced me to <a href="https://frinklang.org">Frink</a>, a “special purpose [language] for dealing with units”.</p>
<p>There’s a <a href="https://futureboy.us/fsp/frink.fsp">web-based interface to Frink</a> available and it supports <a href="https://frinklang.org/frinkdata/units.txt">a dizzying number of units</a>. As someone who often has to convert <a href="https://twitter.com/adamliaw/status/1255608557027749888">American recipe quantities</a>, it will come in <a href="https://futureboy.us/fsp/frink.fsp?fromVal=4+tbsp+butter&toVal=grams#calc">very handy indeed</a>.</p>
<p>Of course, it was named after <a href="https://en.wikipedia.org/wiki/Professor_Frink">Professor John Frink</a>:</p>
<blockquote>
<p>I predict that within 100 years, computers will be twice as powerful, ten thousand times larger, and so expensive that only the five richest kings of Europe will own them.</p>
</blockquote>
</li>
<li>
<p>After an intense week at work, I feel somewhat out of sorts and so decided to take a long, meandering walk and catch up on some <a href="https://atp.fm">podcasts</a> while E took C to visit her aunt.</p>
<p>I made it as far as the local reservoir before a group of people gathered in my path made me turn back. Turns out I’m neither good at meandering nor staying outside the house for longer than half an hour.</p>
</li>
</ul>
Weeknotes 36https://mudge.name/2020/07/05/weeknotes-36/2020-07-05T09:19:00+00:002020-07-05T09:19:00+00:00Visiting other islands, comfort food TV, a market trader mystery, not understanding prescriptions and unexpected sobbing.<ul>
<li>
<p><a href="/2020/05/24/weeknotes-30/">Since getting “Animal Crossing” over a month ago</a>, I’ve gone through phases with it: at first, learning how to play then relentlessly <a href="https://tomstu.art/weeknotes-15-what-a-ride">plundering other islands for resources</a> then <a href="/2020/06/07/weeknotes-32/">checking turnip prices twice a day and having a bad time on the Stalk Market</a>. After feeling a little burnt out, I let go of all material concerns and simply fished to my heart’s content.</p>
<p>This week I visited some friends’ islands for the very first time and hosted <a href="https://twitter.com/ShaneOnSwitch/status/1278770022081388544">my first guest</a>. This social aspect is entirely new to me and, having seen how beautiful others’ islands are, you will now find me happily planting imaginary flowers.</p>
<p class="center"><a href="https://twitter.com/ShaneOnSwitch/status/1278770022081388544"><img src="/i/animalcrossing.jpg" width="375" height="211" alt="" /></a></p>
</li>
<li>
<p>We had occasion to use our car for the first time in months and weren’t surprised when it failed to start. Thankfully, I’ve had to charge our car battery several times before and so painstakingly removed it from the car after reading <a href="https://www.wikihow.com/Disconnect-a-Car-Battery">the order in which to disconnect the two terminals</a> at least six times (negative then positive and then the reverse order to reconnect).</p>
<p>There’s nothing like bringing a metal socket spanner toward a car battery terminal for focussing the mind.</p>
</li>
<li>
<p><a href="/2020/06/28/weeknotes-35/">Last week</a> I credited <a href="http://www.h-lame.com/">Murray</a> with describing modern web development as “putting strings into databases and getting strings out of databases” but <a href="https://twitter.com/hlame/status/1277179500510543875">it turns out</a> he was paraphrasing <a href="https://medium.com/@abscond/putting-strings-into-databases-and-then-taking-them-back-out-again-ce95c2ad2183">James Darling</a> who, <a href="https://twitter.com/abscond/status/1277182825016557569">in turn</a>, was quoting <a href="https://twitter.com/beng/status/1277575198338883584">Ben Griffiths</a>.</p>
</li>
<li>
<p>There’s a new series of <a href="https://www.netflix.com/title/80146601">“Everybody Loves Raymond” creator Phil Rosenthal’s travel documentary “Somebody Feed Phil”</a>. I find all food-based TV shows comforting but Rosenthal’s genuine, unbridled glee makes it especially optimistic viewing. I forgot how <a href="https://twitter.com/PhilRosenthal/status/949471533994594304">catchy the theme song is</a>.</p>
</li>
<li>
<p>Speaking of food TV, a recent episode of “<a href="https://www.channel4.com/programmes/bake-off-the-professionals">Bake Off: The Professionals</a>” featured host <a href="https://www.tomindeed.com">Tom Allen</a> posing as a market trader. When I lived in south-east London, I used to walk past a market stall selling fruit every day and the trader would bellow a single phrase:</p>
<blockquote>
<p>Tray of bananas, sayp!</p>
</blockquote>
<p>For literal years, I could never decode what this last word was.</p>
<p>Finally, after nearly four years, I happened to hear the trader’s bark one more time:</p>
<blockquote>
<p>Box of strawberries a pound, tray of bananas same!</p>
</blockquote>
<p>The mystery was solved.</p>
</li>
<li>
<p>I decided to get a repeat prescription for my <a href="/2020/03/29/weeknotes-22/">occasional migraines</a> and struggled to navigate the different websites and iOS apps required to talk to a doctor.</p>
<ul>
<li>At first, I tried the <a href="https://www.mygp.com">myGP app</a> which I’ve used to book appointments before but nothing was visible there.</li>
<li>I then tried to use the <a href="https://www.patientaccess.com">Patient Access</a> website but still nothing was available.</li>
<li>I then followed instructions to set up the <a href="https://www.nhs.uk/using-the-nhs/nhs-services/the-nhs-app/">NHS app</a> but, again, nothing.</li>
<li>I rang my local surgery and they told me I needed to use a <em>fourth</em> service: <a href="https://demo.webgp.com">WebGP</a>.</li>
</ul>
<p>After filling out a long questionnaire (and tweaking my answers so it didn’t tell me to immediately ring 999 for an ambulance), I got a call from a GP that same morning who happily created a repeat prescription for me.</p>
<p>However, it then became obvious I have no idea how repeat prescriptions work (a familiar fact given I was once shouted out over the phone by a receptionist for not understanding the difference between the <em>prescription</em> and the medication itself). The doctor said they had created the prescription but what did that mean? Looking in the NHS app, I could see the prescription but there was a separate section for “orders”; did I need to order it? After finding no answers online, I went ahead and ordered it.</p>
<p>The next day, my order was marked as “rejected” in the NHS app and I was instructed to ring my GP. After a hilarious exchange as my local surgery attempted to verify my email address and I ran through the four separate addresses I used for each of their services, it turns out I <em>didn’t</em> need to order anything: my prescription was waiting for me at the local pharmacy.</p>
<p>Utterly baffling.</p>
</li>
<li>
<p>Whenever E and I are discussing a topic that requires some disclaimer (e.g. a relatively petty matter given the current pandemic), we’ll preface what we say with the single word “Rigby”, e.g.</p>
<blockquote>
<p>Rigby, I wish there was more variety to our day.</p>
</blockquote>
<p>This <a href="/2020/03/02/weeknotes-18/">incomprehensive phrase</a> comes from <a href="https://youtu.be/IAGoLFCgNtY">an episode of “Silicon Valley”</a>:</p>
<blockquote>
<p>“I know we keep saying this but even though Richard is a great guy—”</p>
<p>“And a brilliant coder.”</p>
<p>“Wonderful. Nevertheless, he is lacking in certain managerial capacities.”</p>
<p>“Totally fair.”</p>
<p>“It’s fair, right?”</p>
<p>“He’s still a great coder.”</p>
<p>“And an amazing human being!”</p>
<p>“OK, we have a lot of shit we want to say about him, do we have to keep prefacing it with all this nice guy stuff? I mean, if so, we’re going to be here all night. […] What if we use like a dictionary patch to compress all the nice guy stuff?”</p>
<p>“Like an acronym?”</p>
<p>“Exactly. Richard Is Great But, Y’know. RIGBY.”</p>
</blockquote>
</li>
<li>
<p>Despite being put off by the trailer, E and I watched “<a href="https://www.netflix.com/title/80244088">Eurovision Song Contest: The Story of Fire Saga”</a> last night after hearing recommendations from several people.</p>
<p>It reduced us both to actual tears.</p>
</li>
</ul>
Weeknotes 35https://mudge.name/2020/06/28/weeknotes-35/2020-06-28T09:40:00+00:002020-06-28T09:40:00+00:00Pixels per inch, kicking up the 4d3d3d3, the importance of ventilation and genuine Computer Science.<ul>
<li>
<p><a href="https://tonsky.me/blog/monitors/">Nikita Prokopov’s “Time to upgrade your monitor”</a> finally convinced me to upgrade to <a href="https://www.dell.com/en-uk/shop/dell-24-ultra-hd-4k-monitor-p2415q/apd/210-adzc/monitors-monitor-accessories">a 4K display</a> after over a decade of staring at less than <a href="https://www.sven.de/dpi/">100 pixels per inch</a> on an old <a href="https://www.benq.eu/en-eu/index.html">BenQ</a> monitor.</p>
<p>In the words of <a href="https://alexspeller.com">Alex</a>, I’ve been <a href="https://twitter.com/alexspeller/status/288661716710068224">punching myself in the eyes with squares</a> for too long.</p>
</li>
<li>
<p>I was <a href="https://twitter.com/siracusa/status/1274765016655372289">reminded</a> that “<a href="https://youtu.be/MHWBEK8w_YY">Celery Man</a>” exists and it brought me great joy.</p>
</li>
<li>
<p>For the past few weeks, my laptop has been increasingly struggling when running Zoom calls with multiple video streams, screen sharing, etc. with its performance dragging to a halt and the <a href="https://twitter.com/Kolokodess/status/1275879372830973952">fan noise approaching that of a jet engine</a>.</p>
<p>Receiving the new monitor and seeing photos of <a href="https://tomstu.art">Tom</a>’s setup inspired me to remove the protective plastic case from my computer and stand it on its end with the help of an old, wooden iPad stand. The roaring fan noise is now gone and my performance woes are no more.</p>
<p>In retrospect, it probably wasn’t a great idea covering up the few vents on the underside of my MacBook Pro for several years.</p>
</li>
<li>
<p>A former colleague asked me for a copy of a presentation I had given at <a href="https://www.altmetric.com">Altmetric</a> about using <a href="https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm">Dijkstra’s algorithm</a> to exhaustively map scholarly identifiers to one another (e.g. <a href="https://www.doi.org">DOI</a>s to <a href="https://pubmed.ncbi.nlm.nih.gov">PubMed</a> IDs to <a href="http://handle.net">Handle</a>s to <a href="https://en.wikipedia.org/wiki/International_Standard_Book_Number">ISBNs</a> to <a href="https://arxiv.org/help/arxiv_identifier">arXiv identifiers</a>).</p>
<p>That project was so much fun to work on and led me to write <a href="https://github.com/mudge/fibonacci_heap">a Fibonnaci Heap implementation</a>.</p>
<p>To paraphrase <a href="http://www.h-lame.com">Murray</a>: so much of modern software development on the web is putting strings <em>in</em> to databases and getting strings <em>out</em> of databases so it is a rare treat to work on a problem that requires <a href="https://en.wikipedia.org/wiki/Graph_(abstract_data_type)">genuine Computer Science</a> to solve.</p>
</li>
<li>
<p>Apologies for the brevity of this week’s notes: I have started working on my <a href="/2019/11/04/weeknotes-1/">unfinished article about React Hooks</a> again.</p>
</li>
<li>
<p>I enjoyed my very first Fathers’ Day.</p>
</li>
</ul>
Weeknotes 34https://mudge.name/2020/06/21/weeknotes-34/2020-06-21T10:55:00+00:002020-06-21T10:55:00+00:00Shia LaBeouf, bad bakes, vertical rhythm, finding a stranger in the Alps and the streetlight effect.<ul>
<li>
<p>While making coffee with C in my arms one morning, he suddenly lunged for the <a href="https://www.chemexcoffeemaker.com">Chemex</a>, spilling a slurry of hot water and coffee grounds as I shouted “no no no no no!” A stunned E declared she would now call me <a href="https://youtu.be/8IXCK1EyP4s">Shia LaBeouf</a> but thankfully that has not happened.</p>
</li>
<li>
<p>I can’t mention Shia LaBeouf without recommending <a href="https://youtu.be/o0u4M6vppCI">Rob Cantor’s song about the true story of an actual cannibal</a>.</p>
</li>
<li>
<p>I’ve been making sourdough bread since March 2012 <a href="https://www.theguardian.com/us-news/2020/apr/19/coronavirus-stress-baking-sourdough-kneading-relax">long before it became cool</a>, even <a href="https://tomstu.art/weeknotes-3-sweating-and-swearing">encouraging others to get into it</a> but my recent bakes have not been good.</p>
<p>My starter has been underactive and overactive, the bulk fermentation has been both too long and too short. More often than not, my bread is what <a href="http://www.h-lame.com">Murray</a> would call “a tasty cowpat”.</p>
<p>As others got into baking, measuring their dough temperature with infrared thermometers and making their own blend of flour based on exacting protein levels, I was dismissive, believing only in some woolly notion of technique. Those bakers now produce consistently great loaves; each of <a href="https://twitter.com/cassarani">Leo</a>’s bakes is more beautiful than the last.</p>
<p>Perhaps I should get with the times and be less <a href="https://oneforkonespoon.wordpress.com/2009/04/24/the-taste-of-your-hands/">“son-mat”</a> and more <a href="https://en.wikipedia.org/wiki/Molecular_gastronomy">molecular gastronomy</a>.</p>
</li>
<li>
<p>After correcting some typos in <a href="/2020/06/14/weeknotes-33/">last week’s notes</a>, I enabled <a href="http://vimdoc.sourceforge.net/htmldoc/spell.html">Vim’s spell checking</a>. The following bit of configuration now highlights any misspelt words in <a href="https://daringfireball.net/projects/markdown/">Markdown</a> files:</p>
<div class="language-vimscript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>autocmd <span class="nb">BufRead</span><span class="p">,</span><span class="nb">BufNewFile</span> *<span class="p">.</span>markdown<span class="p">,</span>*<span class="p">.</span>md <span class="k">setlocal</span> <span class="nb">spell</span> <span class="nb">spelllang</span><span class="p">=</span>en_gb
</code></pre></div> </div>
<p>E tells me I should look into something called a “word processor”.</p>
</li>
<li>
<p>I changed both this site and the <a href="https://www.ghostcassette.com">Ghost Cassette</a> website to have a more consistent <a href="https://zellwk.com/blog/why-vertical-rhythms/">vertical rhythm</a>. I found this little snippet of CSS to display a baseline grid extremely useful when trying to debug spacing issues:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">body</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="n">linear-gradient</span><span class="p">(</span><span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0.3</span><span class="p">)</span> <span class="m">1px</span><span class="p">,</span> <span class="nb">transparent</span> <span class="m">1px</span><span class="p">);</span>
<span class="nl">background-size</span><span class="p">:</span> <span class="m">100%</span> <span class="m">24px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div> </div>
</li>
<li>
<p>C’s latest discovery is clapping. I recommend everyone indulge in a round of applause every meal and at random times throughout the day.</p>
</li>
<li>
<p>I’ve been listening to a lot of <a href="https://phoebefuckingbridgers.com">Phoebe Bridgers</a> as her new album “Punisher” came out this week. I dearly hope her 2018 album “Stranger in the Alps” is named after the <a href="https://youtu.be/LCcKBcZzGdA">censored TV edit of “The Big Lebowski”</a>.</p>
</li>
<li>
<p>I’ve been thinking about <a href="https://en.m.wikipedia.org/wiki/Streetlight_effect">the streetlight effect</a> and problem solving. There are <a href="https://quoteinvestigator.com/2013/04/11/better-light/">many versions</a> of the story but here’s “<a href="https://en.m.wikipedia.org/wiki/Nasreddin#Nasreddin's_ring">Nasreddin’s ring</a>”:</p>
<blockquote>
<p>Mulla had lost his ring in the living room. He searched for it for a
while, but since he could not find it, he went out into the yard and
began to look there. His wife, who saw what he was doing, asked: “Mulla,
you lost your ring in the room, why are you looking for it in the yard?”
Mulla stroked his beard and said: “The room is too dark and I can’t see
very well. I came out to the courtyard to look for my ring because there
is much more light out here.”</p>
</blockquote>
<p>As a software developer, I wonder if there is a trap as you solve similar problems over and over again during the course of your career. Do you become more likely to take <em>any</em> problem and turn it into a familiar one even if it’s not appropriate?</p>
</li>
</ul>
Weeknotes 33https://mudge.name/2020/06/14/weeknotes-33/2020-06-14T11:03:54+00:002020-06-14T11:03:54+00:00Generating human-friendly codes, keeping my cool running a Raspberry Pi and threatening my marriage.<ul>
<li>
<p>I’ve been researching how best to generate unique, human-friendly codes for a new project.</p>
<p><a href="https://medium.com/@scottm">Scott</a> recommended we take a look at <a href="https://www.crockford.com/base32.html">Douglas Crockford’s “Base 32” system</a>:</p>
<blockquote>
<p>The encoding scheme is required to</p>
<ul>
<li>Be human readable and machine readable.</li>
<li>Be compact. Humans have difficulty in manipulating long strings of arbitrary symbols.</li>
<li>Be error resistant. Entering the symbols must not require keyboarding gymnastics.</li>
<li>Be pronounceable. Humans should be able to accurately transmit the symbols to other humans using a telephone.</li>
</ul>
</blockquote>
<p>Crockford excludes the letters I, L and O to avoid ambiguity with the numbers 1 and 0. Less intuitively, he also excludes the letter U as this helps avoid accidentally generating codes that contain English obscenities: a problem I would have missed entirely.</p>
<p>We looked into the implementation of <a href="https://api.rubyonrails.org/classes/SecureRandom.html#method-c-base58">Rails’ <code class="language-plaintext highlighter-rouge">SecureRandom.base58</code></a>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">base58</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="mi">16</span><span class="p">)</span>
<span class="no">SecureRandom</span><span class="p">.</span><span class="nf">random_bytes</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">unpack</span><span class="p">(</span><span class="s2">"C*"</span><span class="p">).</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">byte</span><span class="o">|</span>
<span class="n">idx</span> <span class="o">=</span> <span class="n">byte</span> <span class="o">%</span> <span class="mi">64</span>
<span class="n">idx</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">random_number</span><span class="p">(</span><span class="mi">58</span><span class="p">)</span> <span class="k">if</span> <span class="n">idx</span> <span class="o">>=</span> <span class="mi">58</span>
<span class="no">BASE58_ALPHABET</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
<span class="k">end</span><span class="p">.</span><span class="nf">join</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>I wondered why they didn’t simply <a href="https://en.wikipedia.org/wiki/Modulo_operation">modulo</a> <code class="language-plaintext highlighter-rouge">byte</code> with 58 like so:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">base58</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="mi">16</span><span class="p">)</span>
<span class="no">SecureRandom</span><span class="p">.</span><span class="nf">random_bytes</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="nf">unpack</span><span class="p">(</span><span class="s2">"C*"</span><span class="p">).</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">byte</span><span class="o">|</span>
<span class="n">idx</span> <span class="o">=</span> <span class="n">byte</span> <span class="o">%</span> <span class="mi">58</span>
<span class="no">BASE58_ALPHABET</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span>
<span class="k">end</span><span class="p">.</span><span class="nf">join</span>
<span class="k">end</span>
</code></pre></div> </div>
<p>Scott found the explanation in <a href="https://github.com/rails/rails/pull/25734">a four-year old pull request to simplify the implementation</a>: <a href="https://cmvandrevala.wordpress.com/2016/09/24/modulo-bias-when-generating-random-numbers/">modulo bias</a>. Naïvely simplifying the implementation would mean that the “random” choice would actually be biased towards picking some characters more than others.</p>
<p>Coincidentally, <a href="https://github.com/rails/rails/pull/39511">someone else suggested the same change again</a> 12 days ago.</p>
</li>
<li>
<p>After <a href="/2020/06/07/weeknotes-32/">last week’s big migration to Fargate</a>, I wanted to double check the new <a href="https://www.docker.com">Docker</a> version of our application wasn’t writing to disk (in order to remain <a href="https://12factor.net/processes">stateless</a>).</p>
<p>The <a href="https://docs.docker.com/engine/reference/commandline/diff/">docker diff</a> command handily told me exactly what a container was changing on its file system when running and revealed that <a href="https://www.phusionpassenger.com/library/admin/nginx/log_file/#location-of-the-log-file">Phusion Passenger was logging to a file when I didn’t expect it to</a>.</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>docker diff 1fdfd1f54c1b
<span class="go">A /var/log/nginx/error.log
</span></code></pre></div> </div>
</li>
<li>
<p>E has been binging the French spy drama “<a href="https://www.imdb.com/title/tt4063800/">The Bureau</a>” but I can’t help but think of <a href="https://youtu.be/WjjXTr9F77g">The Day Today’s BBC drama of the same name</a> every time I see her watching it:</p>
<blockquote>
<p>This is supposed to be a high class <em>bureau de change</em>, not some two-bit Punch and Judy show down on the seafront at Margate!</p>
</blockquote>
</li>
<li>
<p><a href="https://www.jeffgeerling.com/blog/2019/best-way-keep-your-cool-running-raspberry-pi-4">Jeff Geerling’s “The best way to keep your cool running a Raspberry Pi 4”</a> convinced me to buy the <a href="https://thepihut.com/products/flirc-raspberry-pi-4-case">FLIRC case from The Pi Hut</a> and he’s right: my Pi runs much cooler than my previous setup with a <a href="https://thepihut.com/products/xl-raspberry-pi-4-heatsink">large heatsink</a> inside <a href="https://thepihut.com/products/raspberry-pi-4-case">the official case</a>:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">mudge@raspberrypi:~ $</span><span class="w"> </span><span class="nb">cat</span> /sys/class/thermal/thermal_zone0/temp
<span class="go">36998
</span></code></pre></div> </div>
</li>
<li>
<p>In a move that threatens my marriage, I have spent far too much time this weekend trying to generate a custom <a href="https://www.raspbian.org">Raspbian</a> image that already has my <a href="/2019/11/12/using-a-raspberry-pi-for-time-machine/">Time Machine backups</a>, <a href="/2020/06/07/weeknotes-32/">Unbound DNS sinkhole</a> and <a href="https://github.com/mikebrady/shairport-sync">AirPlay receiver</a> set up on it.</p>
<p>Thankfully, the <a href="https://github.com/RPi-Distro/pi-gen">tool used to create the raspberrypi.org images</a> is open-source and you can find the results of my indulgent noodling in <a href="https://github.com/mudge/pi-gen">my fork of pi-gen</a>.</p>
</li>
<li>
<p><a href="/2020/06/07/weeknotes-32/">Last week’s investment</a> paid off.</p>
<p class="center"><img src="/i/turnips.jpg" width="375" height="375" alt="I sold a batch of my turnips for 803,400 Bells." /></p>
</li>
</ul>
Weeknotes 32https://mudge.name/2020/06/07/weeknotes-32/2020-06-07T14:18:00+00:002020-06-07T14:18:00+00:00Replacing a Pi-hole with Unbound and ISC DHCP, speeding up deployments and a reckless root vegetable investment.<ul>
<li>
<p><a href="https://blog.may.yt/2020/06/pdns-sinkhole/">Thom May’s “Sinkholing with PowerDNS Recursor”</a> made me wonder why <a href="/2020/04/12/weeknotes-24/">I run both a Pi-hole <em>and</em> our own Unbound DNS resolver</a>. Perhaps I could implement <a href="https://firebog.net">blocklists</a> and <a href="https://github.com/anudeepND/whitelist">recommended exceptions</a> using only <a href="https://nlnetlabs.nl/documentation/unbound/unbound.conf/">Unbound configuration</a>?</p>
<p>Our Pi-hole currently serves two purposes:</p>
<ol>
<li>It runs <a href="https://discourse.pi-hole.net/t/how-do-i-use-pi-holes-built-in-dhcp-server-and-why-would-i-want-to/3026">its built-in DHCP server</a> so every device connecting to our network uses the Pi-hole’s DNS server rather than my ISP’s DNS servers (as <a href="https://community.bt.com/t5/Archive-Staging/Manual-DNS-settings-please-for-home-hub-5/td-p/1226381">this isn’t configurable on my router</a>).</li>
<li>It runs its <a href="https://docs.pi-hole.net/ftldns/blockingmode/">own <abbr title="Faster Than Light">FTL</abbr>DNS server to block access to domains</a>.</li>
</ol>
<p>As Thom points out in his post, given only those two roles and the fact I’m already running <a href="https://nlnetlabs.nl/projects/unbound/about/">Unbound</a>, the required tech stack for the Pi-hole (<a href="https://www.php.net">PHP</a>, <a href="https://www.lighttpd.net">Lighttpd</a>, <a href="http://www.thekelleys.org.uk/dnsmasq/doc.html">Dnsmasq</a>, etc.) seems excessive.</p>
<p>I decided to replace the Pi-hole with only two pieces of software:</p>
<ol>
<li>Use <a href="https://www.isc.org/dhcp/">ISC DHCP</a> as a standalone DHCP server, replacing the Pi-hole’s Dnsmasq-based DHCP server.</li>
<li>Use the existing Unbound DNS server to block unwanted domains.</li>
</ol>
<p><a href="https://firebog.net">The Firebog’s “The Big Blocklist Collection”</a> contains a curated list of domains to block, all in the following format:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0.0.0.0 some.bad.domain.here
0.0.0.0 another.bad.domain.here
</code></pre></div> </div>
<p>In order to block these domains with Unbound, we can use the <a href="https://nlnetlabs.nl/documentation/unbound/unbound.conf/#local-zone"><code class="language-plaintext highlighter-rouge">local-zone</code></a> and <a href="https://nlnetlabs.nl/documentation/unbound/unbound.conf/#local-zone"><code class="language-plaintext highlighter-rouge">local-data</code></a> configuration options. The documentation for <code class="language-plaintext highlighter-rouge">local-zone</code>’s <code class="language-plaintext highlighter-rouge">redirect</code> type contains the following example:</p>
<blockquote>
<p>It can be used to redirect a domain to return a different address record
to the end user, with <code class="language-plaintext highlighter-rouge">local-zone: "example.com." redirect</code> and
<code class="language-plaintext highlighter-rouge">local-data: "example.com. A 127.0.0.1"</code> queries for www.example.com and
www.foo.example.com are redirected, so that users with web browsers
cannot access sites with suffix example.com.</p>
</blockquote>
<p>We’ll <a href="https://docs.pi-hole.net/ftldns/blockingmode/#pi-holes-unspecified-ip-blocking-default">copy Pi-hole’s unspecified IP blocking strategy</a> and return <code class="language-plaintext highlighter-rouge">0.0.0.0</code> for any A record lookups and <code class="language-plaintext highlighter-rouge">::</code> for any AAAA record lookups:</p>
<blockquote>
<p>Following <a href="https://tools.ietf.org/html/rfc3513#section-2.5.2">RFC 3513, Internet Protocol Version 6 (IPv6) Addressing
Architecture, section
2.5.2</a>, the address
<code class="language-plaintext highlighter-rouge">0:0:0:0:0:0:0:0</code> (or <code class="language-plaintext highlighter-rouge">::</code> for short) is the unspecified address. It must
never be assigned to any node and indicates the absence of an address.
Following RFC1122, section 3.2, the address <code class="language-plaintext highlighter-rouge">0.0.0.0</code> can be understood
as the IPv4 equivalent of <code class="language-plaintext highlighter-rouge">::</code>.</p>
</blockquote>
<p>So to block the two example domains above, we would need the following Unbound configuration:</p>
<div class="language-config highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">local</span>-<span class="n">zone</span>: <span class="s2">"some.bad.domain.here."</span> <span class="n">redirect</span>
<span class="n">local</span>-<span class="n">data</span>: <span class="s2">"some.bad.domain.here. IN A 0.0.0.0"</span>
<span class="n">local</span>-<span class="n">data</span>: <span class="s2">"some.bad.domain.here. IN AAAA ::"</span>
<span class="n">local</span>-<span class="n">zone</span>: <span class="s2">"another.bad.domain.here."</span> <span class="n">redirect</span>
<span class="n">local</span>-<span class="n">data</span>: <span class="s2">"another.bad.domain.here. IN A 0.0.0.0"</span>
<span class="n">local</span>-<span class="n">data</span>: <span class="s2">"another.bad.domain.here. IN AAAA ::"</span>
</code></pre></div> </div>
<p>We can do this by stitching together some basic command-line tools:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sSf</span> <span class="s2">"https://v.firebog.net/hosts/static/w3kbl.txt"</span> | <span class="c"># Download a blocklist</span>
<span class="nb">grep</span> <span class="s1">'^0\.0\.0\.0'</span> | <span class="c"># Filter out any comments, etc. that aren't rules</span>
<span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\r'</span> | <span class="c"># Normalize line endings by removing carriage returns</span>
<span class="nb">sort</span> <span class="nt">-u</span> | <span class="c"># Remove any duplicates</span>
<span class="nb">awk</span> <span class="s1">'{print "local-zone: \""$2".\" redirect\nlocal-data: \""$2". IN A 0.0.0.0\"\nlocal-data: \""$2". IN AAAA ::\""}'</span> | <span class="c"># Convert to Unbound configuration</span>
</code></pre></div> </div>
<p>(You can find my <a href="https://gist.github.com/mudge/eaff0a816e8ee49046e263b0a6e99fe7#file-blocklist-sh">full script to download and re-format all ticked blocklists on GitHub</a>.)</p>
<p>As the blocklists contain domains which break essential functionality when blocked, we want to explicitly allow any domains on <a href="https://github.com/anudeepND/whitelist">Anudeep’s list of exceptions</a>.</p>
<p>To do this, we can use <a href="https://nlnetlabs.nl/documentation/unbound/unbound.conf/#local-zone">the <code class="language-plaintext highlighter-rouge">always_transparent</code> type of <code class="language-plaintext highlighter-rouge">local-zone</code></a>:</p>
<blockquote>
<p>Like transparent, but ignores local data and resolves normally.</p>
</blockquote>
<p>So, if we wanted to allow traffic to <code class="language-plaintext highlighter-rouge">some.bad.domain.here</code> even though it is already in our configuration, we need to add the following to our Unbound configuration before our blocklist:</p>
<div class="language-config highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">local</span>-<span class="n">zone</span>: <span class="s2">"some.bad.domain.here."</span> <span class="n">always_transparent</span>
</code></pre></div> </div>
<p>This will ensure we ignore our <code class="language-plaintext highlighter-rouge">redirect</code> to <code class="language-plaintext highlighter-rouge">local-data</code> and look the domain up normally.</p>
<p>Again, we can do <a href="https://gist.github.com/mudge/eaff0a816e8ee49046e263b0a6e99fe7#file-safelist-sh">some basic scripting to convert the list into configuration for us</a>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sSf</span> <span class="s2">"https://raw.githubusercontent.com/anudeepND/whitelist/master/domains/whitelist.txt"</span> |
<span class="nb">sort</span> <span class="nt">-u</span> | <span class="c"># Remove any duplicates</span>
<span class="nb">awk</span> <span class="s1">'{print "local-zone: \""$1".\" always_transparent"}'</span> <span class="c"># Convert to Unbound configuration</span>
</code></pre></div> </div>
<p>If we save those two lists into separate configuration files called <code class="language-plaintext highlighter-rouge">01-safelist.conf</code> and <code class="language-plaintext highlighter-rouge">02-blocklist.conf</code> under <code class="language-plaintext highlighter-rouge">/etc/unbound/lists.d</code>, we can <code class="language-plaintext highlighter-rouge">include</code> them in our Unbound <code class="language-plaintext highlighter-rouge">server</code> definition (e.g. under <code class="language-plaintext highlighter-rouge">/etc/unbound/unbound.conf.d</code>):</p>
<div class="language-config highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">server</span>:
<span class="c"># ...other configuration here
</span>
<span class="c"># Include blocklist and safelist
</span> <span class="n">include</span>: /<span class="n">etc</span>/<span class="n">unbound</span>/<span class="n">lists</span>.<span class="n">d</span>/*.<span class="n">conf</span>
</code></pre></div> </div>
<p>We can use <a href="https://nlnetlabs.nl/documentation/unbound/unbound-checkconf/"><code class="language-plaintext highlighter-rouge">unbound-checkconf</code></a> to ensure we don’t have any syntax errors and <code class="language-plaintext highlighter-rouge">service unbound reload</code> to load our new configuration if you’re using <a href="https://nlnetlabs.nl/documentation/unbound/unbound.conf/#control-enable"><code class="language-plaintext highlighter-rouge">control-enable</code></a>, otherwise <code class="language-plaintext highlighter-rouge">service unbound restart</code>.</p>
<p>We can test this by doing a lookup of a blocked domain with <a href="https://en.wikipedia.org/wiki/Dig_(command)">dig</a>:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">~></span><span class="w"> </span>dig @127.0.0.1 doubleclick.net +short
<span class="go">0.0.0.0
</span><span class="gp">~></span><span class="w"> </span>dig @127.0.0.1 doubleclick.net AAAA +short
<span class="go">::
</span></code></pre></div> </div>
<p>And check that any exceptions still return results:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">~></span><span class="w"> </span>dig @127.0.0.1 cdn.optimizely.com +short
<span class="go">cdn.o6.edgekey.net.
e5048.dsca.akamaiedge.net.
23.59.68.248
</span></code></pre></div> </div>
<p>And with that, I could run <a href="https://docs.pi-hole.net/core/pihole-command/#uninstall"><code class="language-plaintext highlighter-rouge">pihole uninstall</code></a>.</p>
</li>
<li>
<p>I spent a good part of the week working with <a href="https://www.scalefactory.com">The Scale Factory</a> to migrate a project to <a href="https://aws.amazon.com/fargate/">AWS Fargate</a> using the <a href="https://circleci.com/orbs/registry/orb/circleci/aws-ecr">CircleCI Amazon Elastic Container Registry</a> and <a href="https://circleci.com/orbs/registry/orb/circleci/aws-ecs">Elastic Container Service</a> <a href="https://circleci.com/docs/2.0/orb-intro/">Orbs</a>.</p>
<p>It all felt rather familiar given how similar Fargate is to <a href="https://www.hashicorp.com/products/nomad/">Hashicorp Nomad</a> and my experience <a href="https://technicallyshane.com/2020/01/14/week2.html">kicking off a migration to Nomad at Altmetric</a>.</p>
<p><a href="https://twitter.com/matt_macleod">Matt</a> wrote <a href="https://github.com/PowerRhino/rocksteady">Rocksteady</a> to handle orchestrating <a href="https://www.nomadproject.io/docs/job-specification/">jobspec</a> updates when deploying but thankfully, this is provided out-of-the-box by the <a href="https://circleci.com/orbs/registry/orb/circleci/aws-ecs#jobs-deploy-service-update">AWS ECS Orb’s <code class="language-plaintext highlighter-rouge">deploy-service-update</code> job</a>.</p>
<p>After all these changes, I’m hoping I will have cut down the total time to deploy (meaning from the start of our CI build to running our test suite, deploying to staging and then deploying to production) from over 34 minutes to 13 minutes.</p>
</li>
<li>
<p><a href="https://turnipprophet.io?prices=106.90.85.82.78.73.70.66.63....&pattern=3">After a bad week on the Stalk Market</a>, I have now spent my entire life savings in bells on turnips.</p>
</li>
</ul>
Weeknotes 31https://mudge.name/2020/05/31/weeknotes-31/2020-05-31T10:12:00+00:002020-05-31T10:12:00+00:00Unexpected socialising; your mission, should you choose to accept it; computer means and a lifelong commitment.<ul>
<li>
<p><a href="https://www.polygon.com/2020/5/26/21270653/animal-crossing-new-horizons-turnips-daisy-mae-acnh-nintendo-switch-stress-prices">The stress of buying turnips</a> may have impacted the quality of <a href="/2020/05/24/weeknotes-30/">last week’s notes</a>.</p>
</li>
<li>
<p>After putting C to bed, E said “I wish we could just sit outside and have a glass of wine.” Thanks to the range of <a href="/2020/03/08/weeknotes-19/">our baby monitor</a>, we did exactly that.</p>
<p>While setting up, our upstairs neighbour walked by and invited us to join them further up our driveway. We ended up chatting the evening away from socially distanced camping chairs, contactlessly sharing our last four After Eight mints and giving directions to lost Deliveroo riders.</p>
<p>I didn’t realise how much I miss socialising in person with people outside our household.</p>
</li>
<li>
<p>I don’t know why but E and I have been making our way through the “Mission: Impossible” films.</p>
<p>So far, we’ve watched <a href="https://www.imdb.com/title/tt0117060/">Brian De Palma’s “Mission: Impossible”</a>, <a href="https://www.imdb.com/title/tt0120755/">John Woo’s “Mission: Impossible II”</a>, <a href="https://www.imdb.com/title/tt0317919/">J.J. Abrams’ “Mission: Impossible III”</a> and <a href="https://www.imdb.com/title/tt1229238/">Brad Bird’s “Mission: Impossible - Ghost Protocol”</a>.</p>
<p>The second one has not aged well.</p>
</li>
<li>
<p>I made bread this weekend after a short break from baking.</p>
<p class="center"><img src="/i/bread.jpg" width="375" height="375" alt="" /></p>
<p>While our <a href="/2020/03/15/weeknotes-20/">large quantity of flour</a> means I can’t deviate from <a href="https://tartinebakery.com/stories/country-bread">my usual recipe</a>, <a href="https://twitter.com/cassarani">Leo</a> sharing a <a href="https://www.reddit.com/r/Breadit/comments/glzuiz/heres_another_video_of_me_shaping_sourdough_i/?utm_source=share&utm_medium=web2x">video of a baker shaping sourdough</a> has inspired me to try different shaping techniques.</p>
</li>
<li>
<p>I have a file called <code class="language-plaintext highlighter-rouge">budget.rb</code> that contains all our household expenses and calculates how much money we spend every month. I often tinker with it and decided to switch to using the <a href="https://en.wikipedia.org/wiki/Geometric_mean">geometric mean</a> instead of the typical <a href="https://en.wikipedia.org/wiki/Arithmetic_mean">arithmetic mean</a> for some averages. (Despite E dismissing anything other than the arithmetic mean, <a href="https://en.wikipedia.org/wiki/Median">median</a> and <a href="https://en.wikipedia.org/wiki/Mode_(statistics)">mode</a> as a “computer mean”.)</p>
<p>This led me to write <a href="https://gist.github.com/mudge/a653a1fb837e108a5501937c456f4412">a Ruby refinement to add all three Pythagorean means to <code class="language-plaintext highlighter-rouge">Enumerable</code> objects</a>, e.g.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">using</span> <span class="no">PythagoreanMeans</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">].</span><span class="nf">arithmetic_mean</span>
<span class="c1"># => 3</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">].</span><span class="nf">geometric_mean</span>
<span class="c1"># => ~2.61</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">].</span><span class="nf">harmonic_mean</span>
<span class="c1"># => ~2.18</span>
</code></pre></div> </div>
<p>I’ve never written a <a href="https://docs.ruby-lang.org/en/2.7.0/syntax/refinements_rdoc.html">refinement</a> before and immediately encountered <a href="https://bugs.ruby-lang.org/issues/16852">a bug in Ruby 2.7 meaning you can’t refine the <code class="language-plaintext highlighter-rouge">Enumerable</code> module</a>. Despite being four years old, <a href="https://interblah.net/why-is-nobody-using-refinements">James Adam’s “Why is nobody using Refinements?”</a> is still as relevant as ever.</p>
</li>
<li>
<p>C got his first teeth.</p>
<p>Despite months of asking ourselves “could he be teething?” whenever he was irrationally grumpy, it was still a surprise to see them. This means we have to brush his teeth twice daily. So begins a lifelong <a href="https://natbuckley.co.uk/2019/06/02/weeknotes-20-the-wretched-task/">wretched task</a>.</p>
</li>
</ul>
Weeknotes 30https://mudge.name/2020/05/24/weeknotes-30/2020-05-24T14:06:00+00:002020-05-24T14:06:00+00:00A birthday, the island of Custard, more public keys and a new earworm.<ul>
<li>
<p>I successfully completed another full orbit around the sun. To celebrate, E, C and I went for our first “<a href="https://www.gov.uk/government/publications/coronavirus-outbreak-faqs-what-you-can-and-cant-do/coronavirus-outbreak-faqs-what-you-can-and-cant-do">stay alert</a>” picnic in the park. While it was surprisingly busy for a weekday, it was great to be outside for the longest time since lockdown began.</p>
</li>
<li>
<p>As an antidote to <a href="/2020/05/03/weeknotes-27/">the stress of Doom Eternal</a>, <a href="http://willhigo.com">Will</a> and <a href="http://andrewlitt.co.uk">Andrew</a> gave me the gift of “<a href="https://www.animal-crossing.com/new-horizons/">Animal Crossing: New Horizons</a>”. I don’t think I’ve played a game that requires waiting real days for progression before but the enforced slow pace is a welcome change. My island is called Custard and I now understand more of <a href="https://twitter.com/tomstuart/status/1261942415553728512">Tom’s tweets</a>.</p>
</li>
<li>
<p>As a birthday treat, E made <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2017/mar/30/how-to-cook-the-perfect-miso-ramen">Felicity Cloake’s perfect miso ramen</a> and it was fantastic.</p>
</li>
<li>
<p>Following <a href="/2020/05/17/weeknotes-29/">last week’s adventures with GPG</a>, <a href="https://www.instagram.com/_weszlem/">Maciej</a> recommended <a href="https://github.com/drduh/YubiKey-Guide/blob/master/README.md#creating-keys">drduh’s guide to using YubiKey for GPG and SSH</a>. Even if you don’t have a <a href="https://www.yubico.com/products/yubikey-hardware/">YubiKey</a>, it is a thorough guide to setting up <a href="https://github.com/drduh/YubiKey-Guide#master-key">a single GPG master key</a> and single-purpose <a href="https://github.com/drduh/YubiKey-Guide#sub-keys">sub-keys</a>.</p>
</li>
<li>
<p>I went on a related adventure to <a href="https://blog.g3rt.nl/upgrade-your-ssh-keys.html">upgrade my SSH keys from a sole 2048-bit RSA key to a 4096-bit RSA key and a Ed25519 key</a>.</p>
<p><a href="https://twitter.com/cassarani">Leo</a> pointed out you can get the public SSH keys for any GitHub user by appending <code class="language-plaintext highlighter-rouge">.keys</code> to their profile URL, e.g. <a href="https://github.com/mudge.keys">https://github.com/mudge.keys</a>. You can also use <code class="language-plaintext highlighter-rouge">.gpg</code> to get their public GPG keys, e.g. <a href="https://github.com/mudge.gpg">https://github.com/mudge.gpg</a>.</p>
</li>
<li>
<p><a href="https://technicallyshane.com">Shane</a> <a href="https://twitter.com/shamess/status/1261981790794850305">told me off</a> for not simply tilting my head in order to better locate <a href="/2020/05/17/weeknotes-29/">last week’s infernal whining noise</a>.</p>
</li>
<li>
<p>We watched (or is that played?) Netflix’s interactive “<a href="https://www.netflix.com/title/81131714">Unbreakable Kimmy Schmidt: Kimmy vs. the Reverend</a>” and enjoyed it more than I expected. I’m not sure we’ll go back to watch all the alternate paths we could have taken though.</p>
</li>
<li>
<p>Despite being indifferent about the show, E will not stop singing <a href="https://open.spotify.com/track/6tJFtthY0rI1x06qb8NjK0?si=E3pDqHKiT2Cap3lrdmYxCw">The Mandalorian theme tune</a>. It may well unseat <a href="https://open.spotify.com/track/25lTenJPmSfwCRZi2hjCcB?si=DLXmTdOCSLuyQmf6Gq5zcQ">the Outer Wilds theme</a> and <a href="https://open.spotify.com/track/05pN1ltuUivUgSzRlTXhtr?si=E2IjricSTwC9-OrDJ96QyA">XCOM 2’s “Squad Loadout”</a> music as the most annoying earworm in this house.</p>
</li>
<li>
<p><a href="https://ftrain.substack.com/">Paul Ford has started writing a newsletter called “i absolutely am going to bail on this in a month”</a> (there’s an <a href="https://ftrain.substack.com/feed/">RSS feed</a> too). As a fan of Ford’s <a href="https://ftrain.com">Ftrain.com</a> back in the 2000s, this makes me happy.</p>
</li>
</ul>
Weeknotes 29https://mudge.name/2020/05/17/weeknotes-29/2020-05-17T09:59:00+00:002020-05-17T09:59:00+00:00An infernal whining, an update on EOL Ruby versions, the pit of success, a GPG rabbithole and teaching babies about quantum mechanics.<ul>
<li>
<p>After publishing <a href="/2020/05/10/weeknotes-28/">last week’s notes</a>, E and I noticed a high pitched whining noise coming from our kitchen.</p>
<p>Unable to locate the source of the noise, I switched off the power to our oven, fridge and freezer but still the whining continued. I resorted to holding the cardboard tube from a toilet roll to one ear while plugging my other ear with a headphone, crawling on all fours trying to pinpoint this infernal whine.</p>
<p>With a growing headache, I climbed onto the kitchen counter and checked our fire alarm when suddenly I realised: the noise was coming from <a href="https://uk.joiebaby.com/product/dreamer/">C’s baby bouncer</a>. He’d accidentally turned on one of its soothing sounds and the manufacturers had seemingly chosen the sound of a single, agitated mosquito to represent the rainforest.</p>
</li>
<li>
<p>In my search, I did discover our fire alarm had a sticker on its underside that read “Replace by 2014”.</p>
</li>
<li>
<p><a href="https://github.com/rubytogether/ecosystem/pull/284">My changes to RubyGems.org stats were deployed</a> so I can tell you on Friday, 15th May 2020, out of 54,933,639 <code class="language-plaintext highlighter-rouge">gem install</code> and <code class="language-plaintext highlighter-rouge">bundle install</code>s:</p>
<ul>
<li>19,634,055 (35.7%) were from Ruby 2.3 (<a href="https://www.ruby-lang.org/en/downloads/branches/">end-of-life since April 2019</a>)</li>
<li>13,631,483 (24.8%) were from Ruby 2.5</li>
<li>8,370,929 (15.2%) were from Ruby 2.4 (<a href="https://www.ruby-lang.org/en/downloads/branches/">end-of-life since April 2020</a>)</li>
<li>5,952,688 (10.8%) were from Ruby 2.6</li>
<li>3,882,306 (7.1%) were from Ruby 2.1 (<a href="https://www.ruby-lang.org/en/downloads/branches/">end-of-life since April 2017</a>)</li>
<li>1,090,639 (2.0%) were from Ruby 1.9 (<a href="https://www.ruby-lang.org/en/downloads/branches/">end-of-life since February 2015</a>)</li>
<li>782,778 (1.4%) were from Ruby 2.0 (<a href="https://www.ruby-lang.org/en/downloads/branches/">end-of-life since February 2016</a>)</li>
<li>726,975 (1.3%) were from Ruby 2.2 (<a href="https://www.ruby-lang.org/en/downloads/branches/">end-of-life since April 2018</a>)</li>
<li>720,261 (1.3%) were from Ruby 2.7</li>
<li>122,573 (0.2%) were from Ruby 1.8 (<a href="https://www.ruby-lang.org/en/news/2014/07/01/eol-for-1-8-7-and-1-9-2/">end-of-life since August 2014</a>)</li>
</ul>
<p>This means that 34,610,255 (63%) of all requests were from versions of Ruby that are technically “end-of-life”.</p>
</li>
<li>
<p>Following <a href="https://engineering.fb.com/web/facebook-redesign/">Facebook’s description of their newly rebuilt tech stack for Facebook.com</a>, I enjoyed reading <a href="https://macwright.org/2020/05/10/spa-fatigue.html">Tom MacWright’s “Second-guessing the modern web”</a>.</p>
<p>I was particularly drawn to the idea of the <a href="https://blog.codinghorror.com/falling-into-the-pit-of-success/">pit of success</a>:</p>
<blockquote>
<p>And it should be easy to do a good job.</p>
<p>Frameworks should lure people into the pit of success, where following the normal rules and using normal techniques is the winning approach.</p>
</blockquote>
<p>I’ve <a href="/2019/12/08/weeknotes-6/">mentioned my interest in the design concept of “affordance” before</a> and my main consideration when designing software is what kind of behaviours does a decision encourage and discourage. “Pit of success” is a pithy way to communicate this idea: how can we construct a system such that the <em>easiest</em> possible thing is also the <em>right</em> thing to do?</p>
</li>
<li>
<p>In one of our recent trips to <a href="https://www.bbc.co.uk/cbeebies/shows/the-baby-club">The Baby Club</a>, the answer to the regular question of “what’s in the bag?” was “a bag”. The recursive nature of this answer reminded me of <a href="https://www.foodandwine.com/news/solving-mystery-kit-kat-filling">the filling inside KitKats</a> and made me secretly wish the show would take a more surreal turn. I suppose I’ll just have to rewatch “<a href="https://www.youtube.com/channel/UCZOnoLKzoBItcEk5OsES2TA">Don’t Hug Me I’m Scared</a>” instead.</p>
</li>
<li>
<p>This week at work, after successfully launching a major new feature, we took a few days to clean up some <a href="https://martinfowler.com/bliki/TechnicalDebt.html">technical debt</a> we’d accrued to meet our deadline. I took to calling it a “firebreak” week, having <a href="https://insidegovuk.blog.gov.uk/2015/02/06/gov-uks-firebreak-why-and-how-we-spent-a-month-working-differently/">stolen the term from GOV.UK</a>.</p>
<p>Having overheard this, E wished me well “turning off the fire engine”.</p>
</li>
<li>
<p>After seeing <a href="https://twitter.com/siracusa/status/1258113504499904512">John Siracusa recommend using <code class="language-plaintext highlighter-rouge">pmset</code> to diagnose problems with a Mac that refuses to sleep</a>, I used it myself when E’s laptop kept draining its battery with the lid closed.</p>
<p><code class="language-plaintext highlighter-rouge">pmset -g</code> and <code class="language-plaintext highlighter-rouge">pmset -g assertions</code> revealed that <code class="language-plaintext highlighter-rouge">sharingd</code> was preventing <code class="language-plaintext highlighter-rouge">UserIdleSystemSleep</code> and, sure enough, toggling Screen Sharing (and a reboot) fixed the problem.</p>
</li>
<li>
<p>I went down a bit of a <a href="https://www.gnupg.org">GPG</a> rabbithole after discovering that my 12 year old <a href="https://infra.apache.org/openpgp.html#generate-key">1024 bit DSA key is threatened by weaknesses found in SHA-1</a>. Thankfully, <a href="https://infra.apache.org/key-transition.html">The Apache Software Foundation’s “How to transition to a new PGP key”</a> was an excellent guide to <a href="https://infra.apache.org/openpgp.html#generate-key">generating a new 4096 bit RSA key</a> and <a href="https://infra.apache.org/key-transition.html#single-keyring">transitioning to using it</a>.</p>
</li>
<li>
<p>After reading many recommendations, we started watching “<a href="https://disneyplusoriginals.disney.com/show/the-mandalorian">The Mandalorian</a>”. It’s fine.</p>
</li>
<li>
<p>A recurring section of C’s <a href="https://www.singandsign.co.uk">Sing and Sign</a> lessons is a cat named Jessie hidden in a box. I assume this is to teach children <a href="https://en.wikipedia.org/wiki/Schrödinger%27s_cat">the problem of the Copenhagen interpretation of quantum mechanics</a>.</p>
</li>
</ul>
Weeknotes 28https://mudge.name/2020/05/10/weeknotes-28/2020-05-10T10:16:00+00:002020-05-10T10:16:00+00:00A new thought technology, supporting old Ruby versions, making the most of the long weekend and filling the hole left in my life by Keith Brymer Jones.<ul>
<li>
<p>Following <a href="https://www.radicalcandor.com">radical candor</a> and <a href="https://www.newyorker.com/sports/sporting-scene/world-cup-2018-radical-sensibleness-england-team-manager-gareth-southgate">radical sensibleness</a>, I propose a new <a href="https://twitter.com/hotdogsladies/status/656676217408614400">thought technology</a>: radical competency.</p>
<p>It can be summarised in three steps:</p>
<ol>
<li>Think about a thing.</li>
<li>Say you’ll do something related to step 1.</li>
<li>Do the thing you said you’d do in step 2.</li>
</ol>
<p>This is inspired by recent experience and by <a href="https://www.instagram.com/_weszlem/">Maciej</a> sharing <a href="https://chelseatroy.com/2019/12/18/reviewing-pull-requests/">Chelsea Troy’s “Reviewing Pull Requests”</a>. While I’m not sure I agree about pushing commits to someone else’s branch, I agree with the goal of saving time and frustration by placing more burden on the reviewer.</p>
<p>In any exchange, you should be trying to <em>reduce</em> effort for the other party.</p>
</li>
<li>
<p>E and I watched “<a href="https://www.imdb.com/title/tt2527338/">Star Wars: Episode IX - The Rise of Skywalker</a>”. It was not good.</p>
</li>
<li>
<p>Following <a href="/2020/05/03/weeknotes-27/">last week’s debugging adventures</a>, I switched <a href="https://github.com/mudge/re2">re2</a> from using <a href="https://travis-ci.org">Travis CI</a> to <a href="https://github.com/features/actions">GitHub Actions</a>.</p>
<p>Most of my effort went into writing a <a href="https://github.com/mudge/re2-test-action">new, custom GitHub Action called “re2 Test Action”</a> which creates a consistent environment using <a href="https://www.docker.com">Docker</a> for running the <a href="https://github.com/mudge/re2/tree/master/spec">re2 test suite</a> against <a href="https://github.com/mudge/re2/blob/master/.github/workflows/tests.yml#L13-L22">different versions</a> of Ruby and the <a href="https://github.com/google/re2">underlying re2 library</a>.</p>
<p>It was a little tricky because the <a href="https://github.com/actions/setup-ruby">recommended way to setup Ruby versions with GitHub Actions</a> only supports Ruby 2.4, 2.5, 2.6 and 2.7. However, the recently released <a href="http://stats.rubygems.org">RubyGems.org stats</a> shows that the most popular Ruby version is Ruby 2.3 (and over 7% of people still use Ruby 2.1).</p>
<p>I’m <a href="https://twitter.com/mudge/status/1259036292752576513">attempting to get</a> a <a href="https://github.com/rubytogether/ecosystem/pull/284">more detailed breakdown of older Ruby versions</a> as I’d love to know how many people are still using long end-of-lifed versions.</p>
<p>People have often been surprised, confused and occasionally frustrated that I support Ruby 1.8 in my libraries such as re2, <a href="https://github.com/fieldhand/fieldhand">Fieldhand</a> and <a href="https://github.com/altmetric/embiggen">Embiggen</a>. Having wasted so much time in my career struggling to upgrade dependencies that have, in turn, caused other breakages, I strongly believe libraries should go out of their way to be as compatible and frictionless as possible for the user to use. I never want one of my libraries to cause you incidental issues because I made a change internally.</p>
<p>While <a href="https://www.mikeperham.com/2016/02/09/kill-your-dependencies/">Mike Perham called for us to “Kill Your Dependencies”</a>, I agree more with <a href="http://jcoglan.com">James Coglan</a> who talked about this in <a href="https://whyarecomputers.com/1">Why Are Computers’ “Episode 1: A Fairly Deep Yak Shave”</a>:</p>
<blockquote>
<p>The thing that most concerns me is stuff that gets changed because
someone thought that the new way is how it should’ve been done in the
first place and it’s obviously better, but it doesn’t give any real new
capabilities or power and doesn’t really fix any mistakes, and it breaks
their existing software.</p>
<p>To me the canonical example of that is the Ruby 1.9 hash syntax, which a
lot of people are like “oh, it’s obviously better”, but it doesn’t let
you write programs that you couldn’t write before, it doesn’t fix any
mistakes that anyone was making before, and it means that if someone uses
that syntax, that program now won’t run on an older thing. It’s purely an
aesthetic change. The aesthetics of code are important, and it’s
important to have stuff that’s readable, but if you have a thing that’s
already been shipped, making those tiny little fussy aesthetic changes to
it, to me, never seems really worth it.</p>
</blockquote>
</li>
<li>
<p>Never one to let a <a href="https://www.gov.uk/bank-holidays">bank holiday weekend</a> go to waste, I spent a large portion of yesterday on administrivia mostly informed by <a href="https://www.moneysavingexpert.com">Martin Lewis’ “Money Saving Expert”</a>.</p>
</li>
<li>
<p>On a bittersweet note, we <a href="/2020/05/03/weeknotes-27/">finished “The Great Pottery Throw Down”</a>. I am attempting to fill the hole it has left in my life by encouraging C to watch “<a href="https://www.bbc.co.uk/cbeebies/shows/hey-duggee">Hey Duggee</a>” instead.</p>
</li>
</ul>
Weeknotes 27https://mudge.name/2020/05/03/weeknotes-27/2020-05-03T09:40:00+00:002020-05-03T09:40:00+00:00Getting emotional about cups and saucers, a home haircut, the only two good types of advert, new kitchen scales and a debugging adventure.<ul>
<li>
<p>E and I started watching series 3 of “<a href="https://www.channel4.com/programmes/the-great-pottery-throw-down">The Great Pottery Throw Down</a>”. I was drawn in by the sight of expert judge <a href="https://en.wikipedia.org/wiki/Keith_Brymer_Jones">Keith Brymer Jones</a> in dungarees throwing an amphora before asking contestants to do the same. We were hooked when we saw him moved to tears while judging a breakfast set:</p>
<blockquote>
<p>That’s fantastic, mate. Fantastic!</p>
</blockquote>
<p>After writing about my disappointment with the Great British Bake Off in <a href="/2019/11/16/weeknotes-3/">my early weeknotes</a>, this is the feel-good show I’ve been looking for.</p>
</li>
<li>
<p>In <a href="https://www.gov.uk/coronavirus">Coronavirus lockdown</a> news: E cut my hair with a beard trimmer. After watching a few YouTube videos and <a href="https://travel.stackexchange.com/questions/14915/how-to-know-your-number-for-a-machine-haircut-in-a-foreign-country">trying to convert UK haircut “grades” into millimetres</a>, we went for 6 mm on the side, 12 mm on top and 9 mm in between.</p>
</li>
<li>
<p>After <a href="/2020/04/19/weeknotes-25/">my adoption of Shortcuts a few weeks ago</a>, <a href="https://tomstu.art">Tom</a> asked why I didn’t use “<a href="https://support.apple.com/en-gb/HT204389#ios">Hey Siri</a>” to activate my shortcuts instead of fumbling with my phone’s screen in the dark. I had tried but was frustrated pausing between saying “Hey Siri” and waiting for the telltale “hmm?”.</p>
<p>Tom told me you don’t need to pause at all: Siri will buffer everything you say after being activated. You will now find me occasionally saying “Hey Siri Bounce”, “Hey Siri Fold” and “Hey Siri <a href="https://www.bakerybits.co.uk/resources/autolyse-what-why-how/">Autolyse</a>”.</p>
</li>
<li>
<p>I haven’t been enjoying the increased number of emotionally manipulative Coronavirus-related advertisements from commercial companies and seeing <a href="https://youtu.be/_e8XLnMiCOE">Uber’s “Thank You For Not Riding”</a> made me particularly cynical. As <a href="https://youtu.be/vM3J9jDoaTA">Sean Haney demonstrated, “Every Covid-19 Commercial is Exactly the Same”</a>.</p>
</li>
<li>
<p>There should only be two types of advert: the straightforward <a href="https://youtu.be/lrMD_z_FnNk">Cillit Bang</a> approach or the utterly sublime <a href="https://youtu.be/j4IFNKYmLa8">Halo Top</a>.</p>
</li>
<li>
<p>Someone in this household accidentally cooked our kitchen scales, reducing them to molten plastic so we had to invest in a replacement. As <a href="https://mrgan.tumblr.com/post/615569556634763264/nevens-pizza-dough/amp">Neven Mrgan mentions in his pizza dough recipe</a>, having gram-precision scales that have an extra long timeout is ideal for baking:</p>
<blockquote>
<p>My favorite feature: extra long timeout (before it turns off) so I can forget to get the flour and run downstairs and hunt for it and when I come back, my measurement is still up on the screen.</p>
</blockquote>
<p>We got ourselves some <a href="http://www.salterhousewares.co.uk/salter-pro-stainless-steel-digital-kitchen-scales.html">Salter Pro Stainless Steel Digital Kitchen Scales</a> as a replacement and, while it isn’t listed anywhere, they have a very generous timeout.</p>
</li>
<li>
<p><a href="https://siguza.github.io/psychicpaper/">Siguza’s write-up of their “Psychic Paper” security exploit for iOS</a> is a great and extremely accessible read on how the incidental complexity of XML parsing resulted in a very serious bug.</p>
</li>
<li>
<p>We finished <a href="https://www.bbc.co.uk/iplayer/episode/p087gkct/devs-series-1-episode-1">DEVS</a> and, <a href="https://tomstu.art/weeknotes-15-what-a-ride">like Tom</a>, I think it is well worth watching even if it wasn’t perfect.</p>
<p>It made me want to re-read <a href="https://www.penguinrandomhouse.com/books/538034/exhalation-by-ted-chiang/">Ted Chiang’s “Exhalation”</a>, <a href="https://www.penguinrandomhouse.com/books/538163/arrival-stories-of-your-life-mti-by-ted-chiang/">“Arrival” (formerly published as “Stories of Your Life and Others”)</a> and <a href="https://www.gregegan.net/PERMUTATION/Permutation.html">Greg Egan’s “Permutation City”</a>.</p>
</li>
<li>
<p>I investigated and fixed <a href="https://github.com/mudge/re2/issues/43">an issue related to re2</a>, my most popular open source project, which led me on a journey to better understand <a href="http://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html">shared library names and how they communicate <abbr title="Application binary interface">ABI</abbr> compatibility</a>. This then led me to discover and <a href="https://github.com/Homebrew/homebrew-core/pull/54136">fix a bug in a Homebrew formula</a> to resolve the original issue.</p>
<p>I do love the twists and turns of debugging.</p>
</li>
<li>
<p><a href="/2020/04/26/weeknotes-26/">The jury is in</a>: Doom Eternal is far too stressful.</p>
</li>
</ul>
Weeknotes 26https://mudge.name/2020/04/26/weeknotes-26/2020-04-26T11:00:00+00:002020-04-26T11:00:00+00:00The importance of editing, unminifying JavaScript, inappropriate Zoom virtual backgrounds and a thirst for life.<ul>
<li>
<p>I waffle.</p>
<p>My first draft of that sentence was:</p>
<blockquote>
<p>When writing, I have a tendency to waffle.</p>
</blockquote>
<p>Last week’s notes were a perfect example of this. When I first published them, E proofread and found so much to cut and simplify. So <a href="https://github.com/mudge/mudge.github.com/commit/ccc989bfa7780bb1f5e684d799065cb2c82d92b4">I did</a>, <a href="https://github.com/mudge/mudge.github.com/commit/95daf898f4063c6b918d2f6702da2c9f1a454971">over</a> and <a href="https://github.com/mudge/mudge.github.com/commit/1c7eae2c11e473bdc13aad21f1f940bd3075f0e0">over</a> and <a href="https://github.com/mudge/mudge.github.com/commit/bb91896504140b5b605030f276b8279379e540bd">over</a> and <a href="https://github.com/mudge/mudge.github.com/commit/d05f3b4932e060b1794ef972d76065b684de6e88">over again</a>.</p>
<p>In the words of <a href="https://quoteinvestigator.com/2012/04/28/shorter-letter/">Blaise Pascal (not Mark Twain)</a>:</p>
<blockquote>
<p>I have made this longer than usual because I have not had time to make it shorter.</p>
</blockquote>
</li>
<li>
<p>I’m experimenting with writing these notes on my phone during the week. The main issue is how inept I am at editing text on iOS, especially when trying to place the insertion pointer to paste text. It turns out there are <a href="https://support.apple.com/en-gb/guide/iphone/iph3c50f96e/13.0/ios/13.0">three finger gestures for cut, copy and paste</a> which make things much easier. I doubt I would have ever discovered them on my own.</p>
</li>
<li>
<p><a href="https://twitter.com/chastell/status/1252337694320910336">Piotr flagged that my unminified JavaScript from last week fits into a single TCP packet when gzipped</a>. As <a href="https://caniuse.com/#search=gzip">gzip compression has been supported in browsers for the past 20 years</a> and I’m using <a href="https://support.cloudflare.com/hc/en-us/articles/200168396-What-will-Cloudflare-compress-">Cloudflare to compress my JavaScript with Brotli</a>, there really isn’t any need to minify it.</p>
<p>Still, I couldn’t resist playing <a href="https://github.com/mudge/mudge.github.com/commit/dd318af9d8182f80538536d0077947e043275a97">a little code golf</a>.</p>
</li>
<li>
<p>I <a href="https://github.com/mudge/mudge.github.com/blob/51d0fd06ccf168428e1b51ad878db7031c50fb27/_includes/dotiw.test.js">ported the original <code class="language-plaintext highlighter-rouge">distance_of_time_in_words</code> test suite from Rails to JavaScript</a> and used it as an excuse to <a href="https://github.com/mudge/mudge.github.com/actions/runs/87789354">set up my first workflow with GitHub Actions</a>.</p>
<p>Running the test suite revealed <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getYear">JavaScript’s <code class="language-plaintext highlighter-rouge">Date.prototype.getYear()</code> doesn’t return what I expected</a>:</p>
<blockquote>
<p>A number representing the year of the given date, according to local time, minus 1900.</p>
</blockquote>
<p>Sure enough:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="p">(</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="dl">"</span><span class="s2">2020-04-26</span><span class="dl">"</span><span class="p">)).</span><span class="nx">getYear</span><span class="p">()</span>
<span class="mi">120</span>
</code></pre></div> </div>
<p>Thankfully, you can use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getFullYear"><code class="language-plaintext highlighter-rouge">Date.prototype.getFullYear()</code></a> instead:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="p">(</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="dl">"</span><span class="s2">2020-04-26</span><span class="dl">"</span><span class="p">)).</span><span class="nx">getFullYear</span><span class="p">()</span>
<span class="mi">2020</span>
</code></pre></div> </div>
</li>
<li>
<p>I disappeared down a bit of a rabbit hole trying to add a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">Content Security Policy</a> to this site in pursuit of a perfect score from <a href="https://observatory.mozilla.org/">Mozilla Observatory</a>.</p>
</li>
<li>
<p>Following both <a href="https://natbuckley.co.uk/2020/04/19/weeknotes-66-the-novelty-has-worn-off/">Nat</a> and <a href="https://tomstu.art/weeknotes-15-what-a-ride">Tom</a> mentioning the <a href="https://youtu.be/Mh4f9AYRCZY">2017 BBC News interview with Professor Robert Kelly that was famously interrupted by his children</a>, <a href="https://twitter.com/cassarani">Leo</a> told me about a colleague using the footage as a <a href="https://support.zoom.us/hc/en-us/articles/210707503-Virtual-Background">Zoom virtual background</a>. This was too good an idea not to copy, so I downloaded <a href="https://youtu.be/62a-1ZYcsV0">a version of the video with Professor Kelly removed</a> and set it as my background. Sadly, I had to quickly turn it off when the meeting I was hoping to brighten with <a href="https://natbuckley.co.uk/images/girl.gif">that entrance dance</a> turned unexpectedly gloomy.</p>
</li>
<li>
<p>On an impulse, I bought <a href="https://bethesda.net/game/doom">Doom Eternal</a> from the <a href="https://www.playstation.com/en-gb/explore/playstation-network/">PlayStation Network</a>. The jury is still out on whether blasting fiends from hell at an <a href="https://youtu.be/I9ZsFT_eqXY">intentionally unrelenting pace</a> is an activity that can be considered “relaxing”.</p>
</li>
<li>
<p>C is fascinated by anyone drinking. When he gets chance to drink water himself, he goes into a kind of joyful frenzy, finding every aspect of the experience wonderful. His enthusiasm for mundane things is an inspiration.</p>
</li>
</ul>
Weeknotes 25https://mudge.name/2020/04/19/weeknotes-25/2020-04-19T13:28:00+00:002020-04-19T13:28:00+00:00Bank holiday noodling, coffee paraphernalia, a use for Shortcuts and life advice from a Learn with Me Zebra walker.<ul>
<li>
<p>I spent part of the <a href="https://www.gov.uk/bank-holidays">bank holiday weekend</a> adding a new section to every post’s byline showing how long it has been since it was published, e.g.</p>
<blockquote>
<p>By Paul Mucur, <strong>7 days ago</strong> on Sunday 12th April 2020</p>
</blockquote>
<p>As every post is a static page generated by <a href="https://jekyllrb.com">Jekyll</a>, these new relative dates are calculated entirely in your browser so they are always up-to-date, regardless of when I last generated the site. As a bonus, they update every minute so it should still be accurate even if you leave a post open for a while.</p>
<p>I was too lazy to come up with my own algorithm for turning lengths of time into words so I decided to port <a href="https://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-time_ago_in_words">Rails’ <code class="language-plaintext highlighter-rouge">time_ago_in_words</code></a> to JavaScript.</p>
<p>You can <a href="https://github.com/mudge/mudge.github.com/blob/47092082ed7ef6c133f5c35070e4f96c3f523b74/j/1.js">view the full, unminified source code on GitHub</a> (or <a href="https://github.com/mudge/mudge.github.com/blob/47092082ed7ef6c133f5c35070e4f96c3f523b74/j/1.m.js">enjoy the minified version</a>).</p>
</li>
<li>
<p>Since <a href="/2020/03/22/weeknotes-21/">clearing out our planter a few weeks ago</a>, the sight of a rectangle of barren soil with a single, surviving <a href="https://en.wikipedia.org/wiki/Chard">chard</a> plant has not been a cheery one. To improve matters and following <a href="https://twitter.com/matt_macleod">Matt</a>’s lead, I planted a clove of garlic in the hope it will grow.</p>
</li>
<li>
<p>E and I watched <a href="https://www.imdb.com/title/tt7014006/">Bo Burnham’s “Eighth Grade”</a> and loved it.</p>
<p>The movie doesn’t seem to be that well known despite receiving <a href="https://en.wikipedia.org/wiki/Eighth_Grade_(film)#Critical_response">a lot of critical praise</a>.</p>
<p>As <a href="https://twitter.com/MollyRingwald/status/1022250943298121728">Molly Ringwald put it</a>:</p>
<blockquote>
<p>I just saw [Eighth Grade] and thought it was the best film about adolescence I’ve seen in a long time. Maybe ever.</p>
</blockquote>
</li>
<li>
<p>After watching <a href="https://youtu.be/yssNu8Eynb8">the chefs of Bon Appétit make their favourite coffee</a>, E suggested we buy a coffee grinder.</p>
<p>Asking for recommendations led me to the <a href="https://www.wilfa.co.uk/product/kitchen/black-aroma/">Wilfa Svart Aroma Coffee Grinder</a>. Seeing <a href="http://www.timwendelboe.no">Tim Wendelboe</a>’s name <a href="http://nordiccoffeeculture.com/a-tim-wendelboe-review-of-the-wilfa-svart-presisjon/">in association with Wilfa</a> made the decision easy given that E and I had visited his Espresso Bar in Oslo back in September 2015.</p>
<p>During our visit, E asked whether there were any pastries but was told they served no food as it might detract from the coffee drinking experience. While we disagree about the importance of pastries, I trust his judgement on coffee-making equipment.</p>
</li>
<li>
<p>We ended up buying <a href="https://shop.squaremilecoffee.com/products/wilfa-grinder">the grinder from Square Mile Coffee Roasters</a> along with a bag of beans.</p>
<p>The next day, I received the following email:</p>
<blockquote>
<p>Dear Paul,</p>
<p>As you might know, we’ve recently launched our 25% GRINDATHOME discount code on all our grinders. We know, you just missed it, and to make up for it we would like to extend the discount code to your order by refunding the 25% on your recently purchased Wilfa grinder.</p>
</blockquote>
<p>In a week of extremely stressful emails, this was an unexpected and very welcome kindness.</p>
</li>
<li>
<p>To celebrate the arrival of the grinder, I followed <a href="https://bluebottlecoffee.com/preparation-guides/chemex">Blue Bottle Coffee’s Chemex Brewing Guide</a> and, reader, it was a glorious cup of coffee.</p>
</li>
<li>
<p>Despite hearing about it on podcasts for a while, I hadn’t found a use for <a href="https://support.apple.com/en-gb/HT208309">iOS Shortcuts</a>.</p>
<p>When C wakes up unexpectedly in the night, I get up to reassure him and set a timer for 10 minutes to see if he will put himself back to sleep. After struggling to do this in the dark for over a month, I realised this might be the perfect use case.</p>
<p>I created a new shortcut to set a 10 minute timer with a single button press but it is still awkward swiping over to the <a href="https://support.apple.com/en-gb/HT207122">widget in the Today View</a>.</p>
</li>
<li>
<p>E and I started watching <a href="https://www.bbc.co.uk/programmes/p087gj19">Alex Garland’s “DEVS” on BBC iPlayer</a>. I’ll join <a href="https://twitter.com/waxy/status/1250911854123200517">others</a> in highly recommending it.</p>
<p>While the title makes it sound like a comedy in the vein of “<a href="https://www.channel4.com/programmes/the-it-crowd">The IT Crowd</a>” or “<a href="https://www.hbo.com/silicon-valley">Silicon Valley</a>”, it is a drama more akin to Garland’s previous work: “<a href="https://www.imdb.com/title/tt0470752">Ex Machina</a>” and “<a href="https://www.imdb.com/title/tt2798920">Annihilation</a>”.</p>
</li>
<li>
<p>It has been a trying week but as we retired for bed one evening, I accidentally kicked <a href="https://www.fisher-price.com/en-gb/product/learn-with-me-zebra-walker-dlf00">C’s walker</a>. In its cheery voice, it said “keep going!”</p>
</li>
</ul>
Weeknotes 24https://mudge.name/2020/04/12/weeknotes-24/2020-04-12T15:24:00+00:002020-04-12T15:24:00+00:00Repairs, a new Raspberry Pi, an unforgettable ant update, overcomplicated cooking and an unusual birthday.<ul>
<li>
<p>In <a href="https://therestartproject.org" title="The Restart Project">the spirit of repairing things</a>, I took apart C’s broken toy arch, cursing to myself as I pried open the stubborn end caps with a palette knife. The telltale cracking sound of glue gave way to reveal critical bits of broken plastic, which I’ve now mended.</p>
<p>I was watching “<a href="https://www.bbc.co.uk/iplayer/episode/m000h4n6/the-repair-shop-series-6-episode-4">The Repair Shop</a>” throughout, imagining myself as a regular <a href="http://www.williamkirkrestoration.co.uk">Will Kirk</a>.</p>
</li>
<li>
<p><a href="https://www.gyford.com/phil/writing/2020/04/05/weeknotes/#s2">One of Phil Gyford’s weeknotes last week</a> really made me rethink my own discomfort with “performative whimsy”. I won’t quote it here as it is worth reading it in its entirety.</p>
</li>
<li>
<p>After over a year of running our <a href="https://pi-hole.net">Pi-hole</a>, <a href="https://github.com/mudge/homer">my DNS over HTTPS proxy “Homer”</a>, an <a href="https://github.com/mikebrady/shairport-sync">AirPlay audio player</a> and <a href="/2019/11/12/using-a-raspberry-pi-for-time-machine/">using it for Time Machine backups</a>, my <a href="https://www.raspberrypi.org/products/raspberry-pi-1-model-b-plus/">Raspberry Pi 1 Model B</a> started failing under load.</p>
<p>I decided to treat myself to a brand new <a href="https://www.raspberrypi.org/products/raspberry-pi-4-model-b/">Raspberry Pi 4 Model B</a>, going for a <a href="https://thepihut.com/collections/raspberry-pi-kits-and-bundles/products/raspberry-pi-starter-kit">starter kit from The Pi Hut</a> and a <a href="https://thepihut.com/products/xl-raspberry-pi-4-heatsink">heatsink</a> following <a href="https://mobile.twitter.com/matt_macleod">Matt</a>’s recommendation to keep it from <a href="https://www.raspberrypi.org/blog/thermal-testing-raspberry-pi-4/">overheating and throttling</a>.</p>
<p>Everything is now up and running again and I took the opportunity to replace Homer with <a href="https://docs.pi-hole.net/guides/unbound/">running our own Unbound DNS resolver</a> after reading <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS#Criticism">criticism of DNS over HTTPS</a>.</p>
</li>
<li>
<p>I use <a href="https://github.com/Homebrew/homebrew-cask">Homebrew Cask</a> to install software and noticed that running <code class="language-plaintext highlighter-rouge">brew cask upgrade</code> rarely does anything. However, forcibly calling <code class="language-plaintext highlighter-rouge">upgrade</code> on each installed Cask <em>does</em>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew cask list | xargs brew cask upgrade
<span class="c"># or, if you're running bash/zsh:</span>
<span class="nv">$ </span>brew cask upgrade <span class="si">$(</span>brew cask list<span class="si">)</span>
</code></pre></div> </div>
<p>This is because <a href="https://github.com/Homebrew/homebrew-cask/blob/master/USAGE.md#updatingupgrading-casks">Homebrew Cask ignores applications that update themselves when upgrading</a> but you can override this with <code class="language-plaintext highlighter-rouge">--greedy</code>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew cask upgrade <span class="nt">--greedy</span>
</code></pre></div> </div>
</li>
<li>
<p>An update on our <a href="/2020/04/05/weeknotes-23/">ant situation</a>: while washing the dishes, I spotted a sole ant going for some bait on our window sill. Within seconds, an entire procession appeared so I could finally trace back where they have been coming from.</p>
<p>I discovered they were coming from a broken bit of sealant around a kitchen window and promptly filled it up with more silicone. After checking outside and seeing that our wedding rose was doubling as an ant walkway, I returned to check on the window sill and what I saw next is not easily forgotten.</p>
<p>Previously unseen ants found their exit blocked and started <em>crawling out through the imperceptible gap between the glass windowpane and its frame</em>.</p>
<p>I find that thought particularly enjoyable to turn over in your mind just before bed.</p>
</li>
<li>
<p>It was E’s birthday and our first during the <a href="https://www.gov.uk/government/publications/coronavirus-outbreak-faqs-what-you-can-and-cant-do/coronavirus-outbreak-faqs-what-you-can-and-cant-do">Coronavirus outbreak</a>.</p>
<p>Alongside a pack of <a href="https://www.ocado.com/products/9-happy-birthday-balloons-341958011">Ocado’s “Happy Birthday” balloons</a>, I decided to make <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2017/feb/02/how-to-cook-the-perfect-sticky-orange-cake">Felicity Cloake’s perfect sticky orange cake</a> (substituting Seville oranges with blood oranges) and a slap-up evening meal.</p>
<p>I decided to significantly complicate otherwise simple foods, e.g. instead of regular chips, I attempted <a href="https://www.thefatduckgroup.com/hestons-triple-cooked-chips/">Heston’s Triple Cooked chips</a>, boiling and frying chipped potatoes the night before. While doing that, I was infusing white wine vinegar with tarragon and shallots for <a href="https://thehawksmoor.com">The Hawkmoor</a>’s Stilton hollandaise, freeing time to simmer oranges for two hours in the morning while my sourdough bread baked in its Dutch oven.</p>
<p>Am I <a href="https://www.gq.com/story/the-best-way-to-sous-vide-is-to-shut-up-about-it">a Sous Vide Bro</a>?</p>
</li>
<li>
<p><a href="/2020/03/29/weeknotes-22/">My new webcam</a> arrived earlier than expected and we used it to host a birthday video conference in our living room. While I wasn’t sure how it would turn out (after all, people can’t have side conversations), it was surprisingly fun and it lifted E’s spirits to see friends in Aberdeen, Madrid and Dallas all in one place on an unusual birthday.</p>
</li>
</ul>
Weeknotes 23https://mudge.name/2020/04/05/weeknotes-23/2020-04-05T00:00:00+00:002020-04-05T00:00:00+00:00Reassuring TV shows, negotiating pavements, baby sign language, a home baking first and is Bear Grylls’ TV career over?<ul>
<li>
<p>I was somewhat amazed to see <a href="https://www.channel4.com/programmes/jamie-keep-cooking-and-carry-on">Jamie Oliver’s “Keep Cooking and Carry On”</a>, a cookery show made with <a href="https://www.gov.uk/coronavirus">Coronavirus (COVID-19)</a> in mind. As if there was any doubt how quickly he put this out, he even makes a joke about people <a href="https://www.theguardian.com/news/2020/apr/03/off-our-trolleys-what-stockpiling-in-the-coronavirus-crisis-reveals-about-us">stockpiling pasta</a> in the first episode.</p>
</li>
<li>
<p>Don’t worry, everyone: there is a <a href="https://www.bbc.co.uk/programmes/m000gjn3">new series of “The Repair Shop”</a>. I suspect it was <a href="https://youtu.be/T72TopWbXJg">held in reserve for times of crisis</a>.</p>
</li>
<li>
<p>Thanks to meticulous planning around C’s bed time, E and I managed to watch <a href="https://www.imdb.com/title/tt5083738/">Yorgos Lanthimos’ “The Favourite”</a> and loved it. <a href="https://twitter.com/Stew/status/1088952413573259264">Like Euan</a>, I choose to believe <a href="https://youtu.be/sM_fgqjKnUA">the ballroom dance</a> is real.</p>
</li>
<li>
<p>I haven’t really been taking advantage of <a href="https://www.gov.uk/government/publications/coronavirus-outbreak-faqs-what-you-can-and-cant-do/coronavirus-outbreak-faqs-what-you-can-and-cant-do#when-am-i-allowed-to-leave-the-house">being able to leave the house for one form of exercise a day</a> but did venture out for a stroll with C. While it was a nice change to get fresh air and focus on something more than a few metres from my face, I found the silent negotiation of pavement space with other pedestrians strangely stressful.</p>
</li>
<li>
<p><a href="/2020/03/29/weeknotes-22/">Last week’s ant situation</a> led me to fill conspicuous gaps in our kitchen worktops with silicone and—<a href="https://en.wikipedia.org/wiki/Knocking_on_wood">touch faux marble</a>—we haven’t seen any more curious insectoid invaders.</p>
</li>
<li>
<p>My mum sent us a copy of “<a href="http://www.childs-play.com/bookshop/9781904550044.html">My First Signs</a>”, a book of <a href="https://en.wikipedia.org/wiki/British_Sign_Language">British Sign Language</a> (BSL) signs for children. While <a href="https://en.wikipedia.org/wiki/Baby_sign_language#Pros_and_cons">there is debate about the effects of baby sign language</a>, my sister is profoundly deaf so I’m keen to teach C some sign language regardless.</p>
<p>Reading the signs in the book makes it clear the sign language I use with my sister has morphed from BSL into our own private language. After sending her photos of the book, she immediately proposed her own sign for “toilet” as she is not a fan of <a href="https://youtu.be/rCeVPjIEAeo">how it might be confused with a gesture of defiance</a>.</p>
</li>
<li>
<p>In a home baking first, E made a batch of 14 croissants over three days using a recipe from a <a href="https://www.breadahead.com/courses">Bread Ahead vienoisserie course</a> she did three years ago.</p>
<p class="center"><img src="/i/croissants.jpg" width="375" height="300" alt="" /></p>
<p>We now start our day eating more than our daily recommended amount of butter.</p>
</li>
<li>
<p>For the past few weeks, I have been seeing <a href="https://twitter.com/BearGrylls/status/1238824362775588864">bizarre advertisements on Instagram declaring “Bear Grylls [sic] TV Career is over!”</a> because the “sources of his wealth became known”. Despite reporting these ads as scams to Instagram, they continue to flood in from various accounts with different images but always the same message.</p>
<p>I asked E what <a href="https://en.wikipedia.org/wiki/Bear_Grylls">his real name</a> is and she replied “I don’t know; Michael Microwaves?”</p>
</li>
</ul>
Weeknotes 22https://mudge.name/2020/03/29/weeknotes-22/2020-03-29T16:12:00+00:002020-03-29T16:12:00+00:00Driveway picnics, “The Baby Club”, a new enemy at home and the etymology of DIY.<ul>
<li>
<p>Now we are <a href="https://www.gov.uk/government/publications/full-guidance-on-staying-at-home-and-away-from-others">staying at home</a> and have little outside space of our own, we’re having to get creative when it comes to getting fresh air.</p>
<p>Our solution? Lunchtime driveway picnics!</p>
<p>Not only do we get to enjoy some sunshine while remaining on private property but we also get to use our rarely-seen <a href="https://www.vonshef.com/vs_en/ash-picnic-backpack-for-4">VonShef picnic backpack</a>.</p>
</li>
<li>
<p>Following on from <a href="/2020/03/22/weeknotes-21/">last week</a>, I have been using <a href="https://krisp.ai">Krisp</a> to mute background noise on calls and my colleagues report that it works well.</p>
<p>If you’re interested in trying it out, here’s <a href="https://ref.krisp.ai/u/u140a0e434?utm_source=refprogram&utm_campaign=121761&locale=en-GB">an invite from me</a> that’ll give you an extra free month (and <a href="https://help.krisp.ai/hc/en-us/articles/360012135419-What-is-the-Krisp-Referral-Program-">I’ll get two back</a> in return).</p>
</li>
<li>
<p>After awkwardly wrangling my laptop lid a few too many times, I finally ordered a <a href="https://www.logitech.com/en-gb/product/hd-pro-webcam-c920">Logitech C920 HD PRO Webcam</a> on several people’s recommendation. Sadly, it seems I am not the only person who had this idea and they’re <a href="https://www.amazon.co.uk/Logitech-Calling-Recording-Microphones-Adjustable/dp/B006A2Q81M">out of stock on Amazon</a> until the second week of May.</p>
</li>
<li>
<p>E has started doing yoga classes over <a href="https://zoom.us">Zoom</a>. It is reassuring to hear that every class begins by negotiating volume levels and asking people to mute themselves.</p>
</li>
<li>
<p>As we continue to seek ways to entertain C at home, E discovered <a href="https://www.bbc.co.uk/cbeebies/shows/the-baby-club">CBeebies’ “The Baby Club”</a>. You can now find us occasionally singing its theme song or, its other earworm, “what’s in the bag?”</p>
</li>
<li>
<p><a href="https://twitter.com/skillsmatter/status/1243183726101532674">Skills Matter are back and have restored their videos</a>. After <a href="/2019/11/16/weeknotes-3/">previously lamenting their disappearance</a>, I fired up <a href="https://ytdl-org.github.io/youtube-dl/">youtube-dl</a> as fast as I could to archive <a href="https://skillsmatter.com/skillscasts/2435-lrug-puppet">my</a> <a href="https://skillsmatter.com/skillscasts/6282-exploring-to_proc">talks</a>.</p>
</li>
<li>
<p>I struggled with <a href="https://www.nhs.uk/conditions/migraine/">migraines</a> at the start of the year but, thankfully, wearing glasses has helped a lot. Unfortunately, it also means I have developed some new, unconscious facial expressions. E is not a fan of these new tics and has taken to wordlessly scrunching up her own face as a gentle reminder.</p>
</li>
<li>
<p>During the seven years in our flat, I have done battle with various unwanted house pests: first, moths and then mice. This week we met a new enemy: ants.</p>
<p>My hands are now lacerated and stinging with vinegar solution thanks to pulling out our kitchen appliances and spraying potential entry points.</p>
<p>My true nemesis is not the ants themselves but our cursed dishwasher, propped up on three—not four—flimsy plastic legs, ready to buckle at the slightest provocation. I spent more time yesterday trying to re-level the blasted thing than I did creating barriers out of washing up liquid.</p>
</li>
<li>
<p>E and I caught the end of an episode of “<a href="https://www.bbc.co.uk/iplayer/episode/b07236fx/the-100k-house-tricks-of-the-trade-series-2-4-skye-and-mattmary-and-luke">The £100k House: Tricks of the Trade</a>” and looked at each other as we contemplated Piers Taylor’s parting line:</p>
<blockquote>
<p>But will DIY turn to DI-don’t?</p>
</blockquote>
</li>
</ul>
Weeknotes 21https://mudge.name/2020/03/22/weeknotes-21/2020-03-22T00:00:00+00:002020-03-22T00:00:00+00:00COVID-19, asynchronous working, surprise Smash Mouth fandom, flower arranging and being judged by book titles.<ul>
<li>
<p>I would typically euphemistically refer to “<a href="/2020/03/15/weeknotes-20/">current events</a>” but let’s be plain: <a href="https://en.wikipedia.org/wiki/Coronavirus_disease_2019"><abbr title="Coronavirus disease 2019">COVID-19</abbr></a>, the disease caused by the <a href="https://en.wikipedia.org/wiki/Coronavirus">coronavirus</a> <a href="https://en.wikipedia.org/wiki/Severe_acute_respiratory_syndrome_coronavirus_2"><abbr title="severe acute respiratory syndrome coronavirus 2">SARS-CoV-2</abbr></a> has <a href="https://www.gov.uk/coronavirus">dominated our lives</a> this week.</p>
</li>
<li>
<p>We’ve been rallying together with our neighbours and are sharing a precious online supermarket order. I intend to repay their kindness in bread made from my <a href="/2020/03/15/weeknotes-20/">fortuitous flour stockpile</a>.</p>
</li>
<li>
<p>With <a href="https://www.gov.uk/government/publications/closure-of-educational-settings-information-for-parents-and-carers/closure-of-educational-settings-information-for-parents-and-carers">UK schools now closed</a>, more people need to juggle childcare with remote working and I really want to make this work in my current team. I’m hoping “<a href="https://basecamp.com/guides/how-we-communicate">The Basecamp Guide to Internal Communication</a>” and <a href="https://open.buffer.com/asynchronous-communication/">Hailley Griffis’ “Asynchronous Communication and Why It Matters For Remote Work”</a> will come in handy as we move to work more asynchronously.</p>
</li>
<li>
<p>For those of you joining remote calls when you can’t really control the level of background noise (e.g. because your six-month old is <em>really</em> enthusiastic about eating his porridge), <a href="https://alexspeller.com">Alex</a> recommended <a href="https://krisp.ai">Krisp App</a> to me. I haven’t tried it yet but might give it a go this week.</p>
</li>
<li>
<p>Now that I am working from home for the foreseeable future, I was surprised to discover that the daily decision whether to wear socks or not has become much more significant.</p>
</li>
<li>
<p><a href="https://apps.apple.com/us/app/netnewswire-rss-reader/id1480640210">NetNewsWire, an RSS reader for iOS</a> is now out. I admit I’m still struggling to get back into the habit of using an RSS reader versus mindlessly checking individual websites in turn.</p>
</li>
<li>
<p><a href="https://engineering.shopify.com/blogs/engineering/refactoring-legacy-code-strangler-fig-pattern">Adrianna Chang’s “Refactoring Legacy Code with the Strangler Fig Pattern”</a> looks like <a href="/2020/02/23/weeknotes-17/">another</a> great dive into tackling legacy code in a considered, incremental way.</p>
</li>
<li>
<p>Happy Mother’s Day! Or is that <a href="https://en.wikipedia.org/wiki/Mothering_Sunday">Mothering Sunday</a>?</p>
</li>
<li>
<p><a href="https://cabel.com">Cabel Sasser</a> has been putting up <a href="https://twitter.com/cabel/status/1240001079103778816">Twitter polls for MIDI songs to play on his Roland Music Player</a>. I was proud to contribute to <a href="https://twitter.com/cabel/status/1241031821338406912">Friday’s vote</a>, resulting in <a href="https://twitter.com/cabel/status/1241047282406940672">Smash Mouth’s “All Star”</a> and, as I enjoyed the tinny beats, E stunned me by singing along, revealing that she knew <em>all of the lyrics</em>.</p>
</li>
<li>
<p>Keeping the weekend spirit alive, yesterday I indulged in a little flower arranging, adding a little homemade flower food made from a squirt of bleach and a sprinkling of granulated sugar.</p>
</li>
<li>
<p>To celebrate the <a href="https://www.metoffice.gov.uk/weather/learn-about/weather/seasons/winter/when-does-winter-start">end of winter</a>, I removed the Sellotape from our patio door and stepped out onto our tiny spit of outside space. Our planter resembled something out of “<a href="https://www.playstation.com/en-gb/games/the-last-of-us-remastered-ps4/">The Last of Us</a>” so I cleared it of dead salad plants and tilled the soil. It wasn’t quite <a href="https://www.theguardian.com/environment/2019/jun/08/forest-bathing-japanese-practice-in-west-wellbeing">forest bathing</a> but was surprisingly restorative.</p>
</li>
<li>
<p>I learnt that the strange outcrop of salt on the outside of our water softener is known as “salt creep” and tackled it with a combination of a sturdy brush and <a href="/2020/01/26/weeknotes-13/">one of my three trusty cans of WD-40</a> (<a href="https://www.wd40.com/products/silicone-lubricant/">WD-40® Specialist® Water Resistant Silicone Lubricant</a>, for those of you keeping track).</p>
</li>
<li>
<p>One night, as I put my Kindle down, E asked me what I was reading and I replied “<a href="https://www.hachettebookgroup.com/titles/adrian-tchaikovsky/children-of-ruin/9780316452540/">Children of Ruin</a>”. She scoffed, “of course; that’s a very Paul book title.”</p>
</li>
</ul>
Weeknotes 20https://mudge.name/2020/03/15/weeknotes-20/2020-03-15T00:00:00+00:002020-03-15T00:00:00+00:00Working from home, low sodium bread, workplace superpowers and yet another secure shoelace knot.<ul>
<li>
<p>Following <a href="/2020/03/08/weeknotes-19/">recent</a> <a href="https://www.nhs.uk/conditions/coronavirus-covid-19/">events</a>, I’ve been working from home all week.</p>
<p>Thankfully, I recently cobbled together a standing desk out of an old <a href="https://www.benq.eu/en-eu/index.html">BenQ</a> monitor perched atop <a href="https://www-cs-faculty.stanford.edu/~knuth/taocp.html">Donald Knuth’s “The Art of Computer Programming”</a> and <a href="https://www.hachette.co.uk/titles/harold-mcgee/mcgee-on-food-and-cooking-an-encyclopedia-of-kitchen-science-history-and-culture/9780340831496/">Harold McGee’s “McGee on Food & Cooking”</a>. Combining that with <a href="https://tuple.app">Tuple</a> for pair-programming and <a href="https://miro.com">Miro</a> as a virtual whiteboard has made the switch from being co-located to being fully remote easier.</p>
<p>If you’re new to remote working, <a href="https://blog.alicegoldfuss.com/work-in-the-time-of-corona/">Alice Goldfuss’ “Work in the Time of Corona”</a> has some good, practical advice about maintaining good mental health when working from home.</p>
</li>
<li>
<p>Somewhat late to <a href="https://www.rottentomatoes.com/m/hannah_gadsby_nanette">the party</a>, we finally watched—and were moved by—<a href="https://www.netflix.com/title/80233611">Hannah Gadsby’s “Nanette”</a>.</p>
</li>
<li>
<p>As someone who often uses <a href="https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupltcommitgt"><code class="language-plaintext highlighter-rouge">git commit --fixup</code></a> to <a href="https://blog.mocoso.co.uk/talks/2015/01/12/telling-stories-through-your-commits/">tell stories through my commits</a> before code review, <a href="https://github.com/tummychow/git-absorb"><code class="language-plaintext highlighter-rouge">git absorb</code></a> could save me a lot of time (via <a href="https://twitter.com/burntsushi5/status/1238115091868012545">Andrew Gallant</a>).</p>
</li>
<li>
<p>I baked a loaf of bread with less salt than usual for C to try (although he’d have to eat 120g of my usual recipe to meet <a href="https://www.nhs.uk/common-health-questions/childrens-health/how-much-salt-do-babies-and-children-need/">his daily limit</a>). Compared to his reaction to the kale and butternut squash on his plate, I’d say it was a rousing success.</p>
</li>
<li>
<p>While I refuse to <a href="https://www.theguardian.com/world/2020/mar/02/coronavirus-fears-see-stockpiling-of-food-and-medicines-across-uk">stockpile</a>, I do have 41 kg of bread flour in my kitchen from a spontaneous purchase back in January.</p>
</li>
<li>
<p><a href="https://tomstu.art">Tom</a> and I were discussing the workplace superpower of “saying you’ll do a thing and then doing it” and he linked me to a <a href="https://twitter.com/lucyprebblish/status/1236334970869813248">Twitter thread by Lucy Prebble arguing competence is culturally undervalued</a>.</p>
</li>
<li>
<p>I started playing “<a href="https://www.nintendo.com/games/detail/the-legend-of-zelda-links-awakening-switch/">The Legend of Zelda: Link’s Awakening</a>” and it is wonderful so far. Despite owning a <a href="https://en.wikipedia.org/wiki/Super_Nintendo_Entertainment_System">SNES</a>, the first Zelda game I really played was 1998’s <a href="https://www.nintendo.co.uk/Games/Nintendo-64/The-Legend-of-Zelda-Ocarina-of-Time-269536.html">“The Legend of Zelda: Ocarina of Time”</a> on a <a href="https://en.wikipedia.org/wiki/Nintendo_64">Nintendo 64</a> so this is the first top-down perspective Zelda I’ve ever played.</p>
</li>
<li>
<p>I put up a mirror in our bedroom today. Another day, another <a href="https://www.fieggen.com/shoelace/secureknot.htm">Ian’s secure shoelace knot</a>.</p>
</li>
</ul>
Weeknotes 19https://mudge.name/2020/03/08/weeknotes-19/2020-03-08T00:00:00+00:002020-03-08T00:00:00+00:00The Missing Hit, hot lemon and honey, Ugly Delicious and a reassuringly dumb baby monitor.<ul>
<li>
<p>I followed <a href="https://twitter.com/waxy/status/1235716731877961728">Andy Baio’s recommendation</a> and enjoyed listening to <a href="https://gimletmedia.com/shows/reply-all/o2h8bx/158-the-case-of-the-missing-hit">Reply All’s “The Case of the Missing Hit”</a>. To avoid spoiling anything, I won’t say any more but it’s a good one.</p>
</li>
<li>
<p>Despite the current climate of <a href="https://twitter.com/getbentsaggy/status/1235229738504990720">hand washing</a> and sneezing into elbows, I’ve managed to catch a cough. <a href="https://111.nhs.uk">NHS 111</a> reassures me it is nothing more and I have been consuming a record number of lemons in the form of hot drinks. The timing is unfortunate but at least I don’t need to worry about scurvy.</p>
</li>
<li>
<p><a href="https://www.theportablebrain.com">Jean</a> reminded me that the second season of “<a href="https://www.netflix.com/title/80170368">Ugly Delicious</a>” is now on Netflix.</p>
<p>I loved the first season for exploring the complicated notion of “authenticity” in food and its related sociopolitical baggage. While waiting for more, I tried to watch <a href="https://www.netflix.com/title/81038022">“Breakfast, Lunch & Dinner” (also starring David Chang)</a> but the first episode—where Chang gets high with his celebrity friend Seth Rogen—put me off. Thankfully, the first episode of the new season centres on Chang as a nervous dad-to-be so I’m hooked once more.</p>
<p>From <a href="https://www.instagram.com/p/B9NzY9KhpeW/">Chang’s Instagram</a>, I discovered there is <a href="https://en.wikipedia.org/wiki/Zhuazhou">ritual called “Zhuazhou”</a> where a child picks an object that forecasts their future. I’m now planning to perform this ritual with C, placing before him a keyboard, a sword and a plastic dough scraper.</p>
</li>
<li>
<p>We successfully completed the <a href="/2020/03/02/weeknotes-18/">2,000 year old problem of moving C into his own room</a> and so were finally in the market for some sort of baby monitor. As a real <a href="https://twitter.com/internetofshit">Internet of Things sceptic</a>, I was adamant that we buy no device with the word “Smart” in its name. Thankfully, we bought the decidedly dumb <a href="https://shop.bt.com/products/bt-audio-baby-monitor-400-087429-CLWL.html">BT Audio Baby Monitor 400</a> and it is wonderful in its simplicity.</p>
</li>
</ul>
Weeknotes 18https://mudge.name/2020/03/02/weeknotes-18/2020-03-02T00:00:00+00:002020-03-02T00:00:00+00:00The Tube, dad chat, “Shape Up”, improving sleep with first-order logic and what it means when the clown is down.<ul>
<li>
<p>Thanks to all who helped with <a href="/2020/02/10/weeknotes-15/">my job search</a>. I started work with a new client today, commuting on the Tube for the first time in eight years.</p>
</li>
<li>
<p>Speaking of the Tube, I hate that all news coverage of Coronavirus features a photo of someone wearing a face mask on public transport even when the news isn’t about the UK.</p>
</li>
<li>
<p>Every time I mention C to someone, I think of <a href="https://twitter.com/Stew">Euan</a>’s mastery of undersharing. I remember first hearing he had a cat after working with him for <em>four</em> years.</p>
</li>
<li>
<p>Speaking of dad chat, I can’t wait to invest in a <a href="https://youtu.be/LSYsmgZEKv0">Father-Son Podcasting Microphone</a>:</p>
<blockquote>
<p>Well, it’s tough, you know? I mean, there’s no book you can read about being a dad.</p>
<p>Actually, there are several.</p>
</blockquote>
</li>
<li>
<p>I’ve always had a soft spot for macOS indie developers. I have been a customer of <a href="https://www.obdev.at/products/launchbar/index.html">LaunchBar</a>, <a href="https://ranchero.com/netnewswire/">NetNewsWire</a> (and its <a href="https://inessential.com/2018/08/31/netnewswire_comes_home">recent update</a>), <a href="https://c-command.com/dropdmg/">DropDMG</a>, <a href="https://rogueamoeba.com/airfoil/mac/">Airfoil</a>, <a href="https://www.shirt-pocket.com/SuperDuper/SuperDuperDescription.html">SuperDuper!</a>, <a href="https://www.kangacode.com/interarchy/">Interarchy</a>, <a href="https://www.omnigroup.com/omnigraffle">OmniGraffle</a>, <a href="https://www.alsoft.com">DiskWarrior</a>, <a href="https://www.barebones.com/products/bbedit/">BBEdit</a>, <a href="https://panic.com/transmit/">Transmit</a>, <a href="https://c-command.com/spamsieve/">SpamSieve</a> and <a href="https://macromates.com">TextMate</a>. This is partly why I <a href="/2020/02/16/weeknotes-16/">enjoyed hearing about John Siracusa’s new apps</a> and why I’m now trying <a href="https://furbo.org">Craig Hockenberry</a>’s new notepad app—<a href="https://tot.rocks">Tot</a>—after reading <a href="https://daringfireball.net/2020/02/tot">John Gruber’s summary of it</a>.</p>
<p>I secretly wish I had an idea for an app of my own.</p>
</li>
<li>
<p>I read <a href="https://basecamp.com/shapeup">Ryan Singer’s “Shape Up: Stop Running in Circles and Ship Work that Matters”</a> and—despite unnecessarily dismissing existing agile practices in the <a href="https://basecamp.com/shapeup/0.1-foreword">foreword</a>—found a lot I agreed with.</p>
<p>I’m increasingly convinced of the need for something like <a href="https://basecamp.com/shapeup/1.1-chapter-02">shaping</a> before something is picked up by a development team. The struggle, as Singer writes, is getting the right level of detail: neither <a href="https://basecamp.com/shapeup/1.1-chapter-02#words-are-too-abstract">too abstract</a> nor <a href="https://basecamp.com/shapeup/1.1-chapter-02#wireframes-are-too-concrete">too concrete</a>.</p>
<p>I was especially intrigued by the idea of <a href="https://basecamp.com/shapeup/1.2-chapter-03#setting-the-appetite">appetite, not estimation</a> as I’ve often found people unwilling to reveal truly how much they want a particular feature or project and drawing this out up front is extremely appealing.</p>
<p>The idea of <a href="https://basecamp.com/shapeup/1.5-chapter-06">pitch documents</a> that can be reviewed asynchronously by members of the team sounds very similar to a Request For Comments (RFC) process (e.g. <a href="https://github.com/rust-lang/rfcs">RFCs for changes to Rust</a>, <a href="https://github.com/yarnpkg/rfcs">Yarn</a> and <a href="https://github.com/emberjs/rfcs">Ember</a>; see also <a href="https://blog.pragmaticengineer.com/scaling-engineering-teams-via-writing-things-down-rfcs/">Gergely Orosz’ “Scaling Engineering Teams via Writing Things Down and Sharing - aka RFCs”</a>): something I am a huge fan of if you’re looking to introduce both rigour and equitability when proposing significant changes to teams and systems.</p>
</li>
<li>
<p>We took a trip to IKEA to get C a new cot and a Billy bookcase (one of which is sold <a href="https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/">every 5 seconds</a>). I spent a lot of time in the kids’ section, fawning over the train sets, toy kitchen and its many, many accessories. Luckily for my wallet, C is still too young for them.</p>
</li>
<li>
<p>As one of my many “jobs to complete before returning to work”, I lovingly re-seasoned our wooden chopping board following <a href="https://www.seriouseats.com/2015/09/how-to-season-and-maintain-a-wood-cutting-board.html">J. Kenji López-Alt’s advice</a>. There is something therapeutic about rubbing a wooden board with coarse salt and half a lemon.</p>
</li>
<li>
<p>The major thing E and I have been working on is getting C out of our bedroom and into his own room in the hope we can all improve our quality of sleep. Not wanting to confuse him with too much change at once, we first moved ourselves out and onto our sofa bed. When contemplating the next step, I was reminded of the <a href="https://en.wikipedia.org/wiki/Wolf,_goat_and_cabbage_problem">2,000 year old problem of the wolf, goat and cabbage</a>.</p>
<p>In turn, this reminded me of a great <a href="https://london.computation.club">London Computation Club</a> meeting back in 2015: <a href="https://github.com/computationclub/computationclub.github.io/wiki/The-New-Turing-Omnibus-Chapter-58-Predicate-Calculus">“The New Turing Omnibus, Chapter 58: Predicate Calculus”</a> when <a href="https://tomstu.art">Tom</a> showed us how to use <a href="https://en.wikipedia.org/wiki/First-order_logic">first-order logic</a> to mechanically resolve the problem.</p>
<p>Bonus trivia: that was the same meeting that <a href="https://github.com/computationclub/computationclub.github.io/wiki/The-New-Turing-Omnibus-Chapter-58-Predicate-Calculus#show--tell">Chris announced his work on his programming language, Sentient</a>.</p>
</li>
<li>
<p>E and I have many phrases that would be incomprehensible to others but a recent favourite is the following emoji-only message:</p>
<blockquote>
<p>🤡⬇️</p>
</blockquote>
<p>Of course, this translates to “the clown is down” which is a quote from <a href="https://en.wikipedia.org/wiki/The_Parent_Rap">The Simpsons’ “The Parent Rap”</a> we’ve distorted to mean “our baby is asleep”.</p>
</li>
</ul>
Weeknotes 17https://mudge.name/2020/02/23/weeknotes-17/2020-02-23T00:00:00+00:002020-02-23T00:00:00+00:00A car park disaster, animated GIFs, not rewriting legacy systems and how the internet really works.<ul>
<li>
<p>My father-in-law was on <a href="https://www.bbc.co.uk/programmes/m000fk4f">The One Show</a> discussing <a href="https://www.newcivilengineer.com/archive/liverpool-car-park-fire-a-perfect-storm-02-02-2018/">why a multi-storey car park in Liverpool burnt down, destroying 1,300 cars</a>.</p>
<p id="hero" class="center"><a href="https://www.bbc.co.uk/programmes/m000fk4f"><img src="/i/hero.gif" width="480" height="270" alt="" /></a></p>
</li>
<li>
<p>In an effort to be a better son-in-law, I extracted an animated GIF of his heroic introduction and, thanks to <a href="https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/">Collin Burger’s “How to make GIFs with FFMPEG”</a>, ended up using this one-liner:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>ffmpeg <span class="nt">-ss</span> 1499.5 <span class="nt">-t</span> 2.5 <span class="nt">-i</span> oneshow.mp4 <span class="nt">-filter_complex</span> <span class="s2">"[0:v] fps=12,scale=480:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse"</span> hero.gif
</code></pre></div> </div>
</li>
<li>
<p>Following a recommendation from <a href="https://twitter.com/cassarani">Leo</a>, I listened to <a href="https://oxide.computer/blog/on-the-metal-6-kenneth-finnegan/">an episode of “On the Metal” interviewing Kenneth Finnegan about starting his own internet exchange</a>. Despite working on the web for 20 years, it quickly became clear I had no idea how the internet really worked.</p>
</li>
<li>
<p>I enjoyed <a href="https://understandlegacycode.com/blog/avoid-rewriting-a-legacy-system-from-scratch-by-strangling-it/">Nicolas Carlo’s “Avoid rewriting a legacy system from scratch, by strangling it”</a> about the <a href="https://martinfowler.com/bliki/StranglerFigApplication.html">Strangler Fig Application pattern</a>. It reminded me of the time I failed to migrate the main database in a legacy system from <a href="https://www.mongodb.com">MongoDB</a> to <a href="https://www.postgresql.org">PostgreSQL</a> while trying very hard <a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/">not to rewrite it from scratch</a>.</p>
<p>When we finally cancelled the project, I wrote a long explanation to the whole company (like an <a href="https://qz.com/504661/why-etsy-engineers-send-company-wide-emails-confessing-mistakes-they-made/">Etsy PSA</a>) called “The State of Custard”. Sadly, having left the company, that is now locked inside someone else’s <a href="https://basecamp.com">Basecamp</a> account.</p>
</li>
<li>
<p>As part of my <a href="/2020/02/10/weeknotes-15/">job search</a>, I have spent a lot of time editing my <a href="https://www.linkedin.com/in/paulmucur/">LinkedIn profile</a>. In particular, I have changed my “headline” many times in an attempt to concisely explain what I do. At the time of writing it is “Consultant Technical/Engineering Lead”, shamelessly inspired by <a href="https://www.linkedin.com/in/andrewmcdonough/">Andrew McDonough</a> but I’m sure I will continue to agonise about it.</p>
</li>
<li>
<p>Having tempted fate by writing that our <a href="/2020/02/16/weeknotes-16/">white noise snafus were over last week</a>, our Bluetooth speaker simply stopped charging. Sadly, no amount of <a href="/2020/01/26/weeknotes-13/">WD40</a> could fix the problem.</p>
</li>
</ul>
Weeknotes 16https://mudge.name/2020/02/16/weeknotes-16/2020-02-16T00:00:00+00:002020-02-16T00:00:00+00:00Typing practice, an erosion of attention, puzzle solving and wishing we’d done everything differently.<ul>
<li>
<p><a href="/2020/02/10/weeknotes-15/">The job search</a> continues.</p>
</li>
<li>
<p>I’m slowly getting used to <a href="/2020/02/03/weeknotes-14/">my split keyboard</a> thanks to some deliberate <a href="https://www.keybr.com">typing practice</a> recommended by <a href="https://www.instagram.com/_weszlem/">Maciej</a>. I still managed to send an unfinished email instead of pressing the backspace key though.</p>
</li>
<li>
<p>I was intrigued by <a href="https://www.theguardian.com/lifeandstyle/2020/feb/07/how-to-stop-spread-of-fake-news-oliver-burkeman">Oliver Burkeman’s “How to stop the spread of fake news? Pause for a moment”</a> about <a href="https://lindastone.net/qa/continuous-partial-attention/">continuous partial attention</a>:</p>
<blockquote>
<p>We tend to think of distraction as an all-or-nothing affair: either you’re concentrating successfully on something, or else you’ve been distracted by Twitter or Netflix yet again. But this is more like an erosion of attention, consistent with at least nominally remaining focused on the task at hand.</p>
</blockquote>
<p>The article’s conclusion reminded me of <a href="https://xkcd.com/481/">the xkcd comic, “Listen to Yourself”</a> (which <a href="https://blog.xkcd.com/2008/10/08/youtube-audio-preview/">was actually implemented</a>).</p>
</li>
<li>
<p>Shane’s mention that he was trying to write a solver for the <a href="https://youtu.be/73Meh3NPno4">“Snake Egg Puzzle”</a> <a href="http://technicallyshane.com/2020/02/12/week6.html">in his weeknotes</a> led me to recommend <a href="https://sentient-lang.org">Chris Patuzzo’s Sentient programming language</a> for the task and mention the problem to Chris. A few days later and <a href="https://github.com/tuzz/snake-egg-puzzle">Chris successfully wrote a program to solve the puzzle and generate new ones</a>.</p>
</li>
<li>
<p>We’ve managed to drastically reduce the number of <a href="/2020/01/26/weeknotes-13/">white noise snafus</a> by using <a href="https://support.apple.com/en-gb/HT202612">Guided Access</a> on an old iPad. This way we can still enable and disable the susurration of a mild downpour but have the device ignore all accidental button presses. Thanks to <a href="https://tomstu.art">Tom</a> for drawing my attention to this feature of iOS.</p>
</li>
<li>
<p>I enjoyed <a href="https://www.robinsloan.com/notes/home-cooked-app/">Robin Sloan’s story of building a messaging app for his family, “An app can be a home-cooked meal”</a>. For similar reasons, I’ve been enjoying hearing about <a href="https://hypercritical.co/apps/">John Siracusa’s new macOS apps written to meet his own needs</a> on <a href="https://atp.fm">Accidental Tech Podcast</a> (<a href="https://atp.fm/episodes/360">“Big Hole In the Middle”</a> and <a href="https://atp.fm/episodes/365">“Day-One Cowboy”</a>).</p>
</li>
<li>
<p>On a slightly less optimistic note, I also enjoyed <a href="https://usesthis.com/interviews/graydon.hoare/">Daniel Bogan’s interview with the creator of the Rust programming language, Graydon Hoare</a>:</p>
<blockquote>
<p>Software-wise I really wish we’d done almost everything differently. The focus has been on eyeballs and engagement, not like “does it work” or “can it resist attack by a bad actor”. Almost no modern software works well or is safe in any serious sense. It’s always on fire. A sinking ship with everyone frantically bailing. Swiss cheese. Pick your metaphor. So my dream setup would involve “software that has any sort of reliability”. Which probably requires rewinding time to before the personal computing era and enacting liability legislation or something.</p>
</blockquote>
</li>
</ul>
Weeknotes 15https://mudge.name/2020/02/10/weeknotes-15/2020-02-10T00:00:00+00:002020-02-10T00:00:00+00:00Looking for work, laughter, stinging nostrils and writing weeknotes for the future self.<ul>
<li>
<p>After thoroughly enjoying my parental leave, I am now <a href="https://www.ghostcassette.com">looking for contract work</a> so if you know anyone who could make use of a former CTO/Technical Lead with 14 years’ experience developing Ruby and JavaScript in teams, <a href="mailto:paul@ghostcassette.com">please get in touch</a>.</p>
</li>
<li>
<p>It took me a little while but I am loving “<a href="https://www.mobiusdigitalgames.com/outer-wilds.html">Outer Wilds</a>” (and I am <a href="https://www.polygon.com/2019/12/13/21011871/outer-wilds-goty-best-games-of-the-year" title="Polygon's Game of the Year #1: Outer Wilds">not the only one</a>). I’m looking forward to watching <a href="https://youtu.be/LbY0mBXKKT0">Noclip’s “The Making of Outer Wilds - Documentary”</a> when I’m done.</p>
</li>
<li>
<p>I nearly nodded my head clean off my neck while reading <a href="https://basecamp.com/about/policies/until-the-end-of-the-internet">Basecamp’s “Until the End of the Internet”</a> policy for their products.</p>
</li>
<li>
<p>For that reason, I was very excited to <a href="https://hey.com">read the teaser for Basecamp’s new email service, “Hey”</a> coming in April 2020.</p>
</li>
<li>
<p>The <a href="https://podcast.panic.com/#episode002">second episode of the Panic Podcast, “Pantscast”</a>, is funny for admittedly puerile reasons but it made me laugh all the same. I’m not sure how I missed the app when it was <a href="https://daringfireball.net/linked/2009/03/09/pantscast">first released</a>.</p>
</li>
<li>
<p>Speaking of laughter, let me join <a href="https://www.theguardian.com/tv-and-radio/2020/feb/07/frankie-boyles-tour-of-scotland-review-bbc2">many</a> <a href="https://tomstu.art/weeknotes-5-nothing-notable">others</a> by recommending <a href="https://www.bbc.co.uk/iplayer/episode/m000f1vj/frankie-boyles-tour-of-scotland-series-1-1-aberdeen-to-oban">“Frankie Boyle’s Tour of Scotland”</a>. There are far too many lines to quote here but let’s just say I have a soft spot for his monologue about the beautiful <a href="https://en.wikipedia.org/wiki/Glen_Coe">Glen Coe</a>.</p>
</li>
<li>
<p>I didn’t take any notes this week as I spent a large part of it blowing my nose and stinging my nostrils with a mixture of steam and <a href="https://www.olbas.co.uk/products/olbas-oil/">Olbas Oil</a>.</p>
</li>
<li>
<p>Just as blogging about blogging or podcasting about podcasting is a real faux pas, thinking about writing this week’s notes reminded me of a passage from <a href="https://www.penguin.co.uk/books/181/181041/the-mark-and-the-void/9780241953860.html">Paul Murray’s “The Mark and the Void”</a>:</p>
<blockquote>
<p>Although technology has the capability now to record entire lifetimes, meaning that every moment may be pulled from the foaming sea of oblivion to the dry land of perfect recall, the mythic power of the photograph nevertheless relates to the future, and not to the past. Every recording conceals the secret fantasy of a future self who will observe it; this future self is himself the simulacrum, the <em>persona ficta</em>. He exists beyond time, beyond action, beyond need; his only function is to witness the continuum of the past, as he might observe the steps that brought him to godhood. Through this fantasy, time is transformed from the condition of loss into a commodity that may be acquired and stockpiled; rather than disappear ceaselessly into the past, life accumulates, each moment becoming a unit of a total self that is the culmination of our experiences in a way that we—biological composites who profligately shed our cells, our memories and our possessions—can never be.</p>
</blockquote>
</li>
</ul>
Weeknotes 14https://mudge.name/2020/02/03/weeknotes-14/2020-02-03T00:00:00+00:002020-02-03T00:00:00+00:00The anaesthetic properties of cloves, saline rinses, split keyboards and yelling at clouds.<ul>
<li>
<p>Three days before attending a “<a href="https://www.nhs.uk/start4life/weaning/">starting solids</a>” workshop at a local health centre, E made several batches of soup as I pressed a whole clove against the back of my mouth.</p>
</li>
<li>
<p>After two days of alternating doses of paracetamol and ibuprofen as rapidly as enclosed leaflets allowed, my dentist confirmed I had <a href="https://en.wikipedia.org/wiki/Pericoronitis">pericoronitis</a> (do not click unless you have a stomach for pink, glistening medical photography) and sent me away with a prescription for antibiotics.</p>
</li>
<li>
<p>Brushing your teeth and doing a warm saline rinse every time you eat really makes one reconsider snacking. (Apologies to <a href="https://natbuckley.co.uk">Nat</a> and <a href="https://tomstu.art">Tom</a> for mentioning <a href="https://natbuckley.co.uk/2019/06/02/weeknotes-20-the-wretched-task/">the wretched task</a>.)</p>
</li>
<li>
<p>The elation felt when you can once again eat your morning porridge without first thoroughly mashing banana with a fork is exquisite.</p>
</li>
<li>
<p>I am clumsily typing this on a <a href="https://www.microsoft.com/accessories/en-gb/products/keyboards/sculpt-ergonomic-desktop/l5v-00006">Microsoft Sculpt Ergonomic keyboard</a>. The split keys make me realise the homegrown touch typing I’ve been practicing for the past two decades might not be <em>technically</em> correct.</p>
</li>
<li>
<p>I heartily second <a href="https://tomstu.art/weeknotes-3-sweating-and-swearing">Tom’s recommendation</a> for “<a href="https://www.imdb.com/title/tt1489887/">Booksmart</a>” especially as it is currently <a href="https://www.amazon.co.uk/Booksmart-Kaitlyn-Dever/dp/B07V2SLM3K">free for Prime Video subscribers in the UK</a>.</p>
</li>
<li>
<p>I took <a href="https://youtu.be/l2tVGctt4Zo">Ze Frank’s “Human Test For People Who Work In An Office”</a>:</p>
<blockquote>
<p>Has a co-worker ever said—in the nicest way—“you look tired” when, in fact, you weren’t tired at all but now you feel bad because you know you look like crap for some unknown reason and somehow your co-worker gets credit for caring?</p>
</blockquote>
</li>
<li>
<p>I thought of <a href="https://www.buzzfeed.com/benrosen/how-to-snapchat-like-the-teens">Ben Rosen being taught to use Snapchat by his 13-year-old sister</a> as I posted my first <a href="https://help.instagram.com/1660923094227526">Instagram story</a> despite having previously railed at them. I hadn’t understood that people’s responses would be both private and much more personal than a shallow “like”.</p>
<p>Maybe I’ve been <a href="https://knowyourmeme.com/memes/old-man-yells-at-cloud">yelling at clouds</a> all along.</p>
</li>
</ul>
Weeknotes 13https://mudge.name/2020/01/26/weeknotes-13/2020-01-26T00:00:00+00:002020-01-26T00:00:00+00:00Posture, mouth guards, Guy Fieri’s “Trash Can Nachos” and hearing things in the noise.<ul>
<li>
<p>Shoulders back and down.</p>
</li>
<li>
<p>E, C and I went to see “<a href="https://www.imdb.com/title/tt5727208/">Uncut Gems</a>” at <a href="https://www.barbican.org.uk/whats-on/series/parent-and-baby-screenings">baby cinema</a>. As the stress and tension in the film grew relentlessly, I thought of my last two trips to the dentist about the burgeoning “vertical cracks” on my teeth from <a href="https://www.nhs.uk/conditions/teeth-grinding/">grinding</a>.</p>
</li>
<li>
<p>For that reason, I found myself curled up in bed, a neoprene wrist support velcroed to one arm and a remouldable mouth guard separating my long-suffering molars. Not so dissimilar to Bart in “<a href="https://en.wikipedia.org/wiki/The_Last_Temptation_of_Homer">The Last Temptation of Homer</a>”, laden with orthopaedic shoes, hair matted with medicated salve and voice hoarse with throat spray.</p>
</li>
<li>
<p>A friend caught me describing a room as “roasty toasty” and successfully deduced I spend most of my time in conversation with a four-month old baby.</p>
</li>
<li>
<p>Following <a href="https://tomstu.art/weeknotes-2-the-good-half">Tom Stuart’s recommendation</a>, I started watching <a href="https://www.bonappetit.com/video/series/reverse-engineering">Chris Morocco’s “Reverse Engineering”</a>. In it, Morocco attempts to recreate dishes in two days after only tasting them while blindfolded.</p>
<p>From <a href="https://youtu.be/3eVMgUrEazA">Gordon Ramsay’s Beef Wellington</a> to <a href="https://youtu.be/E3e20718ioY">Guy Fieri’s “Trash Can Nachos”</a>, he does consistently well despite never having made these dishes before. There’s a mastery in his ability to quickly recognise various components and techniques in dishes, e.g. a <a href="https://en.wikipedia.org/wiki/Mornay_sauce">Mornay sauce</a> or a <a href="https://en.wikipedia.org/wiki/Roux">roux</a>. Just don’t ask him to <a href="https://youtu.be/zlG_iUEybgM">identify a specific cheese</a>.</p>
</li>
<li>
<p>In an attempt to improve my posture, I’ve been doing yoga every morning under E’s tutelage. Naturally, I made the ritual my own by struggling to use Spotify to find appropriately soothing muzak on our aging iPad and ended up replacing our entire music setup with a <a href="https://www.raspberrypi.org">Raspberry Pi</a> running <a href="https://github.com/mikebrady/shairport-sync">Shairport Sync</a>. We can now stream any audio of our choice over <a href="https://support.apple.com/en-gb/HT202809">AirPlay</a> and I’m no longer allowed to procrastinate.</p>
</li>
<li>
<p>Speaking of which: shoulders back and down.</p>
</li>
<li>
<p>Among our desperate attempts to improve C’s sleep, we use “<a href="https://www.rainrainapp.com">Rain Rain</a>” and a small Bluetooth speaker to play the sound of running water throughout the night. When we’re not accidentally increasing the volume or fast-forwarding to the sound of a raging thunderstorm, I hear the strangest things in the white noise: snatches of a distant news broadcast, the wailing of some audacious post-rock and the hiss of Icelandic death metal on an AM radio.</p>
</li>
<li>
<p>The self check-in at my GP was out of order this week. After speaking with the receptionist, I settled in the waiting room and looked up to see a display showing a “<a href="https://www.microsoft.com/en-us/microsoft-365/windows/end-of-windows-7-support">Your Windows 7 PC is out of support</a>” screen.</p>
</li>
<li>
<p>Watching <a href="https://youtu.be/VPYhem4GRGY">Claire Saffitz soak toast and popcorn in water to flavour jelly beans</a> has inspired me to get back into making ice cream. I enjoyed making <a href="https://www.telegraph.co.uk/foodanddrink/recipes/10957755/Momofukus-Cereal-Milk-ice-cream-recipe.html">Christina Tosi’s “Cereal Milk ice cream”</a> but now I’m wondering what else I could steep.</p>
</li>
<li>
<p>People are never impressed when I talk about my collection of not one, not two but <em>three</em> types of <a href="https://www.wd40.com">WD40</a>. Only I will know the deep satisfaction of spraying <a href="https://www.wd40.com/products/contact-cleaner/">WD40 electrical contact cleaner</a> into sticky game controllers.</p>
</li>
<li>
<p>One more time: shoulders back and down.</p>
</li>
</ul>
Weeknotes 12https://mudge.name/2020/01/19/weeknotes-12/2020-01-19T00:00:00+00:002020-01-19T00:00:00+00:00Yearning for space, failing to make a video, ruining Christmas presents, smart meters and musculoskeletal woes.<ul>
<li>
<p>Since returning to London after our stay in a Glasgow tenement last week, I’ve yearned for the high ceilings and sheer <em>space</em> we enjoyed there. You can often find me standing in each room of our relatively pokey flat, a look of concentration on my face as I desperately try to rearrange furniture in my mind to somehow give us a single morsel more space. So far, I’ve only managed to do this successfully in our bedroom; disappointingly, everywhere else requires the removal of at least one major piece of furniture.</p>
</li>
<li>
<p>E asked me who <a href="https://m.imdb.com/name/nm0147147/">Henry Cavill</a> plays in <a href="https://www.netflix.com/title/80189685">his new Netflix show</a> and I replied, “the titular Witcher.”</p>
</li>
<li>
<p><a href="https://youtu.be/N8jL_ub9gy4">Tom Stuart’s video making Anna Jones’ cassoulet</a> inspired me to make a similar video showing how I bake bread. Not having made a video since I was a teenager, I planned out the various parts I’d need to capture:</p>
<ol>
<li>Making a starter</li>
<li>Making a leaven</li>
<li>Making the initial dough</li>
<li>Autolyse</li>
<li>Adding salt and water</li>
<li>Folding and bulk fermentation</li>
<li>Initial shaping</li>
<li>Final shaping</li>
<li>Proving overnight</li>
<li>Preparing the oven</li>
<li>Scoring</li>
<li>Baking in a Dutch oven</li>
</ol>
<p>On reflection, I think I might have to pick something simpler for my first foray into moving pictures.</p>
</li>
<li>
<p>In an attempt to curb my tissue habit, E gave me a set of handkerchiefs for Christmas. I decided to clean them this morning and may have squirted bleach all over them before really thinking through my actions resulting in an accidental and not entirely pleasant tie-dye.</p>
</li>
<li>
<p>I’ve been going back through <a href="https://adam-buxton.co.uk/podcasts">episodes of Adam Buxton’s podcast</a> and especially enjoyed <a href="https://adam-buxton.co.uk/podcasts/ep79-david-sedaris">his interview with David Sedaris</a>. It might even have convinced me to purchase my very first audiobook just so that I can hear more of Sedaris’ wonderful storytelling.</p>
</li>
<li>
<p>We had our electricity meter replaced with a “smart” one which automatically reports readings to our supplier every month. It came with an “in-home display” but my creeping sense of paranoia has:</p>
<ol>
<li>Precluded me from connecting it to our WiFi network</li>
<li>Encouraged me to consign the thing to a dark cupboard/possible future in the bin</li>
</ol>
<p>I suspect my reticence to have any form of “smart” device in our household is increasingly antiquated and quaint.</p>
</li>
<li>
<p>In a surprise to no one but myself, using my laptop on our sofa seems to have given me an exciting new repetitive strain injury. Sadly, wearing a neoprene splint, moving the computer to our 70s dining table and perching atop a pile of sofa cushions has not improved matters. In fact, I’m writing this week’s notes on my phone thanks to GitHub’s web interface though even this is making one of my hands feel suspiciously cold. Time for a bit of a break from musculoskeletal stress perhaps.</p>
</li>
</ul>
Weeknotes 11https://mudge.name/2020/01/14/weeknotes-11/2020-01-14T00:00:00+00:002020-01-14T00:00:00+00:00Redesigning the site, swimming lessons, first cousins once removed and screwdrivering.<ul>
<li>
<p>I redesigned this site in response to feedback from a former colleague that the old inline lists made it look like the styling had failed to load (never what you want to hear).</p>
<p>It doesn’t look too wildly different but I wanted it to resemble <a href="https://support.apple.com/en-gb/guide/iphone/iphdc30e3b86/ios">Safari’s Reader Mode</a> and look good on mobile devices so I’ve cranked both the font size and the contrast throughout.</p>
<p>I was most thrilled to replace the old <code class="language-plaintext highlighter-rouge">border</code>-based link underlines so <a href="https://css-tricks.com/styling-links-with-real-underlines/">modern browsers can render better ones that respect descenders</a>.</p>
<p>I’ve added an admittedly rather drab personal introduction to the <a href="/">home page</a> after seeing much better introductions on <a href="https://natbuckley.co.uk">Nat Buckley</a> and <a href="https://alicebartlett.co.uk">Alice Bartlett</a>’s sites. I might tweak it some more but <a href="https://en.wikipedia.org/wiki/Perfect_is_the_enemy_of_good">perfect is the enemy of the good</a>.</p>
</li>
<li>
<p>I felt rather sheepish after reading <a href="https://m.signalvnoise.com/the-last-tracker-was-just-removed-from-basecamp-com/">Basecamp removed Google Analytics</a> so I removed all analytics from this site too. To be honest, I never checked the results and it feels increasingly hypocritical to run a <a href="https://pi-hole.net">Pi-hole</a> at home and yet thoughtlessly track visitors on my own sites.</p>
<p>For similar reasons, I’ve also ditched <a href="https://disqus.com">Disqus</a> comments from all posts. I’d disabled it for new posts but it felt a good time to remove them altogether.</p>
</li>
<li>
<p>C had his very first swimming lesson this week. It consisted mostly of E walking up and down the shallow end of the swimming pool singing nursery rhymes while pumping his fat little legs but did involve submerging him completely under water twice. My own father (who was a competitive swimmer in his youth) is particularly excited to hear C is taking to the water as he recently revealed how disappointed he was that I never truly mastered the front crawl or butterfly. Keep those mildly distressing revelations coming, people!</p>
</li>
<li>
<p>I was overjoyed to see two people publish their first weeknotes: <a href="https://tomstu.art/weeknotes-0-collectively-meaningless">Tom Stuart</a> and my former colleague <a href="http://technicallyshane.com/2020/01/07/week1.html">Shane</a>. I continue my campaign to convince others to join in: perhaps <em>you’ll</em> be next?</p>
</li>
<li>
<p>We all went up to Glasgow over the weekend for E’s grandmother’s 94th birthday. This meant C met three <a href="/2019/12/16/weeknotes-7/">grand uncles</a> and three first cousins once removed (you could argue a second cousin was also present but is still in development). One of his grand uncles described C as someone who “perceives life as an adventure, not a threat” which I thought was rather lovely.</p>
<p>I can never remember how cousins work (first? second? how many times removed?) so I am very grateful for <a href="https://en.wikipedia.org/wiki/Cousin#/media/File:CousinTree.svg">this excellent family tree diagram from Wikipedia</a>.</p>
</li>
<li>
<p>On the train up to Scotland, I enjoyed <a href="https://adam-buxton.co.uk/podcasts/ep115-adam-joe">Adam Buxton’s podcast with Joe Cornish</a> where he mentioned Brian Eno’s concept of “screwdrivering”:</p>
<blockquote>
<p>When I had Brian Eno on the podcast he was saying the impulse is always to <em>add</em> when you’re making music or making anything creative. You always feel—he calls it “screwdrivering”—you always feel as if you should be putting more in and that’ll be good but actually what you should be doing is taking things out.</p>
</blockquote>
</li>
<li>
<p>Speaking of creative endeavours, a <a href="http://willhigo.com">good friend of mine</a> is currently making his first full-length feature film and I keep thinking about the sheer enormity of such a project. Sadly, I won’t be reprising my beloved character of <a href="https://vimeo.com/14098600">policeman in the background</a>.</p>
</li>
</ul>
Weeknotes 10https://mudge.name/2020/01/05/weeknotes-10/2020-01-05T00:00:00+00:002020-01-05T00:00:00+00:00Revisiting 2018’s yearnotes, sharing podcasts in the park, the rhythm of baby talk and why you wouldn’t be able to cut much with Apple’s scissors emoji.<ul>
<li>
<p>Happy New Year! And double digit weeknotes too!</p>
<p>It’s amazing to me that it has been over a year since I nervously posted my <a href="/2019/01/02/2018-yearnotes/">2018 Yearnotes</a> having just left a company after eight years. At the time, I was deeply conflicted about my career and didn’t really know what to do. As I wrote then:</p>
<blockquote>
<p>I’m honestly not sure what is next. After working continuously for a decade in London, I plan to take a break from keyboards and screens for a short while before seeing how I can best be of help to others, be that as a contractor or as a full-time employee.</p>
</blockquote>
<p>My “break from keyboards and screens” ended up being a month of not enjoying playing “<a href="https://www.rockstargames.com/reddeadredemption2/">Red Dead Redemption 2</a>” and then, after interviewing for a few full-time roles, landing my first contract as a freelance software developer through a friend of a friend. It was only for two weeks but it took a mere three days to convince me to commit and set up my own limited company, <a href="https://www.ghostcassette.com">Ghost Cassette</a>.</p>
<p>I was extremely fortunate and managed to work with two other companies before C’s turbulent arrival, getting the opportunity to work with technologies I had long wanted to use in production: e.g. <a href="https://www.terraform.io">HashiCorp’s Terraform</a> and <a href="https://reactjs.org">Facebook’s React</a>.</p>
<p>Conspicuously missing from my yearnotes is that we knew C was on his way but it was far too early to share with anyone. During E’s pregnancy, we referred to him as “Tujol” due to a vivid dream I had where I found myself in a hospital, watching <a href="/2019/12/16/weeknotes-7/">Turkish soap operas</a> with a young boy of that name dressed in corduroy and with a small black felt triangle covering the end of his nose. It would later prove extremely difficult to stop affectionately referring to him as “Tuj” but it now seems a distant memory.</p>
<p>Against all odds, I did <a href="/2019/11/04/weeknotes-1/">manage to run again</a> though.</p>
</li>
<li>
<p>On New Year’s Eve, E and I continued our tradition of asking each other to reflect on our highlights and lowlights of the past year (and there was no shortage of either) before thinking about what we’d like to change in 2020.</p>
<p>While I had a few ideas, a common theme in my resolutions was <a href="/2019/12/30/weeknotes-9/">my inability to do nothing</a> or, more specifically, how often I squander the present worrying about something else (e.g. DIY jobs to be done, being a better person, comparing insurance providers, etc).</p>
</li>
<li>
<p>I’m a little <a href="https://www.theguardian.com/tv-and-radio/2019/may/18/the-british-podcast-awards-review-george-the-poet">late to the party</a> but have been blown away by <a href="https://www.georgethepoet.com">George the Poet’s “Have You Heard George’s Podcast?”</a>.</p>
</li>
<li>
<p>Continuing my accidental <a href="/2019/12/16/weeknotes-7/">“things you can do with AirPods” column</a>, E and I listened to “<a href="https://www.bbc.co.uk/programmes/p07qtmfs">A Grenfell Story</a>” together while walking through the park thanks to <a href="https://support.apple.com/en-us/HT210421">iOS 13’s audio sharing</a> which I’m sure made us look extremely antisocial. (I also can’t help but be reminded of the <a href="https://www.newsweek.com/zune-should-go-beyond-squirting-107255">Microsoft Zune’s awfully-named “squirting”</a>.)</p>
</li>
<li>
<p>On E’s mum’s recommendation, we listened to the director of the <a href="https://www.cne.psychol.cam.ac.uk">Centre for Neuroscience in Education</a> at St. John’s College, Cambridge, <a href="https://www.bbc.co.uk/programmes/m000cc0w">Professor Usha Goswami discuss children’s language development on BBC Radio 4’s Woman’s Hour</a>:</p>
<blockquote>
<p>Rhythm is absolutely fundamental to the way the speech signal is structured. So when we’re talking, we’re creating a pressure wave in the air, it’s like a sound wave. It comes to the brain, the brain has its own intrinsic brain rhythms or brainwaves and effectively what happens is that, through automatic synchronisation processes, the brainwaves at different speeds align themselves with these energy changes in the speech signal that are also occurring at different speeds and that’s how you process the speech signal. It’s actually the most complex processing that your brain does, it’s more complex than vision.</p>
</blockquote>
<p>Her explanation of the importance of “baby talk” (characterised by the strong-weak rhythm of words such as “mummy”, “daddy”, “baby”, “doggy”, etc.) was fascinating.</p>
<p>As a result, we’re singing “<a href="/2019/12/30/weeknotes-9/">ye cannae shove your granny off a bus</a>” with even <em>more</em> gusto (if such a thing were possible) and I’m trying to break my habit of referring to C solely as “pud” (short for “pudding”, of course).</p>
</li>
<li>
<p>As with <a href="/2019/12/16/weeknotes-7/">my love of knot-related web sites</a>, I thoroughly enjoyed <a href="https://wh0.github.io/2020/01/02/scissors.html">a detailed analysis of which emoji scissors close</a>. I file it alongside a <a href="https://mobile.twitter.com/jelenawoehr/status/1191872816372600832?lang=e">Twitter thread rating every horse emoji</a> and <a href="https://curlicuecal.tumblr.com/post/175362924100/an-entomologist-rates-ant-emojis">an entomologist rating ant emojis</a>.</p>
</li>
</ul>
Weeknotes 9https://mudge.name/2019/12/30/weeknotes-9/2019-12-30T00:00:00+00:002019-12-30T00:00:00+00:00Learning to do nothing, a deep, mirk widd and exactly which grandparent you can shove off a bus.<ul>
<li>
<p>This past week of relative inactivity is probably best summed up by <a href="https://www.theguardian.com/lifeandstyle/2019/dec/27/learn-how-to-do-nothing">Sam Delaney’s “‘No tricks. No mantras. I just want to learn how to do nothing’: my quest to stay still”</a>:</p>
<blockquote>
<p>“Doing nothing won’t be comfortable at first, however you try,” he says. “You must deliberately put the time aside to do nothing, and have the discipline to get through it, even when it is boring or creates anxiety. You have to sweat it out and gnash your teeth doing nothing for a while, until you learn to cope. The only person you have to live with 24/7 is yourself, so you might as well learn how to do it.”</p>
</blockquote>
<p>Though I did manage to <a href="/2019/12/24/weeknotes-8/">let go of de-duplicating my sister’s photographs</a>.</p>
</li>
<li>
<p>I was struck by the poetry of <a href="https://www.theguardian.com/books/2019/dec/28/its-as-if-im-falling-from-a-50-storey-building-a-novelists-year-without-sleep">Samantha Harvey’s “‘It’s as if I’m falling from a 50-storey building’: a year without sleep”</a>:</p>
<blockquote>
<p>Why is it – she’s thinking – that she has to sit there day after day listening to patients who refuse to take responsibility for their own wellbeing? People in Syria can sleep with bombs falling, why can’t you sleep on your king-size mattress with your winter-togged duvet and your kelp-scented hair on a fake-down pillow under a bomb-free sky? What pea disrupts your sleep, princess? A passing Audi? What paucity and fragility of spirit has left you relying on drugs to do that which is the natural inheritance of all animals everywhere and for ever?</p>
</blockquote>
</li>
<li>
<p>My <a href="https://www.ghostcassette.com/function-composition-in-ruby/">guide to function composition in Ruby</a> made <a href="https://rubyweekly.com/issues/481">Ruby Inside’s “Top 6 Ruby Items of 2019”</a> which was an unexpected and very pleasant early Christmas present.</p>
</li>
<li>
<p>Speaking of gifts, we were very spoilt by others’ generosity including C receiving <a href="http://blackandwhitepublishing.com/the-gruffalo-in-scots.html">Julia Donaldson’s “The Gruffalo” translated into Scots by James Robertson</a> so he can really embrace his heritage:</p>
<blockquote>
<p>A moose took a dauner through the deep, mirk widd.<br />
A tod saw the moose and the moose looked guid.</p>
</blockquote>
</li>
<li>
<p>I received a brand new pair of shoes and what was the first thing I did with them? Why re-lace them with <a href="https://www.fieggen.com/shoelace/overunderlacing.htm">Ian’s favourite “Over Under” lacing</a>, of course!</p>
</li>
<li>
<p>Having had a whale of a time playing it with colleagues earlier in the year, I finally managed to introduce <a href="https://en.wikipedia.org/wiki/Codenames_(board_game)">Codenames</a> to another group of people—my in-laws—via <a href="https://www.horsepaste.com">a free online version</a> and tried to dazzle them with my extremely obtuse code-words (with mixed success).</p>
</li>
<li>
<p>There’s far too much Christmas television to cover but I was particularly pleased to see <a href="https://www.imdb.com/name/nm0188871/">Mackenzie Crook</a>’s “<a href="https://www.bbc.co.uk/programmes/m000csng">Worzel Gummidge</a>” <a href="https://www.theguardian.com/tv-and-radio/2019/dec/26/worzel-gummidge-review-mackenzie-crook-hes-back-hes-hilarious-and-hes-wearing-a-cravat">being well-received</a> after how much we enjoyed his last series, “<a href="https://www.bbc.co.uk/programmes/b06l51nr">Detectorists</a>”.</p>
</li>
<li>
<p>I managed to put on an astonishing amount of weight over the festive period so I’ve already started thinking about having some extremely predictable New Year’s Resolutions. The amount of junk food we’ve returned home with (Jaffa Cakes, After Eight Mints, chocolate Santa Clauses, Oreos, a slab of Christmas cake, mince pies, a tube of Smarties, two bags of chocolate coins, a box of Cadbury “Festive Friends”, etc.) only makes that all the more <em>thrilling</em>.</p>
</li>
<li>
<p>Finally, thanks to another gift for C, we’ve taken to regularly singing the Scottish classic “<a href="https://www.scotslanguage.com/articles/node/id/411">Ye Canny Shove Yer Grannie</a>” and he loves it. My mum (and therefore his ill-fated “daddy’s mammy” in the rhyme) is less keen.</p>
<blockquote>
<p>Ye cannae shove yer granny off a bus (Push push!)<br />
No, ye cannae shove yer granny off a bus (Push push!)<br />
No, ye cannae shove yer granny<br />
‘Cause she’s yer mammy’s mammy<br />
Ye cannae shove yer granny off a bus</p>
</blockquote>
</li>
</ul>
Weeknotes 8https://mudge.name/2019/12/24/weeknotes-8/2019-12-24T00:00:00+00:002019-12-24T00:00:00+00:00Being extraordinarily lazy, cobbling together a homemade banneton and this year’s obligatory bit of family busywork.<ul>
<li>
<p>My constant low-level consumption of gingerbread and “<a href="https://www.bbc.co.uk/programmes/b006v5kb">Homes Under the Hammer</a>” over the past seven days has made it feel like the most sedentary week in recent memory (and this is coming from someone who was living off freezer food with a newborn not too long ago). Despite my only exercise being moving between sofa and dining chair, it’s been remarkably difficult to find time to write this week’s notes so I’ll be brief!</p>
</li>
<li>
<p>We spent the week at my in-laws and I wanted to repay their hospitality by whipping up a loaf of bread or two (as one does). This was yet another adventure in breadmaking away from all my various paraphernalia so I had to cobble together something in which to prove the loaves. Thankfully, a couple of bowls lined with a tea towel in a plastic shopping bag did the trick though you really do need to thoroughly dust the towels with some sort of rice or semolina flour. If not, you’re in for the nerve-wracking and delicate task of gingerly teasing apart cloth and cold dough.</p>
</li>
<li>
<p>A home luxury I’ve been surprised to miss as I idly scroll through my phone on relatives’ WiFi is my <a href="https://pi-hole.net">Pi-hole</a>: the amount of internet advertising it has been dutifully hiding from me (particularly in iOS apps) is quite alarming.</p>
</li>
<li>
<p>We’re now at my family’s home and every year I fail to simply enjoy my time here as a guest and, instead, find some inane job with which to busy myself.</p>
<p>Previously, I’ve found myself precariously balancing on wooden beams in the attic, coordinating with my dad by barking questions down the hatch in order to painstaking identify and label various coaxial cables connected to the TV aerial.</p>
<p>This year, I have somehow found myself feeling responsible for ensuring all my sister’s photographs are stored in <a href="https://www.apple.com/uk/icloud/">iCloud Photo Library</a> and—crucially—are free of duplicates.</p>
<p>Presumably because she used to be an avid <a href="https://picasa.google.co.uk">Picasa</a> user, for some periods of time she has up to four copies of the exact same photo (sometimes in different sizes). I’m not sure how long this has been the case but I’ve been unable to let this rest and so, based on <a href="/2019/11/13/scripting-photos-for-macos-with-javascript/">my recent adventures scripting Photos for macOS</a>, I’ve been tinkering with cleaning them up automatically.</p>
<p>I’ve got as far as the following script but, in my heart of hearts, I know I should just let it be and move on with the holidays.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Photos</span> <span class="o">=</span> <span class="nx">Application</span><span class="p">(</span><span class="dl">"</span><span class="s2">Photos</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">duplicatesAlbum</span> <span class="o">=</span> <span class="nx">Photos</span><span class="p">.</span><span class="nx">albums</span><span class="p">().</span><span class="nx">byName</span><span class="p">(</span><span class="dl">"</span><span class="s2">Duplicates</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">seen</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">photo</span> <span class="k">of</span> <span class="nx">Photos</span><span class="p">.</span><span class="nx">mediaItems</span><span class="p">())</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">lat</span><span class="p">,</span> <span class="nx">lng</span><span class="p">]</span> <span class="o">=</span> <span class="nx">photo</span><span class="p">.</span><span class="nx">location</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">date</span> <span class="o">=</span> <span class="nx">photo</span><span class="p">.</span><span class="nx">date</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">description</span> <span class="o">=</span> <span class="nx">photo</span><span class="p">.</span><span class="nx">description</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">fingerprint</span> <span class="o">=</span> <span class="s2">`date:</span><span class="p">${</span><span class="nx">date</span><span class="p">}</span><span class="s2"> lat:</span><span class="p">${</span><span class="nx">lat</span><span class="p">}</span><span class="s2"> lng:</span><span class="p">${</span><span class="nx">lng</span><span class="p">}</span><span class="s2"> desc:</span><span class="p">${</span><span class="nx">description</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">lat</span> <span class="o">||</span> <span class="o">!</span><span class="nx">lng</span> <span class="o">||</span> <span class="o">!</span><span class="nx">date</span><span class="p">)</span> <span class="p">{</span>
<span class="k">continue</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">seen</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">fingerprint</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">seen</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">fingerprint</span><span class="p">).</span><span class="nx">push</span><span class="p">(</span><span class="nx">photo</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">seen</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">fingerprint</span><span class="p">,</span> <span class="p">[</span><span class="nx">photo</span><span class="p">]);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">duplicates</span> <span class="k">of</span> <span class="nx">seen</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">biggest</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span><span class="p">]</span> <span class="o">=</span> <span class="nx">duplicates</span><span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="nx">b</span><span class="p">.</span><span class="nx">width</span><span class="p">()</span> <span class="o">-</span> <span class="nx">a</span><span class="p">.</span><span class="nx">width</span><span class="p">());</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">rest</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">Photos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">rest</span><span class="p">,</span> <span class="p">{</span><span class="na">to</span><span class="p">:</span> <span class="nx">duplicatesAlbum</span><span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div> </div>
<p>I’m hoping that tomorrow’s traditional morning sherry will render me blissfully free of caring any more about this particular bit of busywork.</p>
</li>
<li>
<p>We finally managed to watch “<a href="https://www.imdb.com/title/tt4154796/">Avengers: Endgame</a>” but it took around six sittings and a lot of rewinding. It turns out 3 hour 1 minute action epics are trickier to enjoy with a three-month old.</p>
</li>
</ul>
Weeknotes 7https://mudge.name/2019/12/16/weeknotes-7/2019-12-16T00:00:00+00:002019-12-16T00:00:00+00:00Great-grand relatives, the joy of tying knots and Turkish soap operas.<ul>
<li>
<p>We’re in Yorkshire spending time with our family in the run up to Christmas so please forgive the lateness of this week’s notes.</p>
</li>
<li>
<p>I was very excited to consult “<a href="https://www.animatedknots.com">Animated Knots by Grog</a>” to fix a new loop of string onto a duster (a lovely <a href="https://www.animatedknots.com/bowline-knot">bowline knot</a> did the trick). I’d be remiss if I didn’t take this opportunity to mention my favourite web site: “<a href="https://www.fieggen.com/shoelace/">Ian’s Shoelace Site</a>”. It embodies exactly what I love about the internet: a true labour of love that is narrow in scope but <em>extremely</em> deep. I am yet to convert E (much to my annoyance as her laces come undone when we’re out walking) but I never leave home without tying a couple of trusty <a href="https://www.fieggen.com/shoelace/secureknot.htm">Ian’s secure shoelace knots</a>.</p>
</li>
<li>
<p>Travelling north meant our very first significant road trip with C so we kitted out the car with a backseat baby mirror and tentatively set off up the M1. While we were nervous about C getting upset along the way, I got to indulge in one of my great, guilty pleasures in life: spending time in motorway services. I don’t just mean fancy, peaceful ones like <a href="https://www.tebayservices.com">Tebay</a> but ones with appalling men’s health warnings on toilet stall doors presumably targetted at long haul drivers. I’m not sure what it is about them but I find them curiously relaxing places to be; there’s something about their transitory, incidental nature that is strangely freeing.</p>
</li>
<li>
<p>C met his great-grandfather and great-grand-aunt for the very first time who are 91 and 95 years his senior respectively (the latter worked at the <a href="https://en.wikipedia.org/wiki/Avro#History">Avro “shadow” factory in Leeds during the Second World War</a> building <a href="https://en.wikipedia.org/wiki/Avro_Lancaster">Lancaster bombers</a>). I’m mostly sharing this to use the term <a href="https://blogs.ancestry.com/ancestry/2013/10/25/great-and-grand-aunts/">grand-aunt instead of great-aunt</a>.</p>
</li>
<li>
<p>As we’re away from home in houses that actually have more than three rooms, we’ve been putting C down to sleep farther from us than ever before. We don’t have any sort of baby monitor but I’ve been using <a href="https://support.apple.com/en-us/HT209082">Live Listen with AirPods</a> to turn my phone into a microphone and listen in. It has the added benefit of meaning I get to sit with a single earpiece in like some sort of cool dude.</p>
</li>
<li>
<p>Continuing my streak of recommending <a href="https://www.theguardian.com/profile/felicity-cloake">Felicity Cloake</a> recipes in the past <a href="/2019/12/08/weeknotes-6/">few</a> <a href="/2019/11/24/weeknotes-4/">weeks</a>: we made a large batch of her <a href="https://www.theguardian.com/food/2019/dec/04/felicity-cloakes-mince-pie-masterclass-an-easy-step-by-step-recipe">perfect mince pies</a>. To be honest, I’m just excited whenever we get an excuse to use our increasingly dusty bottle of orange blossom water.</p>
</li>
<li>
<p>I tried to make a loaf of bread with my sourdough starter (“Colin”) at my mum’s house but got a cracking migraine early in the process which meant a 40 minute <a href="https://www.bakerybits.co.uk/resources/autolyse-what-why-how/">autolyse</a> unexpectedly became a <em>24 hour</em> autolyse. Seeing as I had already used the ingredients, I decided to go ahead with the rest of the recipe and, while the dough was extremely difficult to handle, I was pleasantly surprised to end up with a decent loaf!</p>
<p>I’m increasingly inclined to agree with <a href="https://interblah.net">James Adam</a>:</p>
<blockquote>
<p>I bet if you just chuck all the ingredients in a bowl, stir it once, put it in an oven whose thermostat is triggered on and off by radioactive decay, you’ll get a normal loaf at the end of the process.</p>
</blockquote>
</li>
<li>
<p>Spending time with my dad means catching glimpses of the two hour long miniature epics that are <a href="https://www.theguardian.com/tv-and-radio/2019/sep/13/turkish-tv-magnificent-century-dizi-taking-over-world">Turkish soap operas, or “dizi”</a>: I never watch enough to understand the plot but you can guarantee there’ll be some smouldering looks and some fantastic facial hair.</p>
</li>
</ul>
Weeknotes 6https://mudge.name/2019/12/08/weeknotes-6/2019-12-08T00:00:00+00:002019-12-08T00:00:00+00:00Christmas films, participating in democracy and things a true Englishman will never countenance.<ul>
<li>
<p>Following <a href="/2019/12/01/weeknotes-5/">last week</a>’s beginnings of a cold for the newest member of our family, both E and I also succumbed, all three of us sniffling and coughing though none more pitifully than C. I took to using a large muslin square as a handkerchief which gave E great amusement every time I would unfurl it from a pocket while she relied on the restorative power of <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2013/jan/02/how-to-make-the-perfect-hot-toddy">Felicity Cloake’s perfect hot toddies</a>.</p>
</li>
<li>
<p>With little desire to leave the house while recuperating, we found ourselves drawn to the neverending stream of made-for-TV Christmas films shown during the day on <a href="https://www.channel5.com">Channel 5</a>. An early highlight was 2017’s “<a href="https://www.imdb.com/title/tt7030432/">The Sweetest Christmas</a>”, which featured the following classic line:</p>
<blockquote>
<p>I have to tell him how I feel the best way I know how: <em>with gingerbread</em>.</p>
</blockquote>
<p>A bonus mention has to go out to 2017’s “<a href="https://www.imdb.com/title/tt7642558/">Four Christmases and a Wedding</a>” for its title.</p>
<p>All of this reminded me of <a href="https://maximumfun.org/episodes/who-shot-ya/who-shot-ya-episode-68-christmas-extravaganza-love-actually-white-elephant-and-dan/">Alonso Duralde’s “El Mero Mero De Navidad Christmas Minute” on “Who Shot Ya?”</a> where the film critic would review as many <a href="https://www.hallmarkchannel.com/christmas">Hallmark Channel Christmas movies</a> as possible in a minute.</p>
</li>
<li>
<p>E baked a large batch of <a href="https://www.theguardian.com/food/2019/nov/30/sweet-christmas-gift-recipes-helen-goh-hazelnut-brittle-rugelach-truffles-gingerbread">Helen Goh’s black treacle gingerbread</a> with the treacle left over from <a href="/2019/11/24/weeknotes-4/">our Christmas pudding adventures</a> and, what’s more, even managed to use up <a href="/2019/11/10/weeknotes-2/">the blight on our household that is greaseproof paper</a> to wrap them up as gifts. The combination of large quantities of biscuits and a daily ritual of tea in the afternoon is proving disastrous for my waistline.</p>
</li>
<li>
<p>We <a href="https://www.gov.uk/government/publications/apply-for-a-postal-vote">voted by post</a> for the very first time which I found rather exciting having only ever been to polling stations in the past. I was particularly intrigued by the design problem of explaining exactly how to fill out a postal ballot; not least because it involves two envelopes and <em>not</em> tearing off a slip of paper. Perforated paper seems like a classic <a href="https://en.wikipedia.org/wiki/Affordance#As_perceived_action_possibilities">affordance</a> and the temptation to tear it, even having read the instructions multiple times, was huge.</p>
</li>
<li>
<p>I’ve been using <a href="https://support.apple.com/en-us/HT204380">FaceTime</a> more and more so that distant family members can see C and have been startled when calls have been answered with unfamiliar faces present. It’s not quite <a href="https://kottke.org/10/06/david-foster-wallace-on-iphone-4s-facetime">David Foster Wallace’s predictions about video telephony from “Infinite Jest”</a> but there’s something odd when you expect to see a loved one at the other end of a call and instead find yourself suddenly showcasing your child for a stranger.</p>
</li>
<li>
<p>Continuing the theme of things for which I lack mental preparation: we visited two nurseries for C as there are waiting lists of up to a year in our area. I was a little caught in the headlights as we were given tours and heard about the various facilities on offer (even being reassured that any information technology used in the nursery was divorced from social media).</p>
</li>
<li>
<p>Finally, our year-long nightmare of not opening a tiny cardboard door every day is over: it’s advent calendar season!</p>
<p>A former colleague of mine really made me question what is sacred when he took up happily consuming entire advent calendars as a snack in the run up to Christmas a few years ago. As I pleaded with him that such a thing was <em>just not done</em>, I realised that I had become a character in <a href="https://youtu.be/7lpN-3-SaHs">a Mitchell and Webb Look sketch about starving Antarctic explorers</a>:</p>
<blockquote>
<p>Lieutenant, there are some things that a true Englishman will never countenance. That is a Christmas pudding and we are saving it for Christmas, is that understood?</p>
</blockquote>
</li>
</ul>
Weeknotes 5https://mudge.name/2019/12/01/weeknotes-5/2019-12-01T00:00:00+00:002019-12-01T00:00:00+00:00The unexpected joy of baby cinema, doing everything wrong and our daily multiplayer crossword routine.<ul>
<li>
<p>For a few years now, cookery shows on TV have been a guilty pleasure of mine: when your day has been a little too trying for the latest nail-biting thriller or existential drama, I find there’s no better way to unwind than to watch <a href="https://www.jamieoliver.com" title="Jamie Oliver">Jamie</a> “glug” olive oil or “click off” lettuce leaves. This week I indulged in <a href="https://www.bbc.co.uk/programmes/m000b1qp">Rick Stein’s new series “Secret France”</a> which was <a href="https://www.theguardian.com/tv-and-radio/2019/nov/23/rick-stein-secret-france">expertly summed up by Joel Golby in his review for The Guardian</a>:</p>
<blockquote>
<p>Every episode of every TV show Rick Stein has ever made is essentially like watching your dad have a particularly joyful Christmas Day, rubbing his hands with glee over a long-aged sherry, sitting upright to eat a large plate of continental sausages, regaling you with tales of childhoods gone by as soon as someone rolls a braised rabbit out in front of him, saying he’s too full for pudding but eating it anyway.</p>
</blockquote>
</li>
<li>
<p>Having waited 12 years since <a href="https://www.half-life.com/en/episode2" title="Half-Life 2: Episode Two">the last episode</a>, watching <a href="https://youtu.be/O2W0N3uKXmo">the announcement trailer</a> for “<a href="https://www.half-life.com/en/alyx">Half-Life: Alyx</a>” was a strange experience having reached a kind of heavy-hearted acceptance I would never step foot in City 17 again.</p>
<p>I last built a PC 15 years ago, specifically to play “<a href="https://www.half-life.com/en/halflife2">Half-Life 2</a>” and it is tempting to repeat that effort to meet <a href="https://www.half-life.com/en/alyx/vr">the new game’s lofty VR requirements</a>. That said, the <a href="https://store.steampowered.com/valveindex">Valve Index VR Kit</a> alone is over £900 so I might just have to rely on the kindness of any VR early adopters I know.</p>
</li>
<li>
<p>I reluctantly attended my very first <a href="https://www.barbican.org.uk/whats-on/series/parent-and-baby-screenings">Parent and Baby Screening at the Barbican</a> (or, as we like to call it, “baby cinema”) to see <a href="https://www.netflix.com/gb/title/80223779">Noah Baumbach’s “Marriage Story”</a>. I was mostly dreading trying to keep C happy and quiet throughout the film’s two and a quarter hours of running time but, with E’s help, we actually got to enjoy it. It’s not exactly <em>light</em> viewing for a married couple who have just had a child together but it’s well worth it.</p>
<p>Before the film started, I took C for a tactical change and was unexpectedly grilled by a member of staff when trying to re-enter the screen. I was slightly incredulous as I stood there coatless, holding C in my arms as I recited my row and seat number, imagining someone going to great lengths to smuggle their baby into a screening but then I thought of <a href="https://fs.blog/2012/04/david-foster-wallace-this-is-water/">David Foster Wallace’s “This Is Water”</a>.</p>
<blockquote>
<p>The point is that petty, frustrating crap like this is exactly where the work of choosing is gonna come in. Because the traffic jams and crowded aisles and long checkout lines give me time to think, and if I don’t make a conscious decision about how to think and what to pay attention to, I’m gonna be pissed and miserable every time I have to shop. Because my natural default setting is the certainty that situations like this are really all about me. About <em>my</em> hungriness and <em>my</em> fatigue and <em>my</em> desire to just get home, and it’s going to seem for all the world like everybody else is just in my way. And who are all these people in my way? And look at how repulsive most of them are, and how stupid and cow-like and dead-eyed and nonhuman they seem in the checkout line, or at how annoying and rude it is that people are talking loudly on cell phones in the middle of the line. And look at how deeply and personally unfair this is.</p>
</blockquote>
<p>As Wallace warned, it’s difficult to catch yourself in a moment where you feel personally inconvenienced but it’s good to consider—if only for a second—maybe this isn’t all about <em>you</em>.</p>
</li>
<li>
<p>Having said that, it is spectacularly annoying how many different, inconsistent screens one has to find the opt-out link on (no, I don’t want an Amazon Prime trial; no, I don’t want to try Kindle Unlimited) when resetting an <a href="https://www.amazon.com/kindle">Amazon Kindle</a> these days.</p>
</li>
<li>
<p>In this week’s mostly pointless computer noodling corner: I was keen to get my <a href="https://www.zsh.org">zsh</a> startup time down as much as possible and so used the “faster way to measure the total zsh startup time” from <a href="https://esham.io/2018/02/zsh-profiling">Benjamin Esham’s “How to profile your zsh startup time”</a> to gradually whittle it down under 30 milliseconds.</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nb">time </span>zsh <span class="nt">-i</span> <span class="nt">-c</span> <span class="nb">echo</span>
<span class="go">zsh -i -c echo 0.01s user 0.01s system 89% cpu 0.028 total
</span></code></pre></div> </div>
<p>You can find <a href="https://github.com/mudge/dotfiles/blob/master/.zshrc">my full <code class="language-plaintext highlighter-rouge">.zshrc</code> for macOS Catalina on GitHub</a> but the main trick is to avoid spawning subprocesses where possible. For me, this meant switching my prompt to use <a href="http://zsh.sourceforge.net/Doc/Release/User-Contributions.html#Version-Control-Information">the <code class="language-plaintext highlighter-rouge">vcs_info</code> function</a> for showing git branch names and setting up my own alias for <a href="https://hub.github.com"><code class="language-plaintext highlighter-rouge">hub</code></a> rather than using <a href="https://hub.github.com/hub-alias.1.html"><code class="language-plaintext highlighter-rouge">hub alias -s</code></a>.</p>
<p>(I told you it was mostly pointless.)</p>
</li>
<li>
<p>As it is Christmas shopping season, I was roped in to do some technical support as a family member was struggling with an online retailer’s checkout. Fortunately, after listening to <a href="https://atp.fm/episodes/354">John Siracusa describe his latest bout of family tech support on Accidental Tech Podcast</a>, I <a href="https://support.apple.com/en-gb/guide/messages/icht11883/mac">used Messages on macOS’ little known “Ask to Share Screen” functionality</a> to guide them through the process (and subsequently got angry that an email address ending in <a href="https://en.wikipedia.org/wiki/.name"><code class="language-plaintext highlighter-rouge">.name</code></a> was considered invalid 18 years after the domain was first introduced).</p>
</li>
<li>
<p>A member of our <a href="https://www.nct.org.uk">NCT group</a> told us about <a href="https://www.bbc.co.uk/programmes/b09tf362">an excellent episode of Adam Buxton’s BBC Radio 4 show “You’re Doing It Wrong” about parenting</a>:</p>
<blockquote>
<p>In this programme, we’re taking a look at parenting and proving that no matter how hard you try, you’re probably going to screw it up and raise a psychopath who hates you.</p>
</blockquote>
<p>This quickly led to us listening to <a href="https://www.bbc.co.uk/programmes/b09v3gwk">all other episodes</a> covering topics such as <a href="https://www.bbc.co.uk/programmes/b09snrb7">work</a>, <a href="https://www.bbc.co.uk/programmes/b09v6vt6">diet</a> and <a href="https://www.bbc.co.uk/programmes/b09w0vhz">the environment</a>.</p>
</li>
<li>
<p>I started reading <a href="https://en.wikipedia.org/wiki/The_Three-Body_Problem_(novel)">Cixin Liu’s award-winning “The Three-Body Problem”</a> and was immediately struck by how much of what I read is based in USA or Western Europe and what a refreshing change it is to read something set elsewhere (in this case, China).</p>
</li>
<li>
<p>I finally made a batch of <a href="https://www.bonappetit.com/recipe/bas-best-chocolate-chip-cookies">Bon Appétit’s Best Chocolate Chip Cookies</a> following <a href="https://chriszetter.com">Chris Zetter</a>’s recommendation. I was warned they would spread when baking but I was still taken aback by <a href="/i/cookies.jpg">how much</a>! Luckily, they are delicious and even better the next day when they’ve firmed up a bit (eating them shortly after baking was basically like eating warm caramel-flavoured melted chocolate).</p>
</li>
<li>
<p>Speaking of Chris, we have now formed a daily bedtime routine of using “<a href="https://multicrosser.chriszetter.com">Several People are Solving</a>” to do <a href="https://www.theguardian.com/crosswords/series/quick">The Guardian quick crossword</a> together. <a href="https://chriszetter.com/blog/2018/12/02/multiplayer-crosswords/">Chris wrote up how he made it in “Multiplayer Crosswords”</a> and I highly recommend giving it a go with a fellow crossword enthusiast.</p>
</li>
<li>
<p>C has his very first cold so I’ll be spending today mostly worrying about him and audibly aching at every pitiful sneeze and cough.</p>
</li>
</ul>
Weeknotes 4https://mudge.name/2019/11/24/weeknotes-4/2019-11-24T00:00:00+00:002019-11-24T00:00:00+00:00Struggling with structured data, Christmas puddings, the joy of repairing things and everything being a trade-off.<ul>
<li>
<p>I read <a href="https://asthasr.github.io/posts/danger-of-simplicity/">Blake Hyde’s “The Danger of ‘Simplicity’”</a> this week and wished I had written it. I’ve wanted to write about everything being a trade-off for years (I even have a draft from 2014 but sadly it only contains a bulleted list of technology buzzwords) but this is a fantastic, concise piece on the difficulty of pursuing simplicity in software.</p>
<blockquote>
<p>In other words: every decision made in order to simplify a program will cost something.</p>
</blockquote>
</li>
<li>
<p>I also enjoyed <a href="https://getkiss.org/blog/20191004a">Dylan Araps’ “I’ve gone to great lengths for this silence”</a> as relentlessly optimising web page markup is also a bit of a guilty pleasure of mine. Dylan goes much further than I have but I thoroughly appreciated their efforts.</p>
</li>
<li>
<p>On the topic of markup, I did <em>not</em> have fun trying to switch to using <a href="https://developers.google.com/search/docs/guides/sd-policies">Google’s recommended structured data</a> (based on <a href="https://json-ld.org">JSON-LD</a>) for this site. The <a href="https://search.google.com/structured-data/testing-tool">Google Structured Data Testing Tool</a> does not approve of these blog posts (marked up as <a href="https://schema.org/BlogPosting"><code class="language-plaintext highlighter-rouge">BlogPosting</code></a>s) not having a <a href="https://schema.org/publisher"><code class="language-plaintext highlighter-rouge">publisher</code></a> that is an <a href="https://schema.org/Organization"><code class="language-plaintext highlighter-rouge">Organization</code></a> with a mandatory <a href="https://schema.org/logo"><code class="language-plaintext highlighter-rouge">logo</code></a>. Frustrated, I added only the markup that seemed to make sense but I risk the ire of Googlebot.</p>
</li>
<li>
<p>E made our very first batch of Christmas puddings using <a href="https://www.theguardian.com/lifeandstyle/wordofmouth/2016/nov/17/how-to-cook-the-perfect-christmas-pudding">Felicity Cloake’s “perfect” recipe</a> (prefixing search terms with “Felicity Cloake perfect” is the first thing I do before cooking or baking <em>anything</em>) and we each took it in turns (including C) to stir the mixture. Unexpected bonus baked good: we used up the rest of the recipe’s Guinness in <a href="https://www.nigella.com/recipes/chocolate-guinness-cake">Nigella Lawson’s Chocolate Guinness Cake</a> and let’s just say that our daily regimen of tea and cake in the afternoon isn’t doing wonders for my weight. I’d be remiss if I didn’t also mention my former colleague <a href="https://github.com/mattmacleod/Recipes/blob/master/Desserts%20and%20puddings/Christmas%20pudding.md">Matt MacLeod’s Christmas pudding recipe</a> which features both <a href="https://www.agbarr.co.uk/our-brands/irn-bru/">IRN-BRU</a> and <a href="https://www.buckfast.com/">Buckfast</a> and is always in great demand.</p>
</li>
<li>
<p>There’s a new series of <a href="https://www.bbc.co.uk/programmes/b08l581p">BBC’s “The Repair Shop”</a> and it truly warms my heart. Having already been made a fan of <a href="https://www.youtube.com/channel/UCMrMVIBtqFW6O0-MWq26gqw">mechanical restoration videos</a> following <a href="https://twitter.com/urbanautomaton/status/1087089421424513025">Simon Coffey’s recommendation</a>, this takes the calming effect of watching a true expert methodically repair something and combines it with heartwrenching human emotion. E still teases me about being in floods of tears hearing <a href="https://www.bbc.co.uk/programmes/m0004ppp">the tale of “wheelie ted”</a>.</p>
</li>
<li>
<p>We began watching <a href="https://tv.apple.com/us/show/the-morning-show/umc.cmc.25tn3v8ku4b39tr6ccgb8nl6m">Apple TV+’s “The Morning Show”</a> as E has a free trial for a year and I was overjoyed to be reminded of the existence of <a href="https://www.benjaminclementine.com">Benjamin Clementine’s wonderful music</a> in <a href="https://open.spotify.com/track/7aNwPr0nR3zocELBBe0Xzv?si=oNhmBpTpRLqbYEuB7aCb3Q">its theme song, “Nemesis”</a>.</p>
</li>
<li>
<p>We’ve taken to singing nursery rhymes to C but I’m having to make alterations to <a href="https://open.spotify.com/track/0EXodPnj0bgFFUg0o80s5S?si=PitqtcxiSWqkzyOn8du0TA">the version of “The Wheels on the Bus” we found on Spotify</a> as we’re not sure “the mummies on the bus go ‘chatter, chatter, chatter’” and “the daddies on the bus go ‘nod, nod, nod’” is a great depiction of our family life.</p>
</li>
<li>
<p>I’m not exercising great restraint in sharing pictures, videos and stories of C with friends (have I told you he’s taking to shouting “goo”?) but a poignant moment in <a href="https://www.bbc.co.uk/iplayer/episode/m00077cb/mortimer-whitehouse-gone-fishing-series-2-episode-1">a recent episode of BBC’s “Mortimer & Whitehouse: Gone Fishing”</a> as Bob talked about the loss of his mother gave me pause:</p>
<blockquote>
<p>When my mum died, so I had zero parents, do you know the thing I really miss? And I miss it nearly every Sunday, because I used to phone her every Sunday, so it always passes through my mind even now on Sundays is… It was lovely having someone to tell your kids’ little achievements. Someone who <em>really</em> was interested. No-one else cares less, do they, really? But you could phone up your mum and she loved hearing what they’d said or what they’d done. Now I’ve got no-one to tell, you know?</p>
</blockquote>
</li>
</ul>
Weeknotes 3https://mudge.name/2019/11/16/weeknotes-3/2019-11-16T00:00:00+00:002019-11-16T00:00:00+00:00Writing, solving logic puzzles and the Bake Off we deserve.<ul>
<li>
<p>While we initially recoiled at the adverts for the <a href="https://www.channel4.com/programmes/junior-bake-off">Junior Bake Off</a>, after reading that <a href="https://www.theguardian.com/tv-and-radio/2019/nov/11/junior-bake-off-the-kids-spinoff-thats-the-perfect-antidote-to-baking-brutality">it is “the Bake Off we deserve” after this year’s disappointing series</a>, we gave it a go and are now fully converted. Watching the contestants recover from baking disasters and help each other without hesitation is truly joyful in a way the regular series totally failed to be.</p>
</li>
<li>
<p>After my <a href="/2019/11/10/weeknotes-2.html">computer noodling last week</a>, I decided to write up in detail how to <a href="/2019/11/12/using-a-raspberry-pi-for-time-machine/">use a Raspberry Pi for Time Machine</a> and how to <a href="/2019/11/13/scripting-photos-for-macos-with-javascript/">script Photos for macOS with JavaScript</a>. It was good to write something in-depth again, especially for the first post as I wanted to ensure every step was both necessary and justified which was especially tricky given <a href="/2019/11/12/using-a-raspberry-pi-for-time-machine/#configuring-avahi">the rather opaque configuration necessary for Avahi</a>.</p>
</li>
<li>
<p>Long-time host of the <a href="http://lrug.org">London Ruby User Group</a> <a href="https://www.linkedin.com/pulse/skills-matter-appointed-administrators-wendy-devolder/">Skills Matter recently went into administration</a> and their recordings of previous talks have sadly been disappearing from the internet. Among them, the video of my March 2015 talk <a href="/2014/11/26/data-structures-as-functions.html">“Exploring <code class="language-plaintext highlighter-rouge">#to_proc</code>”</a> seems to have been been removed. Another day, another reason to think about the 21-year old <a href="https://www.w3.org/Provider/Style/URI">“Cool URIs don’t change”</a>.</p>
</li>
<li>
<p>Having said that, I changed the <a href="https://jekyllrb.com/docs/permalinks/#built-in-formats">format of permalinks</a> on this site to drop the <code class="language-plaintext highlighter-rouge">.html</code> extension from the URLs. Thankfully, there’s a <a href="https://github.com/jekyll/jekyll-redirect-from">redirection plugin for Jekyll</a> to keep old links working.</p>
</li>
<li>
<p><a href="https://codon.com">Tom Stuart</a> shared the following <a href="https://math.stackexchange.com/questions/2217248/which-answer-in-this-list-is-the-correct-answer-to-this-question">logic puzzle</a> on the <a href="https://computationclub-slack.herokuapp.com/">London Computation Club Slack</a>:</p>
<blockquote>
<p>Which answer in this list is the correct answer to this question?</p>
<ol>
<li>All of the below.</li>
<li>None of the below.</li>
<li>All of the above.</li>
<li>One of the above.</li>
<li>None of the above.</li>
<li>None of the above.</li>
</ol>
</blockquote>
<p>I thought this would be an excellent opportunity to use <a href="https://sentient-lang.org">Chris Patuzzo’s Sentient programming language</a> to solve it:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">,</span> <span class="n">answer6</span><span class="p">;</span>
<span class="c1"># 1. All of the below.</span>
<span class="n">invariant</span> <span class="n">answer1</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">,</span> <span class="n">answer6</span><span class="p">].</span><span class="nf">all?</span><span class="p">(</span><span class="o">*</span><span class="nb">self</span><span class="p">);</span>
<span class="c1"># 2. None of the below.</span>
<span class="n">invariant</span> <span class="n">answer2</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">,</span> <span class="n">answer6</span><span class="p">].</span><span class="nf">none?</span><span class="p">(</span><span class="o">*</span><span class="nb">self</span><span class="p">);</span>
<span class="c1"># 3. All of the above.</span>
<span class="n">invariant</span> <span class="n">answer3</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">].</span><span class="nf">all?</span><span class="p">(</span><span class="o">*</span><span class="nb">self</span><span class="p">);</span>
<span class="c1"># 4. One of the above.</span>
<span class="n">invariant</span> <span class="n">answer4</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">].</span><span class="nf">one?</span><span class="p">(</span><span class="o">*</span><span class="nb">self</span><span class="p">);</span>
<span class="c1"># 5. None of the above.</span>
<span class="n">invariant</span> <span class="n">answer5</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">].</span><span class="nf">none?</span><span class="p">(</span><span class="o">*</span><span class="nb">self</span><span class="p">);</span>
<span class="c1"># 6. None of the above.</span>
<span class="n">invariant</span> <span class="n">answer6</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">].</span><span class="nf">none?</span><span class="p">(</span><span class="o">*</span><span class="nb">self</span><span class="p">);</span>
<span class="n">expose</span> <span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">,</span> <span class="n">answer6</span><span class="p">;</span>
</code></pre></div> </div>
<p>Which returned a single answer: number 5!</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>sentient <span class="nt">--number</span> 0 answers.snt
<span class="go">{"answer1":false,"answer2":false,"answer3":false,"answer4":false,"answer5":true,"answer6":false}
{}
</span></code></pre></div> </div>
<p>The story of Sentient is a great one and <a href="https://whyarecomputers.com/4">is told excellently by Chris and Tom in episode 4 of “Why Are Computers”</a>.</p>
</li>
<li>
<p>A blog post circulated this week <a href="https://thesmartnik.com/solving-pazzles-wth-amb.html">about solving puzzles with Amb</a> so I thought I’d try using <a href="https://github.com/chikamichi/amb">an ambiguous function/operator implementation in Ruby</a> to solve the logic puzzle as well:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'amb'</span>
<span class="no">A</span> <span class="o">=</span> <span class="no">Class</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="kp">include</span> <span class="no">Amb</span> <span class="p">}.</span><span class="nf">new</span>
<span class="n">answer1</span> <span class="o">=</span> <span class="no">A</span><span class="p">.</span><span class="nf">choose</span><span class="p">(</span><span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="n">answer2</span> <span class="o">=</span> <span class="no">A</span><span class="p">.</span><span class="nf">choose</span><span class="p">(</span><span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="n">answer3</span> <span class="o">=</span> <span class="no">A</span><span class="p">.</span><span class="nf">choose</span><span class="p">(</span><span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="n">answer4</span> <span class="o">=</span> <span class="no">A</span><span class="p">.</span><span class="nf">choose</span><span class="p">(</span><span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="n">answer5</span> <span class="o">=</span> <span class="no">A</span><span class="p">.</span><span class="nf">choose</span><span class="p">(</span><span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="n">answer6</span> <span class="o">=</span> <span class="no">A</span><span class="p">.</span><span class="nf">choose</span><span class="p">(</span><span class="kp">true</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>
<span class="c1"># 1. All of the below.</span>
<span class="no">A</span><span class="p">.</span><span class="nf">assert</span> <span class="n">answer1</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">,</span> <span class="n">answer6</span><span class="p">].</span><span class="nf">all?</span>
<span class="c1"># 2. None of the below.</span>
<span class="no">A</span><span class="p">.</span><span class="nf">assert</span> <span class="n">answer2</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">,</span> <span class="n">answer6</span><span class="p">].</span><span class="nf">none?</span>
<span class="c1"># 3. All of the above.</span>
<span class="no">A</span><span class="p">.</span><span class="nf">assert</span> <span class="n">answer3</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">].</span><span class="nf">all?</span>
<span class="c1"># 4. One of the above.</span>
<span class="no">A</span><span class="p">.</span><span class="nf">assert</span> <span class="n">answer4</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">].</span><span class="nf">one?</span>
<span class="c1"># 5. None of the above.</span>
<span class="no">A</span><span class="p">.</span><span class="nf">assert</span> <span class="n">answer5</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">].</span><span class="nf">none?</span>
<span class="c1"># 6. None of the above.</span>
<span class="no">A</span><span class="p">.</span><span class="nf">assert</span> <span class="n">answer6</span> <span class="o">==</span> <span class="p">[</span><span class="n">answer1</span><span class="p">,</span> <span class="n">answer2</span><span class="p">,</span> <span class="n">answer3</span><span class="p">,</span> <span class="n">answer4</span><span class="p">,</span> <span class="n">answer5</span><span class="p">].</span><span class="nf">none?</span>
<span class="nb">puts</span> <span class="s2">"Answer 1: </span><span class="si">#{</span><span class="n">answer1</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"Answer 2: </span><span class="si">#{</span><span class="n">answer2</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"Answer 3: </span><span class="si">#{</span><span class="n">answer3</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"Answer 4: </span><span class="si">#{</span><span class="n">answer4</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"Answer 5: </span><span class="si">#{</span><span class="n">answer5</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">"Answer 6: </span><span class="si">#{</span><span class="n">answer6</span><span class="si">}</span><span class="s2">"</span>
</code></pre></div> </div>
<p>And it returned the same answer!</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>ruby answers.rb
<span class="go">Answer 1: false
Answer 2: false
Answer 3: false
Answer 4: false
Answer 5: true
Answer 6: false
</span></code></pre></div> </div>
</li>
<li>
<p>A friend bought C a lovely <a href="https://uk.tomy.com/products/lamaze-freddie-firefly">toy firefly</a> which constantly reminds me of the butterfly from <a href="https://youtu.be/sXOdn6vLCuU">Don’t Hug Me I’m Scared 3</a> (we’d even taken to calling it “Bertie the Butterfly”). Pesky bee.</p>
</li>
</ul>
Weeknotes 2https://mudge.name/2019/11/10/weeknotes-2/2019-11-10T00:00:00+00:002019-11-10T00:00:00+00:00Fixing dates on old videos in Photos, using a Raspberry Pi for Time Machine and the right tool for the job.<p>While I was conflicted about publishing <a href="/2019/11/04/weeknotes-1.html">my first weeknotes last week</a> (were they too self-indulgent? are they interesting to anyone else?), I received some nice words of encouragement and so on with this week’s!</p>
<ul>
<li>
<p>A recurring lesson in the past few weeks has been the importance of the right tool for the job:</p>
<ol>
<li>
<p>Our kitchen tap began very occasionally dripping so I decided to replace its cartridges. However, despite my best efforts with a series of adjustable spanners and a hastily-purchased set of box spanners, all I managed to do was chew up the metal of the existing cartridge. Frustrated and close to admitting defeat, I remembered that I bought a set of socket and ratchet spanners to replace a car brakelight many years before and—lo and behold—that worked straight away and I was able to swap out the cartridges in mere minutes!</p>
</li>
<li>
<p>While I was engaged in this exciting DIY adventure, E was having a customer service battle with our water company as we’d been sent a bill ten times our usual amount. Adamant we hadn’t suddenly taking to bathing around the clock, the company finally agreed to send out an engineer to investigate. Ever keen to see the workings of such things, I accompanied them to the street as they discovered they have been reading the wrong water meter for us for six years. In trying to identify our <em>real</em> meter, they accidentally broke the stop cock on it, leaving us without any running water. A second engineer promptly came to fix it using a simple-looking tool known as a stop cock key and complained to me that if only they sent all engineers out with one, there would be no need for him to have visited. I nodded politely.</p>
</li>
<li>
<p>As discussed <a href="/2019/11/04/weeknotes-1.html">last week</a>, I’ve been baking some bread again and experimented with putting dough on baking parchment before lowering it into a Dutch oven. At least, I <em>thought</em> I was using baking parchment but, thanks to swearing my head off while trying to pick apart an unholy fusion of paper and dough, I have since learnt that “greaseproof paper” and “greaseproof <em>and baking</em> paper” are crucially different.</p>
</li>
</ol>
</li>
<li>
<p>Reading <a href="https://vfoley.xyz/lesser-known-coding-fonts/">Vincent Foley’s “Lesser Known Coding Fonts”</a> sent me down a bit of a rabbithole trying to find the <em>perfect</em> monospaced typeface, particularly after watching the recent episode of <a href="https://www.netflix.com/gb/title/80057883">“Abstract: The Art of Design”</a> about <a href="https://www.typography.com">typeface design with Jonathan Hoefler</a>. In the end, I abandoned this quest for what it was: an act of pure time-wasting.</p>
</li>
<li>
<p>It has been five years since <a href="https://www.youtube.com/watch?v=QrGrOK8oZG8">Casper Kelly’s “Too Many Cooks”</a>. I continue to pay tribute to it in my own small way by having <a href="http://rushcoil.bandcamp.com/track/too-many-cooks-rush-coil-nes-cover">Rush Coil’s NES cover of it</a> as my ringtone.</p>
</li>
<li>
<p>C having his 8-week <a href="https://www.nhs.uk/conditions/vaccinations/">vaccinations</a> and talk of preventing fevers led me to <a href="https://www.theguardian.com/lifeandstyle/2019/jun/04/why-parents-are-addicted-to-calpol">Jenny Kleeman’s “Why parents are addicted to Calpol”</a> so it was with an unearned sense of pride I marched into our local pharmacy and asked for whatever generic, liquid baby paracetamol suspension they had.</p>
</li>
<li>
<p>It has been quite the week of computer noodling for me:</p>
<ul>
<li>
<p>As I manage my family’s email accounts, I decided to spend some time implementing <a href="https://support.google.com/a/answer/2466580?hl=en">Domain-based Message Authentication, Reporting & Conformance (DMARC)</a> for my domains. As a side-effect, I now get a daily report of the times people are sending emails pretending to be from <code class="language-plaintext highlighter-rouge">mudge.name</code>. These attempts <em>should</em> now go straight to spam but it’s still a little disconcerting.</p>
</li>
<li>
<p>Following <a href="/2019/11/04/weeknotes-1.html">last week</a>’s adventures with backups, I decided to set up <a href="https://support.apple.com/en-us/HT202784">a network-based Time Machine backup</a>.</p>
<p>The first step was to find a hard drive big enough to store backups and, in my search, I uncovered old videos of myself dating back to 1998. This led me to compress all these 21-year old videos using <a href="https://support.apple.com/en-gb/guide/automator/welcome/mac">Automator</a> and then import them into <a href="https://www.apple.com/uk/macos/photos/">Photos</a>. However, none of the videos had the correct date on them.</p>
<p>Thankfully, I noticed the videos <em>did</em> contain their real date and time in their titles, e.g. <code class="language-plaintext highlighter-rouge">CLIP-2003-08-02 21;40;28.MOV</code>. As there were over 700 of these videos, updating them manually didn’t seem a smart option so I decided to look into scripting Photos via the <a href="https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/index.html#//apple_ref/doc/uid/TP40016239">Mac Automation Scripting Guide</a>. While it seemed like I might have to learn a whole new programming language, I vaguely recalled that you can now script macOS applications with JavaScript.</p>
<p>I wondered if I could use any modern JavaScript features and <a href="http://umm.io">Matt MacLeod</a> pointed out <a href="https://github.com/JXA-Cookbook/JXA-Cookbook/wiki/ES6-Features-in-JXA">“ES6 Features in JXA”</a> which explains that you can use any JavaScript features supported by the version of Safari bundled with your OS.</p>
<p>I’ve since written up <a href="/2019/11/13/scripting-photos-for-macos-with-javascript.html">how I scripted Photos with JavaScript to update all the videos in one go</a>.</p>
</li>
<li>
<p>Videos of a much softer-faced and squeakier-voiced me safely stored in iCloud, it was time to get that Time Machine set up on my <a href="https://www.raspberrypi.org">Raspberry Pi</a>. This resulted in <a href="/2019/11/12/using-a-raspberry-pi-for-time-machine.html">a new post about how to use a Raspberry Pi for Time Machine</a>.</p>
</li>
</ul>
</li>
<li>
<p>C is now ten weeks old and I’ve been thinking about something from <a href="https://vimeo.com/41756240">Michael Please‘s BAFTA award-winning short film, “The Eagleman Stag”</a>: yesterday was less than a 12,500th of my life but, for C, it was over 177 times more.</p>
</li>
<li>
<p><a href="https://codon.com">Tom Stuart</a> said I might regret the semicolons in last week’s notes so I hope you enjoy this week’s luxurious full stops instead.</p>
</li>
</ul>
Weeknotes 1https://mudge.name/2019/11/04/weeknotes-1/2019-11-04T00:00:00+00:002019-11-04T00:00:00+00:00Dadding, baking, TV and being a paranormal insurance investigator.<p>Following a trail already blazed by <a href="https://natbuckley.co.uk/2019/01/20/weeknotes-1-tv-ads-and-business-models/">Nat Buckley</a>, <a href="https://alicebartlett.co.uk/blog/weaknotes">Alice Bartlett</a> and <a href="https://chriszetter.com/blog/2019/10/26/a-visit-to-kew-gardens-and-a-baking-failure/">Chris Zetter</a> and at the very real risk of over-sharing, here are my first weeknotes:</p>
<ul>
<li>
<p>I thoroughly enjoyed playing <a href="https://obradinn.com">Lucas Pope’s multi-award-winning “Return of the Obra Dinn”</a> which is now <a href="https://www.nintendo.co.uk/Games/Nintendo-Switch-download-software/Return-of-the-Obra-Dinn-1633060.html">available for the Nintendo Switch</a>, a game I finished while walking C back to sleep at 6 am. From the use of music and text alone, you can tell this is from the creator of <a href="https://papersplea.se">Papers, Please</a>;</p>
</li>
<li>
<p>I keep wanting to talk to people about how fantastic “<a href="https://www.hbo.com/succession">Succession</a>” is but so few people have heard of it, let alone seen it. I assume this is because it’s not on Netflix, Prime Video or the regular telly in the UK but I hope it gets the audience it deserves;</p>
</li>
<li>
<p>On that note, I was simultaneously amused and dismayed to read that <a href="https://www.vulture.com/2019/08/succession-connor-roy-hyperdecanting-wine.html">Connor Roy’s “hyperdecanting” is a real thing coined by Nathan Myhrvold</a>;</p>
</li>
<li>
<p>While I can’t really make time for <a href="https://london.computation.club">London Computation Club</a> any more, I have been thoroughly enjoying the <a href="https://computationclub-slack.herokuapp.com/"><code class="language-plaintext highlighter-rouge">#bread</code> Slack channel</a>, learning how to <a href="https://www.seriouseats.com/2014/11/troubleshoot-bad-bread-messed-up-loaf.html">troubleshoot bad bakes</a> and experiment with <a href="http://tartine-bread.blogspot.com/2014/01/guest-baker-chad-robertson.html">long autolyse times, baking from cold and scoring with scissors</a> (in fact, as I write this, I have some dough in bulk fermentation);</p>
</li>
<li>
<p>Having C in my life has predictably altered my perspective so it’s been with some trepidation that I’ve been listening to <a href="https://www.nickcave.com/ghosteen/">Nick Cave and the Bad Seeds’ “Ghosteen”</a> given <a href="https://en.wikipedia.org/wiki/Ghosteen#Background">its subject matter</a> but it is rather wonderful;</p>
</li>
<li>
<p>On a similarly morbid note, I’m considering re-reading <a href="https://en.wikipedia.org/wiki/Lincoln_in_the_Bardo">George Saunders’ “Lincoln in the Bardo”</a> but I am having too much fun learning about <a href="https://en.wikipedia.org/wiki/Hyperion_(Simmons_novel)">the Shrike</a> at the moment;</p>
</li>
<li>
<p>As a lot of care for C in the first few weeks involved staring directly at my phone, I went through a bit of a panic after reading <a href="https://www.theguardian.com/technology/2019/oct/12/stop-email-ping-pong-nine-ways-to-avoid-digital-distraction-nir-eyal">Nir Eyal’s guide to avoiding digital distraction</a> and followed quite a bit of advice on <a href="https://medium.com/better-humans/how-to-set-up-your-iphone-for-productivity-focus-and-your-own-longevity-bb27a68cc3d8">how to configure your iPhone to work for you, not against you</a> especially around restricting notifications. I’m not entirely convinced it has helped though as I now compulsively check applications that <em>may</em> notify me and moving applications onto secondary screens and into folders hasn’t really increased friction significantly;</p>
</li>
<li>
<p>While I have very rarely opened my laptop in the past two months, I couldn’t resist the urge to format it and do a clean install of <a href="https://www.apple.com/uk/macos/catalina/">macOS Catalina</a> (despite the growing <a href="https://tidbits.com/2019/10/21/six-reasons-why-ios-13-and-catalina-are-so-buggy/">criticism of Apple’s recent software quality</a>). I decided to use it as a bit of a Disaster Recovery test to see how easily I could recover my files if my hard drive was lost or failed.</p>
<p>The most straightforward recovery were my photos and files stored in <a href="https://www.apple.com/uk/icloud/">iCloud</a> and passwords in <a href="https://1password.com">our 1Password family account</a> (though downloading over 15 years of photos took quite some time). Less successful was the restoration of my music library and many software projects from a full <a href="https://www.backblaze.com">Backblaze</a> backup as I hadn’t realised that Backblaze does not backup file permissions (including whether a file is executable or not). <a href="https://mjtsai.com/blog/2014/05/22/what-backblaze-doesnt-back-up/">Michael Tsai wrote about Backblaze’s shortcomings in this area over five years ago</a> and I ended up restoring those files from a <a href="https://support.apple.com/en-us/HT201250">Time Machine backup</a> I’d done as a failsafe instead (I would have used <a href="https://www.shirt-pocket.com/SuperDuper/SuperDuperDescription.html">David Nanian’s SuperDuper!</a> but it doesn’t support Catalina yet).</p>
<p>Following advice from others, I’ll probably relegate Backblaze for total disasters and introduce a <a href="https://support.apple.com/en-us/HT202784">network-based Time Machine backup</a>, most likely <a href="https://github.com/mr-bt/raspberrypi-timemachine">connected to my Raspberry Pi</a>;</p>
</li>
<li>
<p>Thanks to <a href="https://natbuckley.co.uk/2019/08/26/weeknotes-32-the-one-about-baking/">Nat Buckley mentioning Claire Saffitz in their weeknotes</a>, I binged quite a few episodes of <a href="https://video.bonappetit.com/series/gourmet-makes">Bon Appétit’s “Gourmet Makes”</a> and ended up baking <a href="https://doriegreenspan.com/recipe/world-peace-cookies-the-newest-version-from-dories-cookies-sneak-peek/">Dorie Greenspan’s “World Peace Cookies”</a> mentioned in the <a href="https://video.bonappetit.com/watch/pastry-chef-attempts-to-make-gourmet-oreos?c=series">Oreo episode</a> and they were the best home-made cookies I’d ever made. <a href="https://chriszetter.com">Chris Zetter</a> recommended <a href="https://www.bonappetit.com/recipe/bas-best-chocolate-chip-cookies">Kate Davis’ “Brown Butter and Toffee Chocolate Chip Cookies” recipe</a> so I might have to give that a go soon;</p>
</li>
<li>
<p>I’ve spent an unhealthy amount of time reading and talking about <a href="https://www.theguardian.com/tv-and-radio/2019/oct/30/great-british-bake-off-is-broken-heres-how-to-fix-it">why the latest series of the Great British Bake Off went wrong</a> but I was overjoyed to discover <a href="https://www.earwolf.com/episode/how-did-you-win-great-british-bake-off-with-nancy-birtwhistle-winner-of-great-british-bake-off-series-5/">a recent episode of “Getting Curious with Jonathan Van Ness” about series 5 winner, Nancy Birtwhistle</a>;</p>
</li>
<li>
<p>I have a very long, unfinished blog post about <a href="https://reactjs.org/docs/hooks-intro.html">React Hooks</a> and <a href="https://gist.github.com/mudge/eb9178a4b6d595ffde8f9cb31744afcf">my attempts to implement debouncing with them</a> that I really should finish and publish on <a href="https://www.ghostcassette.com">my consultancy website</a> (especially since <a href="https://tuzz.tech/blog/react-commentary-sidebar-2">Chris Patuzzo mentioned it in his latest blog post</a>) but it’ll have to wait for another day;</p>
</li>
<li>
<p>Reader, you won’t believe me but <a href="/2019/01/02/2018-yearnotes.html">I ran</a> for the first time since I got married and, what’s more, it was my first time around the park we’ve lived next to for over six years;</p>
</li>
<li>
<p>I have now been a dad for nine weeks.</p>
</li>
</ul>
2018 Yearnoteshttps://mudge.name/2019/01/02/2018-yearnotes/2019-01-02T15:36:00+00:002019-01-02T15:36:00+00:00Reflecting on the past year, both personally and professionally.<p>Last year, inspired by <a href="http://ntlk.net/2017/12/29/2017-yearnotes/">Nat Buckley’s “2017 Yearnotes”</a> and <a href="https://jvns.ca/blog/2017/12/31/2017--year-in-review/">Julia Evans’ “2017: Year in review”</a>, I wrote my own “2017 Yearnotes”. Despite the fact that year saw me getting <a href="https://www.instagram.com/p/BRlk68ngORO/?taken-by=mudgemeister">engaged at the top of Goat Fell</a> on the <a href="https://en.wikipedia.org/wiki/Isle_of_Arran">Isle of Arran</a> and later <a href="https://twitter.com/mudge/status/914594665315356672">married</a> (not before completing the <a href="https://en.wiktionary.org/wiki/yak_shaving">frankly colossal act of yak shaving</a> of <a href="https://github.com/mudge/engine">writing my own PHP web framework</a>), I never published the piece. Reading it now, it’s somewhat strange to read my reflections on a lack of separation between my career and my identity and that my two hopes for 2018 were as follows:</p>
<ul>
<li>Figure out a healthier work-life balance;</li>
<li>Start running again.</li>
</ul>
<p>I flat-out failed at the latter and, as for the former, well, you could say I made some progress in that area but not in the way I could have predicted.</p>
<p>And so, some reflections on 2018.</p>
<h2 id="travelling">Travelling</h2>
<p><img src="/i/tromso.jpg" class="pull-right" width="360" height="270" alt="" /> In February, I travelled to <a href="https://en.wikipedia.org/wiki/Tromsø">Tromsø, Norway</a> for a week-long holiday exploring the breathtaking <a href="https://en.wikipedia.org/wiki/Lofoten">Lofoten islands</a>.</p>
<p>It was the first time I’ve ever been within the <a href="https://en.wikipedia.org/wiki/Arctic_Circle">Arctic Circle</a> and the combination of natural beauty and outside temperatures of -13 °C made for a heady mix of awe and fearful respect. In retrospect, our desire to take it all in and reach <a href="https://en.wikipedia.org/wiki/Å,_Moskenes">Å</a> within seven days meant we spent too much time on the road but I did get my first exposure to <a href="https://en.wikipedia.org/wiki/Lane_departure_warning_system">lane assist technology</a> which was frankly terrifying on icy roads.</p>
<p>One of the intentions of the trip was to break our daily habit of being glued to our phones and to break our dependence on constant stimulation from the internet. As with the rest of the year, I admit I wasn’t wholly successful in this endeavour though the place itself was wonderful.</p>
<p>Not long after returning from Norway, we travelled to Cheshire for the first of several weddings this year (the rest would later take us to Liverpool, Shoreditch and Durham) which—in a change from last year—are all-the-more enjoyable when you’re not planning your own.</p>
<p>2018 saw two weekend breaks: the first to sunny <a href="https://en.wikipedia.org/wiki/Margate">Margate</a> and the second, my very first weekend break abroad when we went to <a href="https://en.wikipedia.org/wiki/Porto">Porto, Portugal</a>. The latter gave us 48 hours to spelunk in port caves and seek out <a href="https://en.wikipedia.org/wiki/Pastel_de_nata">pastéis de nata</a> in very good company.</p>
<p>In further attempts to “get away from it all”, we journeyed up to Scotland several times, visiting family in Aberdeen and Glasgow and, in search of true respite, <a href="https://www.instagram.com/p/BotuE8vAnrs/">retreating to remote parts of Skye</a> (where I ended up spending hours playing <a href="http://hollowknight.com">Hollow Knight</a> on the Nintendo Switch).</p>
<p>You may have noticed a recurring theme in the aforementioned journeys: attempts to dramatically switch context, to shake off daily routine and to find some peace outside of daily life in London.</p>
<h2 id="open-source">Open source</h2>
<p>I began the year by open sourcing the aforementioned PHP web framework I wrote <a href="https://twitter.com/mudge/status/908383877927985152">in order to get married</a>: <a href="https://github.com/mudge/engine">Engine</a>.</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>composer create-project mudge/engine-skeleton:dev-master my-project
<span class="gp">$</span><span class="w"> </span><span class="nb">cd </span>my-project
<span class="gp">$</span><span class="w"> </span>php <span class="nt">-S</span> localhost:8080 <span class="nt">-t</span> public
</code></pre></div></div>
<p>The core idea of Engine is that modelling a web application as a pure function that takes in a request and produces a response as an output (i.e. <code class="language-plaintext highlighter-rouge">f(request) = response</code>) no longer feels accurate. Perhaps it is better to think of a web application as something that takes <em>both</em> a request and a response as input and achieves its desired goal by using the response for side-effects (think of the response as a sort of open socket). This way, you can easily send your response in bits or stream responses indefinitely.</p>
<p>It was also a great excuse to take advantage of <a href="http://php.net/releases/7_1_0.php">PHP 7.1’s type system</a> and baffle people who haven’t seen any recent PHP. For example, here is some real source code from our wedding site:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?php</span>
<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>
<span class="kn">namespace</span> <span class="nn">Wedding</span><span class="p">;</span>
<span class="k">final</span> <span class="kd">class</span> <span class="nc">TravelController</span> <span class="kd">extends</span> <span class="nc">PrivateController</span>
<span class="p">{</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">index</span><span class="p">():</span> <span class="kt">void</span>
<span class="p">{</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">requireUser</span><span class="p">();</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">render</span><span class="p">(</span>
<span class="s1">'travel.html'</span><span class="p">,</span>
<span class="p">[</span><span class="s1">'lodges'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="n">user</span><span class="o">-></span><span class="nf">isStayingInLodges</span><span class="p">()]</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In February, I followed this up with a failed experiment to implement <a href="https://github.com/mudge/readable">a <code class="language-plaintext highlighter-rouge">Readable</code> module in Ruby</a> that would allow users to create <a href="http://ruby-doc.org/core/IO.html"><code class="language-plaintext highlighter-rouge">IO</code></a> objects from arbitrary sources much like <a href="http://ruby-doc.org/core/Enumerator.html"><code class="language-plaintext highlighter-rouge">Enumerator</code></a> allows users to create lazy <a href="http://ruby-doc.org/core/Enumerable.html"><code class="language-plaintext highlighter-rouge">Enumerable</code></a> objects:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">readable</span> <span class="o">=</span> <span class="no">Readable</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">yielder</span><span class="o">|</span>
<span class="n">yielder</span> <span class="o"><<</span> <span class="s2">"</span><span class="se">\u</span><span class="s2">001F</span><span class="se">\x8B\b\u</span><span class="s2">0000[</span><span class="se">\u</span><span class="s2">0017</span><span class="se">\x88</span><span class="s2">Z</span><span class="se">\u</span><span class="s2">0000"</span>
<span class="n">yielder</span> <span class="o"><<</span> <span class="s2">"</span><span class="se">\u</span><span class="s2">0003</span><span class="se">\xF3</span><span class="s2">H</span><span class="se">\xCD\xC9\xC9\xD7</span><span class="s2">)</span><span class="se">\xCF</span><span class="s2">/</span><span class="se">\xCA</span><span class="s2">I"</span>
<span class="n">yielder</span> <span class="o"><<</span> <span class="s2">"</span><span class="se">\u</span><span class="s2">0001</span><span class="se">\u</span><span class="s2">0000)^</span><span class="se">\u</span><span class="s2">0014</span><span class="se">\xFC\v\u</span><span class="s2">0000</span><span class="se">\u</span><span class="s2">0000</span><span class="se">\u</span><span class="s2">0000"</span>
<span class="k">end</span>
<span class="n">gz_reader</span> <span class="o">=</span> <span class="no">Zlib</span><span class="o">::</span><span class="no">GzipReader</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">readable</span><span class="p">)</span>
<span class="no">CSV</span><span class="p">(</span><span class="n">gz_reader</span><span class="p">).</span><span class="nf">gets</span>
<span class="c1">#=> ['Hello', 'world']</span>
</code></pre></div></div>
<p>Sadly, this <a href="https://github.com/mudge/readable#why">didn’t fully work due to the sprawling surface area of the <code class="language-plaintext highlighter-rouge">IO</code> module</a> (though, later in the year, <a href="https://github.com/dkandalov/coroutines-explained">Dmitry Kandalov’s “Coroutines explained”</a> made me want to try this again with <a href="https://ruby-doc.org/core/Fiber.html"><code class="language-plaintext highlighter-rouge">Fiber</code></a>s).</p>
<p>In July, thanks to the help of <a href="http://london.computation.club">London Computation Club</a>, a particularly thorny <a href="https://en.wikipedia.org/wiki/Graph_theory">Graph theory</a> problem at work led me to implement <a href="https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm">Dijkstra’s algorithm</a> in Ruby.</p>
<p>While the initial implementation using arrays worked, it could take an hour to run and so I looked into implementing a <a href="https://en.wikipedia.org/wiki/Priority_queue">priority queue data structure</a> to speed things up. In a surprising twist, I discovered an ideal algorithm not online but in a printed textbook gathering dust on my shelf: <a href="https://mitpress.mit.edu/books/introduction-algorithms-third-edition">Cormen, Leiserson, Rivest and Stein’s “Introduction to Algorithms, Third Edition”</a>. Implementing their description of Fibonacci Heaps was difficult but ultimately rewarding when I published a new RubyGem imaginatively titled <a href="https://github.com/mudge/fibonacci_heap">Fibonacci Heap</a>.</p>
<p>The data structure allows you to insert nodes with a given priority and efficiently <code class="language-plaintext highlighter-rouge">pop</code> the highest priority node from the heap even if that priority has been modified after insertion:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">heap</span> <span class="o">=</span> <span class="no">FibonacciHeap</span><span class="o">::</span><span class="no">Heap</span><span class="p">.</span><span class="nf">new</span>
<span class="n">foo</span> <span class="o">=</span> <span class="no">FibonacciHeap</span><span class="o">::</span><span class="no">Node</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s1">'foo'</span><span class="p">)</span>
<span class="n">bar</span> <span class="o">=</span> <span class="no">FibonacciHeap</span><span class="o">::</span><span class="no">Node</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="s1">'bar'</span><span class="p">)</span>
<span class="n">baz</span> <span class="o">=</span> <span class="no">FibonacciHeap</span><span class="o">::</span><span class="no">Node</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="s1">'baz'</span><span class="p">)</span>
<span class="n">heap</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">foo</span><span class="p">)</span>
<span class="n">heap</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">bar</span><span class="p">)</span>
<span class="n">heap</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">baz</span><span class="p">)</span>
<span class="n">heap</span><span class="p">.</span><span class="nf">pop</span>
<span class="c1">#=> #<FibonacciHeap::Node key=0 value="bar"></span>
<span class="n">heap</span><span class="p">.</span><span class="nf">decrease_key</span><span class="p">(</span><span class="n">baz</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="n">heap</span><span class="p">.</span><span class="nf">pop</span>
<span class="c1">#=> #<FibonacciHeap::Node key=0 value="baz"></span>
</code></pre></div></div>
<p>Replacing my array-based implementation of Dijkstra’s algorithm with a Fibonacci Heap-based one sped up our build times to mere <em>seconds</em> and was well worth it.</p>
<p>That <em>would</em> have been my open source highlight of the year until something else happened on Christmas Day: <a href="https://www.ruby-lang.org/en/news/2018/12/25/ruby-2-6-0-released/">Ruby 2.6 was released</a> containing <a href="https://bugs.ruby-lang.org/issues/6284">a certain new feature</a>.</p>
<p>That feature was the ability to compose <a href="https://ruby-doc.org/core/Proc.html"><code class="language-plaintext highlighter-rouge">Proc</code></a> objects using <a href="https://ruby-doc.org/core-2.6/Proc.html#method-i-3C-3C"><code class="language-plaintext highlighter-rouge"><<</code></a> and <a href="https://ruby-doc.org/core-2.6/Proc.html#method-i-3E-3E"><code class="language-plaintext highlighter-rouge">>></code></a>. While I have dabbled in functional programming before, this feature is of particular interest to me as it is based on <a href="https://bugs.ruby-lang.org/issues/6284#note-26">a series of patches I submitted in 2015</a>. This is my first code contribution to Ruby to be accepted and, as such, has to be my open source highlight of the year.</p>
<h2 id="public-speaking">Public speaking</h2>
<p>I only did one talk this year: <a href="https://speakerdeck.com/mudge/hyperloglog-in-15-minutes">“HyperLogLog in 15 minutes”</a> at the <a href="https://www.meetup.com/London-Ruby-Meetup/">Drover Ruby Meetup in November</a>. Huge thanks to those who listened to me <a href="https://github.com/computationclub/computationclub.github.io/wiki/HyperLogLog-in-15-minutes">rehearse this presentation</a> (some people more than once), the feedback I received massively improved everything from the slides to my delivery.</p>
<p>I’m considering expanding this into a longer talk for the new year so that I can reduce the amount of hand-waving and dig into the mathematics a bit more but I’m still undecided.</p>
<h2 id="work">Work</h2>
<p><a href="https://www.instagram.com/p/Bmd7oNYAOVy/"><img src="/i/altmetric.jpg" class="pull-right" width="360" height="360" alt="" /></a> The elephant in this post is that in August of this year, after 5 years, I gave notice of my resignation as CTO of <a href="https://www.altmetric.com">Altmetric</a>.</p>
<p>The year leading up to that point had seen some great successes from the team: the rewrite of the company’s oldest data source, the <a href="https://www.altmetric.com/press/press-releases/patent-data-in-altmetric-highlights-the-commercialization-of-research/">addition of over 8 million patents</a>, a <a href="https://www.altmetric.com/press/press-releases/altmetric-releases-explorer-highlights-and-enhanced-user-experience/">huge user interface and technology overhaul to the flagship product</a>, the <a href="https://www.altmetric.com/press/press-releases/altmetric-adds-dimensions-citation-data-to-highlight-academic-impact-of-scholarly-work/">integration of scholarly citations for the full database</a> and some especially tricky performance work on the company’s <a href="https://en.wikipedia.org/wiki/Extract,_transform,_load">Extract, transform, load processes</a>.</p>
<p>Ultimately though, my struggle to find a sustainable place for myself within the company (as evidenced by my feelings in the previous year and by my attempts to take a break and even start <a href="https://www.headspace.com">meditating</a>) was in vain. Considering how blissful I felt during my honeymoon at the end of 2017, it was shocking to me how much my work permeated my every waking thought and how quickly I fell into the same trap of my own doing.</p>
<p>At my lowest ebb, I booked a long Bank Holiday weekend off at the last minute and asked many people in my field for advice, many of them from <a href="http://london.computation.club">London Computation Club</a>. I am hugely thankful to all those who kindly took the time to speak with me and help me navigate a very difficult decision.</p>
<p>I worked the remaining four months of the year, handing over to <a href="https://www.altmetric.com/about-us/people/">my amazing colleagues</a>, confident in their ability to learn from my mistakes and lead the team to even greater success.</p>
<p>I hope to write more about my time at Altmetric in the future as I learnt an incredible amount trying to build a software development team there over five and a half years and I would do it all over again in a heartbeat.</p>
<h2 id="2019">2019</h2>
<p>I’m honestly not sure what is next. After working continuously for a decade in London, I plan to take a break from keyboards and screens for a short while before seeing how I can best be of help to others, be that as a contractor or as a full-time employee.</p>
<p>Despite having already published <a href="https://github.com/mudge/homer">a new open source project this year</a>, I hope to continue seeking a better balance between work and life and, as with last year, get back into running.</p>