<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
  <channel>
    <title>remy sharp's b:log</title>
    <atom:link href="https://remysharp.com/blog.xml" rel="self" type="application/rss+xml"></atom:link>
    <link>https://remysharp.com</link>
    <description>About [code] and all that jazz</description>
    <image>
      <title>remy sharp's b:log</title>
      <link>https://remysharp.com</link>
      <url>https://remysharp.com/images/avatar-125.jpg</url>
      <width>125</width>
      <height>125</height>
    </image>
    <lastBuildDate>Mon, 18 May 2026 21:00:45 +0000</lastBuildDate>
    <managingEditor>remy@remysharp.com (Remy Sharp)</managingEditor>
    <webMaster>remy@remysharp.com (Remy Sharp)</webMaster>
    <language>en</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <generator>Honestly, I don't know what most of these tags do, I copied a lot of them from other people's web site ¯\_(ツ)_/¯</generator>
    <item>
      <title>The 30 year game</title>
      <guid isPermaLink="false">the-30-year-game</guid>
      <link>https://remysharp.com/2026/05/17/the-30-year-game</link>
      <pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[Today I'm releasing my Game Boy game called Marbles² (or Marbles Squared). It's a port of a game that I originally wrote back in 2002, but is a game that started in my life in (or around) 1996.
The development this iteration of the game, the one in 2026, took me about a week and I haven't written a single line of code. Here's how things have changed.]]></description>
      <content:encoded><![CDATA[
<p>Today I'm releasing my Game Boy game called Marbles² (or Marbles Squared). It's a port of a game that I originally wrote back in 2002, but is a game that started in my life in (or around) 1996.</p>
<p>The development this iteration of the game, the one in 2026, took me about a week and I haven't written a single line of code. Here's how things have changed.</p>
<h2>TL;DR</h2>
<p>I get it, this is a stupidly long blog post. There's no real value buried inside of it, it's pretty much entirely a story of how technology has progressed over 30 years (not really a spoiler) and my journey to evolve a game that I found in 1996.</p>
<p>The latest edition of this game was coded entirely by Claude Code with one strict rule: keep inside of the 32K boundary limit. There was a huge saving that Claude never suggested (1bpp font tiles instead of 2bpp) which bought me 3K - which was enough to add a full tutorial in the game along with additional small features.</p>
<p>I also tried to vibe code another game using the same process only to fail - the key take away for me was that my experience and knowledge was still the guiding light. With the other game, it failed because my knowledge was shallow, so the output was shallow. Sort of obvious on paper, but a useful exercise for me.</p>
<p>Oh, and if you scroll to the end, there's a link to the game download page.</p>
<h2>1996</h2>
<p>At some point during my days during Sixth Form (college to some, basically aged 16-18), a close friend had (some) machine (that neither of us can remember) and they let me play a game on it.</p>
<figure><img src="https://remysharp.com/images/remy-6th-form-days.avif" alt="A picture of Remy and Julie when they were just babies, both looking dreamily into the camera" decoding="async"><figcaption>Remy &amp; Julie from their Sixth form days in the 90s</figcaption></figure>
<p>I remember really enjoying the game. It was technically a very simple puzzle game: a bunch of balls that you would tap the matching colour groups of balls and they would collapse down into the corner. The goal being to clear the screen.</p>
<p>I never caught the name of the game but my dad had a <a href="https://en.wikipedia.org/wiki/Psion_Series_3">Psion</a> and remembered that in the instruction manual (long gone are those times) it mentioned you could write your own software.</p>
<p>I'd been making little <a href="https://en.wikipedia.org/wiki/Visual_Basic_(classic)">Visual Basic</a> <a href="https://tweets.remysharp.com/1181605537571655681/">programs for friends</a> for a few years by then, so I decided that since I had access to the internet (of sorts) through bulletin boards I would try and build my version of game so I could play it all the time!</p>
<p>When I finally managed to download <em>and</em> printed out the <a href="https://archive.org/details/psion-sibo-c-sdk">documentation</a> (I remember it just kept printing and printing and printing), the very first is what hit the hardest.</p>
<p>SDK. This was foreshadowing. I had no idea what an <em>SDK</em> was. Nor any of the other mass of acronyms. And software development being gate-kept by the usual nerds meant there wasn't going to be any help with the &quot;basic stuff&quot;.</p>
<p>(In truth is might have been any kind of acronym, I just remember being bewildered).</p>
<p>Google doesn't exist yet so I tried thumbing through the documentation and spent probably weeks just going back and forth just looking for any clue that would help me get a footing so I'd have something to start with.</p>
<p>In the end, I got absolutely nowhere. I park the idea and put it down to lack of knowledge, and lack of experience. This was not for someone like me. This was for people who had the inside knowledge. Plus, at that age, I had plenty of other (better) distractions.</p>
<h2>2002</h2>
<p>In 1999 I started my work placement for a year as part of my Computer Information Systems Design sandwich course degree (a mouthful!). In the end, I'd never return to finish my degree and stayed on for a decade.</p>
<figure><img src="https://remysharp.com/images/remy-dl-days.avif" alt="Remy, just about, wearing god knows what: large Elvis sunglasses, fake elf ears, a set of flowers around his neck, a fake HEAD (of all things) and a nurses hairband - it's not pretty" decoding="async"><figcaption>Certainly a lot less dreamy that the 90s</figcaption></figure>
<p>But back to '99 and the start of the new century. The company was a web agency with the boss and two students (myself being one). Along with client work, the agency also had it's own side projects, one of which was <a href="https://web.archive.org/web/20011225185437/http://www.eurocool.com/palm/apps/latest/index.html">Eurocool</a> - a website dedicated to software for the Palm Pilot (they even made <a href="https://web.archive.org/web/20011212081559/http://news.bbc.co.uk/hi/english/events/the_launch_of_emu/the_uk_and_emu/newsid_222000/222808.stm">BBC news</a>).</p>
<p>So I eventually bought myself one and through Eurocool I started notice that some of the games and some of the utilities came with source code and the source code was not huge and by this point I had learnt a lot more programming.</p>
<p>By that time I had tinkered with some C, worked with Java. I was writing JavaScript (JScript and VBScript). I was writing Perl and I could start to understand the functions of the source code and I went about making my first utility for the Palm pilot for me.</p>
<p>I started with a couple of smaller bits of software for the Palm Pilot, before I realised I could (and should) make a game</p>
<p>As it turns out that I'm not a very good graphic designer, so when it came to drawing circles for the Palm Pilot it was…very, very bad. The best I could do was draw squares and colour them in. So this is where the name Marbles Squared comes from (<em>&quot;it's a feature, not a bug!&quot;</em>).</p>
<p>It took me until the mid 2020 so realise that the original game from the 90s was actually <a href="https://en.wikipedia.org/wiki/SameGame">SameGame</a> that I had unwittingly cloned (though only the game play mechanic, the Wiki page details the scoring system which I was never aware of).</p>
<p>One of the key mechanics of my version of the game was that the the random function would have a <a href="https://en.wikipedia.org/wiki/Random_seed">seed</a>, and that seed would make the grids being played repeatable and shareable.</p>
<p>My game even <a href="https://remysharp.com/2007/03/23/the-day-i-appeared-in-scientific-american">turned up in American Scientific</a> (though the original article has been long disappeared sadly). After a few years, the game had been downloaded over 1/4 million times.</p>
<p>More importantly though, the coding part took me (probably) 3 months to go from scratch to the first version of a workable game, mostly coding late into the night because I had a full-time job (running in start-up days of getting home at 8pm and later). I'd continue to iterate on it for a further 6 months and the <a href="https://ihatemusic.com/">last release was in 2003</a>. If you want to try the game on a (pretty faithful) web port of the Palm Pilot, I'm hosting the <a href="https://marbles2.com/palm/">Palm version on the Marbles</a> website.</p>
<h2>2008</h2>
<p>By the mid-late 2000s, I had move away from London and I joined the web scene down in Brighton. Coincidentally the iPhone had launched in 2007.</p>
<figure><img src="https://remysharp.com/images/remy-2008-cats.avif" alt="Remy awkwardly holding his three cats, one black and white, one tabby and one long haired" decoding="async"><figcaption>I remember using this as an excuse for not going out and meeting Brighton webbies</figcaption></figure>
<p>Eventually I caved and got myself an iPhone and Steve Jobs <a href="https://m.youtube.com/watch?v=p1nwLilQy64">original vision</a> was for developers to use the web and the power of the browser (sidebar: that wasn't really his vision, he just <a href="https://www.theguardian.com/technology/appsblog/2011/oct/24/steve-jobs-apps-iphone">didn't want 3rd party apps</a>).</p>
<p>So that's what I did. I took my knowledge of Marbles Squared and the only code that was directly ported was the random function so that I could continue supporting the seed mechanic.</p>
<p>I also add a timer bar across the bottom of the screen (to let me play with CSS transitions), but functional it did nothing, so watching friends play the game and panicking about the timer running out was a real delight!</p>
<p>Developing the core of the mobile version was a matter weeks (if that) because I was breathing JavaScript daily, and that I was so familiar with how the game would play.</p>
<p>As with all these projects, it really wasn't the core gameplay that took the time, it was the UX around the game. The damn button in particular with the &quot;new&quot; <code>border-image</code> (vendor prefixed to kingdom come!) probably proved to be the trickiest parts.</p>
<p>A it probably took me a few weeks to finish the <a href="https://marbles2.com/mobile">mobile version</a>.</p>
<h2>2020</h2>
<p>In 2020, the <a href="https://www.kickstarter.com/projects/spectrumnext/zx-spectrum-next">ZX Spectrum Next Kickstarter</a> project was delivered (after quite a few long years of waiting) and with the pandemic lockdown in full swing, it was time to tinker.</p>
<figure><img src="https://remysharp.com/images/remy-zxnext.avif" alt="Remy with his head resting awkwardly on a desk, with a spectrum, a modern keyboard, some kind of phone and various figurines from lego Batman characters to super mini Jokers" decoding="async"><figcaption>This was the vibe of tinkering on tech during lockdown!</figcaption></figure>
<p>The Next came with an upgraded (and much more capable) NextBASIC (plus the crystal boost to 28Mhz made a big difference). So I set about making myself a game.</p>
<p>I wrote a clone of <a href="https://remysharp.itch.io/go-mummy">Oh Mummy</a> that I used to play on the my friends' Commodore C64 but after that was done I turn my hand to recreating my Marbles game.</p>
<p>The challenge here was that even though the Next was faster than an original ZX Spectrum, NextBASIC still wasn't fast enough to do the grid traversing to workout available moves - or certainly not in a way that felt responsive.</p>
<p>So this meant turning my hand to Z80 assembly. I'd tinkered with the language but never had a real project to solve with it. This was perfect for dipping my toe into. Plus, there was 40+ years of experience on the web to learn from.</p>
<p>The bulk of the game would still be in NextBASIC but it would call out to a library that I wrote in assembly for <em>hard</em> tasks.</p>
<p>That project probably took me over 3 months because it was a brand new language. It was also much harder to debug. The development cycle is a lot longer compared to something like JavaScript.</p>
<p>But as I reach the end of that piece of work, I realised that I wanted to add a high score table.</p>
<p>The Spectrum Next (most times) included a Wi-Fi module, but it didn't have any native HTP protocol support so was another side quest.</p>
<p>I built an <a href="https://github.com/remy/next-http">HTTP client</a> in assembly for the spectrum and one of my more prouder moments that my <code>.http</code> is now part of the <a href="https://gitlab.com/thesmog358/tbblue/-/blob/master/docs/dotcommands/http.md">Spectrum Next distro</a>, part of the default library of tools.</p>
<p>This would let players see a <a href="https://marbles2.com/spectrum">global leaderboard</a> on the spectrum <em>and</em> share their own high scores directly from the retro hardware.</p>
<p>The http part was probably another three or more months, but absolutely worth it.</p>
<h2>2026</h2>
<p>In the intervening years AI has exploded as we're all well aware.</p>
<p>In the last few months I have taken to using LLMs to create small personal projects that serve a single purpose, a tool to calculate the distance between a and b, but also to add how long it would take me to walk or run and so on. Or maybe it would be a small song game that I would share with my friends.</p>
<p>About a month ago, I decided that I would take my experience of vibe coding and see if I could build the Game Boy game that I've been wanting the Game Boy version of Marles Squared (ironically I discovered, literally today, that Samegame was available on the Game Boy in 1997 - though it's very different from the game I wanted to enjoy).</p>
<figure><img src="https://remysharp.com/images/remy-2026-gameboy.avif" alt="Remy standing in front of his arcade build with his Marbles Game Boy game running" decoding="async"><figcaption>30 years later, Remy's able to play the game he wanted from 1996</figcaption></figure>
<p>I was able to describe in detail exactly what I wanted and I was able to iterate over and over <em>and over</em> to get all the details kind of fine-tuned. It certainly wasn't just a <em>one shot</em> build me a Game Boy game.</p>
<p>As with all the previous iterations, the real work was in the UI.</p>
<p>I did however, set myself the constraint that the game <strong>had to</strong> fit into a 32K rom (this means it works on a cart that doesn't have an MBC - memory bank controller - chip - which is what allowed games to grow up to 8MB large).</p>
<p>The LLM (Claude Code in my case) was also vital to really quickly turning around tooling that helped me in the development process. Tools to slice my <a href="https://tools.remysharp.com/gb-tile-converter/">graphics into tiles</a> or to <a href="https://tools.remysharp.com/gb-tile-visualizer/">visualise tile data</a> inline to code, or to <a href="https://tools.remysharp.com/gb-font-extractor/">convert fonts</a> (for variable width fonts which I didn't use in the end due to byte size limits).</p>
<p>I was also able to take an existing rust implementation of a Game Boy emulator and layered on an MCP server so that that Claude Code could test, screenshot and probe the memory of the game to see if it was doing what I needed to do.</p>
<p>But the whole process was me just talking back and forth to Claude code in particular to get this game built and the vast majority of it took about a week.</p>
<p>After a couple of rounds of testing, I did notice that the couple of people who tried the game didn't <em>quite</em> know what the goal was. A &quot;how to play&quot; section was needed in the game, but given the constraints of 32K, lumping in a load of text copy wasn't going to work.</p>
<p>One of the biggest wins I had in optimising the codebase was <em>not</em> from Claude. I could see all the text tiles (the tiles that make up the font) were two colours. Whereas a Game Boy tile stores 4 bits of colour in a &quot;pixel&quot;, I could see I was wasting a lot of space. GBDK even includes a native function <code>set_bkg_1bpp_data</code> that will make use of 1bpp tile data. From this single change (I had to update my tools to generate 1bpp) I was able to unlock over 3K. This space was then used for the tutorial and a number of other small changes (including increasing the pool of &quot;easy game seeds&quot;).</p>
<hr>
<p>So in the 30 years that's passed I've gone from taking weeks and weeks looking at documentation and getting nowhere to taking a week to build a fully working piece of software that suits all of my requirements.</p>
<p>But the reality isn't as straightforward to say you can just magic up anything you want. My advantage I have is that I've got decades worth of experience in software development. I also have an intimate understanding as to how this game is supposed to work.</p>
<p>As an experiment I tried to vibe code another game. Our family was playing <a href="https://100jumps.org">100 Jumps</a> and I thought it'd be a good candidate for a Game Boy game.</p>
<p>The source code is online, and on the surface it seemed pretty straightforward to me to re-use the techniques that I was using to develop Marble Squared.</p>
<p>Except what happened is I kept iterating on something that was awful. The physics were wrong. The movement was wrong. The graphics were bad. No matter how many times I tried to iterate and even reset, it just didn't come out feeling like a decent experience.</p>
<p>I've come to realise that the reason for this, is because of where my experience lies, and in particular my intimate understanding of how my <em>own</em> game works. This knowledge allowed me to carefully plan out the development, to know where all the pitfalls were ahead of time, which I simply didn't have for 100 Jumps.</p>
<p>I think we're starting to see in the tech industry that the pitch that AI is here to replace our jobs isn't entirely true. In fact, a lot of us have known for decades that the real skill isn't in producing code, but actually understanding functionality and translating it to the real world.</p>
<hr>
<p>If you made it this far, thank you for reading. If you simply skipped from the top to find the download link, I appreciate you too 😄</p>
<p>You can download the game over on itch, and if you fancy your chances, you can submit your high score via the <a href="https://marbles2.com/gameboy">Marbles website</a>.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/05/17/the-30-year-game">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>A popover backdrop anti-pattern</title>
      <guid isPermaLink="false">popover-backdrop-anti-pattern</guid>
      <link>https://remysharp.com/2026/05/08/popover-backdrop-anti-pattern</link>
      <pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[After attending recent conferences and learning that I can swap out buckets of JavaScript for the HTML native popover property, I've been using it liberally throughout my own projects.
For little pop out menus it's perfect. Then I also started using it for modal boxes (yes, you might be thinking ahead of me: dialog, still, let's find out what happened).
The popover property was really easy for this, and the ::backdrop selector was perfect for adding a little backdrop blur to tell my user &quot;this is the thing you're dealing with&quot;.
I especially love that all the UX around dismissing is handled for me. I can hit the escape key, or click outside the popover and it's dismissed.
Except, I kept noticing that when I dismissed my popover it would sometimes vanish then immediately pop back to life…]]></description>
      <content:encoded><![CDATA[
<p>After attending recent conferences and learning that I can swap out buckets of JavaScript for the HTML native <code>popover</code> property, I've been using it liberally throughout my own projects.</p>
<p>For little pop out menus it's perfect. Then I also started using it for modal boxes (yes, you might be thinking ahead of me: <code>dialog</code>, still, let's find out what happened).</p>
<p>The popover property was really easy for this, and the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::backdrop"><code>::backdrop</code></a> selector was perfect for adding a little backdrop blur to tell my user &quot;this is the thing you're dealing with&quot;.</p>
<p>I especially love that all the UX around dismissing is handled for me. I can hit the escape key, or click outside the popover and it's dismissed.</p>
<p>Except, I kept noticing that when I dismissed my popover it would sometimes vanish then immediately pop back to life…</p>
<h2>On using the wrong tool</h2>
<p>It was through a chat with <a href="https://www.bram.us/">Bramus</a> during a <a href="https://beyondtellerrand.com/">BTConf</a> lunch that it was pointed out that I should be using a <code>dialog</code> for a modal.</p>
<p>I think the reason I didn't think of this earlier is down to the age old problem of naming. I have a mental model that a modal is a different thing from a dialog box. Specifically a dialog lets me set it aside whilst I carry on with what I'm doing.</p>
<p>For example, in many native desktop apps, a preference dialog can be opened, but I can still interact with the software.</p>
<p>Whereas a modal is &quot;you need to do this and we'll lock out the app until you do&quot;. Although dismissing the modal is also valid as an action.</p>
<p>Anyway, that was my thinking as to why I hadn't used a dialog for modal actions.</p>
<p>I also didn't know I could use the <code>::backdrop</code> selector with <code>dialog</code> - mostly because I'd heard of it in the context of the popover.</p>
<h2>So what's the anti-pattern?</h2>
<p>A popover doesn't trap focus (which is the whole point of a <code>dialog</code>). But I wasn't looking at keyboard focus, I was looking at mouse focus.</p>
<p>When I used the popover for a modal (looking) element, and used a blur on the <code>backdrop</code> selector, I didn't realise that clicking on the backdrop would let the click carry through to whatever was underneath it.</p>
<p>So visually the UX is saying the modal is the thing that you need to do, and clicking anywhere else will dismiss it. Technically, I could have a solid colour hiding everything under the modal and you'd have no idea what you were clicking through to.</p>
<p>Let's look at the two buttons below. The &quot;click jack&quot; one will show an alert box. If you click the &quot;modal&quot; button, you'll get my modal, and under the solid backdrop, I'm going to position the &quot;click jack&quot; button across the entire screen, so when you click away to dismiss the modal, you should get the alert.</p>
<div id="demo-popover" popover class="popover-demo">
  <p>This is my modal. I don't have a button to dismiss this, but you can click anywhere on the page to dismiss it, and … see what happens.</p>
</div>
<button type="button" class="button inline" onclick="alert('click jack button got clicked')" id="demo-clickjack">click jack</button>
<button type="button" class="button inline" popovertarget="demo-popover">modal</button>
<style>
#demo-popover::backdrop {
  /* note that I've used this semi-opaque color due to a bug in Firefox
     https://bugzilla.mozilla.org/show_bug.cgi?id=2038260
  */
  background-color: rgba(255, 19, 240, 0.99);
}

#demo-popover:popover-open + #demo-clickjack {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  cursor: auto;
}
</style>
<p>Besides the JavaScript click handler for the click jack alert, this is all done with CSS:</p>
<pre><code class="language-css"><span class="token comment">/* put a solid colour over the page */</span>
<span class="token selector">#demo-popover::backdrop</span> <span class="token punctuation">{</span>
  <span class="token property">background-color</span><span class="token punctuation">:</span> hotpink<span class="token punctuation">;</span>
  <span class="token comment">/*
    note there's a bug that I've filed in FF that
    doesn't like solid backgrounds.
    https://bugzilla.mozilla.org/2038260
  */</span>
<span class="token punctuation">}</span>

<span class="token comment">/* expand the "bad" button under the page */</span>
<span class="token selector">#demo-popover:popover-open + #demo-clickjack</span> <span class="token punctuation">{</span>
  <span class="token property">position</span><span class="token punctuation">:</span> fixed<span class="token punctuation">;</span>
  <span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
  <span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
  <span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
  <span class="token property">height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>This isn't really a demo of click jacking (I've not fully thought though whether it's a real world issue or not), but it shows how I had managed to click to dismiss my modal and then accidentally hit a &quot;live&quot; target in my blurred background.</p>
<h2>Now with a dialog</h2>
<dialog id="demo-dialog" class="popover-demo" closedby="any">
  <p>This is my modal. I don't have a button to dismiss this, but you can click anywhere on the page to dismiss it, and … see what happens.</p>
</dialog>
<button type="button" class="button inline" onclick="alert('click jack button got clicked')" id="demo-modal-clickjack">click jack</button>
<button type="button" class="button inline" command="show-modal" commandfor="demo-dialog">modal</button>
<p>In this example, when the <code>::backdrop</code> is active, it does still have a giant &quot;click jack&quot; button under it (you'll need to inspect the DOM to validate this), but the backdrop <em>also</em> traps the clicks and it doesn't go through.</p>
<p>That said, you can swap this same code to use <code>popovertarget</code> and the same problem occurs (because it's not modal, and won't trap the click).</p>
<p><em>Side note: oddly, if you happen to click on the button that opened the popover, it doesn't re-open the popover. You can see this in the <a href="https://developer.mozilla.org/en-US/play?uuid=e989dbaddcdee714679e010fe188787cc8126a7b&amp;state=hVBBCgIxDPxKyFndu9S9%2BAAvHnup29BWum3ZZhUR%2F26gK7ggeMokzEwmeaLnMeIe1WVmzglKLvlGE5vJER80jo%2BtDSZmp7E%2FFUrQOtU1fq%2BTTqrNINi14GMmJABV%2BrMPddHD3VTI4kcW5hqSA7NebQYOEscwT0FW0U51pfn8D%2FrLSRg%2BWJIrjjFX%2BsqvuqYTiBscapVvCLgulT2NJDAG5xlfbw%3D%3D&amp;srcPrefix=%2Fen-US%2Fdocs%2FWeb%2FHTML%2FReference%2FElements%2Fdialog%2F">MDN demo of dialog with popover</a></em>.</p>
<style>
#demo-dialog::backdrop {
  background-color: rgba(255, 19, 240, 0.99);
}

#demo-dialog[open] + #demo-modal-clickjack {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  cursor: auto;
}
</style>
<h2>I think ::backdrop might be the problem</h2>
<p>For me to fall so quickly into this pattern of using a <code>popover</code> with the <code>::backdrop</code>, and not realise I was taking the wrong approach <em>feels</em> like it might be something others will fall into quickly too - especially if you work in an environment where generated code isn't scrutinised.</p>
<p>I could even see developers trying to use <code>pointer-events</code> to prevent clicking through the <code>::backdrop</code> whilst using <code>popover</code> which would really be committing to the wrong approach.</p>
<p>Another question is: what is the <code>::backdrop</code> pseudo element for, if not obscuring the background? Therefore, if the background is obscured, then surely we're looking at a modal element (because the background is now visually out of focus)?</p>
<p>Which all leads me to wonder if <code>popover</code> should even have support for <code>::backdrop</code>.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/05/08/popover-backdrop-anti-pattern">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>devtools: how to query through the shadow DOM</title>
      <guid isPermaLink="false">devtools-how-to-query-through-the-shadow-dom</guid>
      <link>https://remysharp.com/2026/05/01/devtools-how-to-query-through-the-shadow-dom</link>
      <pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[This is a literal TIL but was so handy I had to put it my blog so that I wouldn't forget it later.
Thanks to Big Brain Keith Cirkel for sharing this.
As (hopefully) you know, there's the $ and $$ functions in devtools. For querySelector and querySelectorAll respectively.
Well there's also $$$ to query through the shadow DOM. This means the query will cut through and into the nested DOM inside of the shadow DOM making it much easier to navigate and search for elements when debugging or styling.
It's worth also adding that, just like $ and $$, this third function also takes a second argument for context, so it means this is valid (where $0 is the currently selected DOM node in the devtools inspector):
$$$('ha-card', $0)

Begs the question: what's going to be lurking inside of $$$$ in 5 years time!]]></description>
      <content:encoded><![CDATA[
<p>This is a literal <abbr title="Today I learnt">TIL</abbr> but was so handy I had to put it my blog so that I wouldn't forget it later.</p>
<p><a href="https://bsky.app/profile/keithamus.social/post/3mkqqbxnnys27">Thanks to</a> Big Brain Keith Cirkel for sharing this.</p>
<p>As (hopefully) you know, there's the <code>$</code> and <code>$$</code> functions in devtools. For <code>querySelector</code> and <code>querySelectorAll</code> respectively.</p>
<p>Well there's <em>also</em> <code>$$$</code> to query <em>through</em> the shadow DOM. This means the query will cut through and into the nested DOM inside of the shadow DOM making it <em>much</em> easier to navigate and search for elements when debugging or styling.</p>
<p>It's worth also adding that, just like <code>$</code> and <code>$$</code>, this third function <em>also</em> takes a second argument for context, so it means this is valid (where <code>$0</code> is the currently selected DOM node in the devtools inspector):</p>
<pre><code>$$$('ha-card', $0)
</code></pre>
<p>Begs the question: what's going to be lurking inside of <code>$$$$</code> in 5 years time!</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/05/01/devtools-how-to-query-through-the-shadow-dom">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Apps I use, that you might not know</title>
      <guid isPermaLink="false">apps-i-use-that-you-might-not-know</guid>
      <link>https://remysharp.com/2026/04/27/apps-i-use-that-you-might-not-know</link>
      <pubDate>Mon, 27 Apr 2026 09:00:00 +0000</pubDate>
      <description><![CDATA[This is a cheap post to share some of the apps (macos, sorry Windows users) that I use on a fairly regular basis and think some of you might not have heard of before (whilst still being useful).]]></description>
      <content:encoded><![CDATA[<p>This is a cheap post to share some of the apps (macos, sorry Windows users) that I use on a fairly regular basis <em>and</em> think some of you might not have heard of before (whilst still being useful).</p>
<h2>Aliento</h2>
<p>For some reason macos, for quite a number of versions has made it really hard to dismiss notifications (heaven forbid you repeatedly flash a raspberry pi pico). The hot target is horrible and there's many notifications that don't bundle together (the unmounted drive springs to mind).</p>
<p>Aliento give me a single key binding that swiped all notifications away. A daily use for me.</p>
<p><a href="https://inchman.gumroad.com/l/Aliento">Aliento</a></p>
<h2>Muzzle</h2>
<p>Speaking of notifications, if you've ever given or seen a talk or video call when a distracting notification appears then this the app to solve it.</p>
<p>It's a toggle that <em>muzzles</em> your notifications. Once turned off, they'll all pile in - and you can review or use Aliento.</p>
<p><a href="https://muzzleapp.com/help/">Muzzle</a></p>
<h2>Shottr</h2>
<p>I'm sure most of us have our goto screenshot tool. I've been using Shottr for quite a few years now. The base functionality is good, but what it adds is a quick way to draw, point, annotate and even OCR screenshots.</p>
<p>One extra little feature I like is that I can then drag the screenshot to where I want to share and then the file is never saved (my screenshot folder is 'uuuge already!).</p>
<p><a href="https://shottr.cc/">Shottr</a></p>
<h2>VoiceInk</h2>
<p>As big as my phone is, I find myself dictating more and more (somewhere last year the voice recognition had a massive step change improvement).</p>
<p>Lately I'd been looking for a suitable voice to text on my desktop and laptop but ideally with the processing done on machine (rather than needing to wait for the roundtrip to some random server).</p>
<p>I use VoiceInk multiple times a day. Both for push-to-talk and for much longer sentences.</p>
<p>There's options to download different models, but I've found the Apple native STT to be suitable and usually the quickest (even though one machine is on Somona and the other is running Tahoe).</p>
<p><a href="https://github.com/Beingpax/VoiceInk">VoiceInk</a></p>
<h2>KeyboardCleanTool</h2>
<p>It helps you do the thing it says. Though it doesn't clean your keyboard, it does trap all key presses so you're not randomly posting emails containing keyboard mashing.</p>
<p><a href="https://folivora.ai/keyboardcleantool">KeyboardCleanTool</a></p>
<h2>Karabiner-Elements</h2>
<p>This one has been installed on my machine(s) for years. It's one of those apps that sit in the background, do the work, and make things just a little bit more to your liking.</p>
<p>Primarily it's a key binding app. In my case, I wanted to swap my ctrl and alt keys, but only when using a specific keyboard. This does the job.</p>
<p>It also allows me to add much more complicated bindings (which historically have been tricky, but LLMs are pretty good at generating these if you need them too).</p>
<p><a href="https://karabiner-elements.pqrs.org/">Karabiner-Elements</a></p>
<h2>Better mouse</h2>
<p>I've got a Logi MX (something) mouse. It's very fancy, and has more buttons than camo trousers have pockets.</p>
<p>Earlier this year, Logitech pushed a server change that somehow (I really don't understand how or why) borked the Logi mice/mouses(?!).</p>
<p>With that I immediately sourced an alternative that didn't bind my physical device to a cloud server 🤦</p>
<p>BetterMouse is that for me. Tonnes of configuration, specifically I can bind the buttons to perform custom tasks depending on the focused application.</p>
<p>What also stood out for me, the UI for finding the right button (that I wanted to bind) was much easier than the existing Logitech software.</p>
<p><a href="https://better-mouse.com/">BetterMouse</a></p>
<h2>Sim Daltonism</h2>
<p>I can't remember quite when I first installed this app, but it's got to be at least a decade.</p>
<p>It's a window that sits above all else, and let's me quickly cycle through different types of colour blindness.</p>
<p>I know that some of the browser dev tools included this, but I found a standalone app the simplest (since it can also see beyond the browser, such as mock ups in Photoshop or Sketch and the likes).</p>
<p>It's an app they I always reach for once the colour system is in place for the website I'm working on.</p>
<p><a href="https://michelf.ca/projects/sim-daltonism/">Sim Daltonism</a></p>
<h2>Tahoe Electron Detector</h2>
<p>Let me say this first: do not, if possible, upgrade to Tahoe. It's a total mess.</p>
<p>Still, if you unwittingly upgraded like I did, you might find that software becomes somewhat sluggish. For me I noticed hover effects in Firefox was firing log after I had left that target.</p>
<p>The issue is related (from what I understand) to some private libraries being used by Electron apps. A full reboot would resolve the issue until it raised its ugly head again.</p>
<p>So Tahoe Electron Detector checks your installed apps, and tells you which haven't been upgraded away from this particular bug.</p>
<p>Once I know what the list looked like it was easy for me to stop using those apps (Dropbox and Discord at the time, and I could use the web alternatives), and the sluggish problem went away.</p>
<p><a href="https://furbo.org/2025/10/06/tahoe-electron-detector/">Tahoe Electron Detector</a></p>
<h2>Bartender</h2>
<p>This is probably one of the most popular apps I'm listing, so I suspect many of you have already heard of it. Hopefully it helps a few of you though.</p>
<p>Bartender let's me pick and choose which menubar icons are visible, and which are tucked away. There's also a rule system that lets you show particular menubar icons depending on conditions (such as battery level or WiFi connection).</p>
<p>There are a few issues that I have with the latest edition (but are primarily due to macos rather than Bartender itself): there's a couple of system menubar icons you can't hide (like the notification centre), and I've found that some menubar apps don't open and <em>stay</em> open when Bartender is running (NordVPN being one of those apps).</p>
<p><a href="https://www.macbartender.com/">Bartender</a></p>
<hr>
<p>There's many other apps I use, but they're much more common place. There's also apps that are very cool (notist) that I just can't make into a habit.</p>
<p>On that note, I'd love to read your favourites that might be hidden gems.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/04/27/apps-i-use-that-you-might-not-know">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Fixing my slow Mac network speeds</title>
      <guid isPermaLink="false">fixing-my-slow-mac-network-speeds</guid>
      <link>https://remysharp.com/2026/03/26/fixing-my-slow-mac-network-speeds</link>
      <pubDate>Thu, 26 Mar 2026 09:00:00 +0000</pubDate>
      <description><![CDATA[For a while now both my desktop Mac (running Sonoma) and my laptop (running Tahoe - do not recommend) have had sub-optimal network speeds.
My Android phone, on our network, on the same SSID, speed tests at 1.1Gbps. My laptop gets around 160Mbps and my desktop pulls in around 80Mbps.
That's quite a difference, and even though I don't need the gigabit speeds - sometimes I can tell when the desktop is being slow (web site be getting big).
What was weird is that the network speeds get much faster if I add something like NordVPN to my connection…which doesn't make sense…]]></description>
      <content:encoded><![CDATA[
<p>For a while now both my desktop Mac (running Sonoma) and my laptop (running Tahoe - do not recommend) have had sub-optimal network speeds.</p>
<p>My Android phone, on our network, on the same SSID, speed tests at 1.1Gbps. My laptop gets around 160Mbps and my desktop pulls in around 80Mbps.</p>
<p>That's quite a difference, and even though I don't <em>need</em> the gigabit speeds - sometimes I can tell when the desktop is being slow (web site be getting big).</p>
<p>What was weird is that the network speeds get much faster if I add something like NordVPN to my connection…which doesn't make sense…</p>
<h2>TL;DR - AWDL</h2>
<p>It's a &quot;magic&quot; non standard enhancement (Apple Wireless Direct Link) to WiFi that Apple use that can seriously hamper your network connectivity. It's hard to kill, so the &quot;simpler&quot; approach is to ensure the Wifi channels you use are 44 (for 5Ghz in Europe, or 36 outside).</p>
<h2>Slightly longer</h2>
<p>My router, for it's 5Ghz channel, uses 160Mhz using (something called) DFS (apparently nothing to do with sofas…sorry, British joke): Dynamic Frequency Selection.</p>
<p>This seems to throw Apple a curve ball when it comes to their AWDL (honestly, screw Apple for having more tech that we can't control and ends up getting in the way).</p>
<p>AWDL is part of the system that's (from what I understood) responsible for things like AirDrop (which I don't use). Of course, turning it off doesn't really work - it's an easter process: it keeps respawning. It sort of stayed disabled on my laptop running Tahoe, but on my desktop running Somona it wouldn't stay down regardless of what I did.</p>
<p>Even though this problem is exclusively an Apple problem, the only way I could resurrect my network connectivity (and even though I caught it at 60Mbps, I'd seen it much, much slower) was by switching the 5Ghz to 80Mhz (all the hertz) and selecting 44 as my channel.</p>
<p>Night and day difference:</p>
<figure><img src="https://remysharp.com/images/speed-test.avif" alt="On the left, with AWDL, 60Mbps, on the right, without and getting around 1.2Gbps" decoding="async"></figure>
<p>Ultimately my plan is to set the Wifi booster/mesh/thingy to have a Mac specific 5Ghz connection point, which is then hard wired through our house into the router, then I can have 5Ghz at 160Mhz band (so nice for all other devices, like our TV) and - hopefully - the pesky Apple devices can pull data at a decent speed too.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/03/26/fixing-my-slow-mac-network-speeds">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Web of State of the Browser Day Out</title>
      <guid isPermaLink="false">web-of-state-of-the-browser-day-out</guid>
      <link>https://remysharp.com/2026/03/18/web-of-state-of-the-browser-day-out</link>
      <pubDate>Wed, 18 Mar 2026 09:00:00 +0000</pubDate>
      <description><![CDATA[Okay, that's a stupidly obscure title. It's meant to represent the combined events: State of the Browser and Web Day Out - two events I attended in the last month.
The short version is: if you get the chance to attend these events or even anything similar, I'd highly recommend that you grab that ticket and let the event wash over you.
For those that didn't attend (or maybe you did and wanted to read my perspective) then here's my thoughts on the separate events and then the round up of my own experience.]]></description>
      <content:encoded><![CDATA[
<p>Okay, that's a stupidly obscure title. It's meant to represent the combined events: <a href="https://2026.stateofthebrowser.com/">State of the Browser</a> and <a href="https://webdayout.com/">Web Day Out</a> - two events I attended in the last month.</p>
<p>The short version is: if you get the chance to attend these events or even <em>anything</em> similar, I'd highly recommend that you grab that ticket and let the event wash over you.</p>
<p>For those that didn't attend (or maybe you did and wanted to read my perspective) then here's my thoughts on the separate events and then the round up of my own experience.</p>
<h2>State of the Browser</h2>
<p>London based and fronted by the lovable <a href="https://letorey.co.uk/">Dave Letorey</a>, always has a strong accessibility story - both from a practical (&quot;how to make the web accessible&quot;) but also universal access - in that all are welcome and an open web is championed through the talks and the heart of the event.</p>
<p>I tried to take a few notes for myself during the day, but this quickly fell to the wayside by midday (I find if I'm taking notes I'm slowly losing track of the talk, so it was more important at the time to stay in the moment).</p>
<p>The day did start with <a href="https://www.bram.us/">Bramus Van Damme</a> with an excellent dive into CSS anchor position, an exciting new feature of CSS that's landing throughout browsers that means we can move a lot of complex JavaScript into the bin. The CSS spec itself it's wholly simple, and there's a blind spot specifically around recreating call out arrows (usually done using rotated weird border magic).</p>
<p>I do question the ease of testing - though I think a talk on how to test CSS (with launching a browser and diffing the screenshot) would be extremely valuable.</p>
<p>Another talk by <a href="https://www.linkedin.com/in/fionasafari/">Fiona Safari</a> on how they learnt to work with their introvertness(?) left me feeling very &quot;seen&quot;, though <a href="https://www.kryogenix.org/days/">Stuart</a> did raise the valid point that this felt a little like the <a href="https://en.wikipedia.org/wiki/Barnum_effect">Barnum effect</a> - where you're able to recognise yourself in most things (we're human after all, and patterns and self interest is programmed deep in us).</p>
<p>This quote really meant a lot to me, and I think about kids who don't push themselves to the front of the class - this is one to remember:</p>
<figure><img src="https://remysharp.com/images/introvert.avif" alt="Being an introvert &amp; extrovert had nothing to do with confidence, social skills or personality flaws - Carl Jung" decoding="async"></figure>
<p><a href="https://www.zachleat.com/">Zach Leatherman's</a> talk looking at how a common picture compare component would and could be developed from the day the first <code>img</code> element was released to today. Especially drawing attention to the &quot;dead zone&quot; where code has landed in the browser yet the interactivity isn't at all ready.</p>
<p>The time to interactivity is a metric I think (I hope), we're thinking of all the time, but to see it articulated and in the context of a relatively contained context really helped - especially considering that webbies have to go back to work and argue the case.</p>
<p>Then <a href="https://jason-williams.co.uk/">Jason Williams</a> gave a wonderful context to how <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal">Temporal</a> came about and the sheer scale of the work.</p>
<p>I've spent my fair share of bemoaning the JavaScript date object (since '99, so yeah, I've been at the sharp end of <code>Date</code> suckage!), so learning about the mammoth effort that went into bringing a completely new date structure was a real treat.</p>
<p>I've been using Temporal for a good few years via a polyfill, but to learn there's so much more <em>and</em> it's arriving cross browser (and server-side) is valuable.</p>
<p><a href="https://www.linkedin.com/posts/kitation_the-plateau-of-accessibility-compliance-activity-7434924356574752768-Szon/">Chad Gowler's accessibility talk</a> was absolutely superb and I do hope they have the opportunity to give the talk again at more events - so many people can learn from these experiences.</p>
<p>Likewise with <a href="https://bsky.app/profile/mikehall314.bsky.social">Mike Hall's</a> story of battling for every single byte for performance in a time when it really, <strong>really</strong> mattered (and in some senses, still does). Mike's always a pleasure to listen to (both as a speaker but also his podcast).</p>
<p>I'll be there for next year's <a href="https://2026.stateofthebrowser.com/2027/">State of the Browser</a></p>
<figure><img src="https://remysharp.com/images/dave-sotb.avif" alt="Dave sharing the news of State of the Browser 2027" decoding="async"></figure>
<h2>Web Day Out</h2>
<p>My general feeling from (the first) Web Day Out was that there was a strong sense of practical takeaway from the talks.</p>
<p>I'm hoping the slides will be available (if they're not already) because there's lots of little details in many of the talks that I definitely felt like I could apply today.</p>
<p>All of the talks from Web Day Out were ace quality, reflecting the wonderful speaker line up.</p>
<p><a href="https://www.jemimaabu.com/">Jemima Abu</a> very much set the starting tone for the day. A taste of new and exciting CSS features (touching on some of the items from Bramus at State of the Browser), and speaking to the solid idea that there's technology that's moved out of the JavaScript cycle deeper into the platform, either available in CSS or event HTML itself (such as <code>popup</code>). Giving me even more reason to <a href="https://remysharp.com/2026/01/13/bytes-i-can-delete-after-all-this-time">eject bytes</a> from my code.</p>
<p>The original Lady of CSS, <a href="https://rachelandrew.co.uk/">Rachel Andrew</a> talked about what <a href="https://web.dev/baseline">Baseline</a> means to development and talked about the very practical nature of support.</p>
<p>One incredibly interesting item that I've not heard others mention: Rachel points out that given the nature of the data collected on Baseline support, it's now possible to accurately predict browser support by a given amount of time. Specifically, if a feature moves to &quot;newly available&quot; today, it will land in &quot;widely available&quot; in 30 months. This might sound like a long time, but Rachel makes the excellent point that developers working on a year-long+ project can use features that may not have wide support from the outset, but they'll be able to pick features based on expected support at the time they go live.</p>
<p>Rachel did also talk about how the Baseline project, in an early form, did consider whether to include <a href="https://remysharp.com/2010/10/08/what-is-a-polyfill">polyfills</a> as part of the support timelines, but the search for a &quot;gold standard&quot; of polyfill was…elusive. For my part, personally, I wouldn't want or expect to see polyfills being stamped with approval by browsers. As Rachel said herself, the requirements for polyfills (and what's considered Baseline) is entirely dependant on your own projects and products.</p>
<figure><img src="https://remysharp.com/images/rachel_polyfill.avif" alt="Rachel stands in front of a slide that reads: The search for gold standard polyfills - the perfect polyfill is hard to find" decoding="async"></figure>
<p>Next was <a href="https://alethgueguen.com/">Aleth Gueguen</a> who shared their story of development and testing directly from the trenches - figuratively <em>and</em> literally. In the post-social a good number of people shared how much they appreciated the insights saying how they'd love to hear more real-life stories, as often it's the real development and challenges overcome that make an interesting story (echo'ing Mike Hall's story from State of the Browser).</p>
<p>Then <a href="https://csswizardry.com/">Harry Roberts</a> took the stage to talk outside of his usual stomping ground. That's to say, instead of performance and measurements he instead spoke to the impact of <em>not</em> building for the web. He shared practical examples of where (anonymised) clients had brought Harry in to work on their performance issues whilst they grappled with framework upgrades. It's a strange web-world we live in when the typical path development path is to reach for Next/Nuxt/Fux/Whux/evar because everyone is does and laden the users with bloat.</p>
<p>Harry had many excellent points during his talk, one that comes to mind as I write today was how optimising for 2nd page load with a coach load of JavaScript, when the bounce rate is so high is premature optimisation (pause for feigned shock) - which ultimately costs negatively.</p>
<p>This wasn't Harry's normal wheelhouse of talk - by his own admission - but an excellent one, and I hope he gets to repeat it for others.</p>
<p>Post lunch, was <a href="https://matuzo.at/">Manuel Matuzovič</a> sharing his newly gained knowledge from questioning if what he knew 3 years ago still stands, and modifying and improving the tech he was using - squarely in CSS land. He used his own pet project <a href="https://olicss.com/">oli.css</a> as a place to experiment (I <strong>strongly</strong> approve - always find a safe place to play), and in particular shared his really interesting take on a CSS &quot;reset&quot; (not a reset per se, because browsers have managed to converge on a solid starting point, but a great starter CSS) and how he layers and reuses throughout the project.</p>
<p>Keeping in this space, which <a href="https://adactio.com/">Jeremy</a> made clear he had curated the talks and order into these excellent pairings, <a href="https://clagnut.com/">Richard Rutter</a> talked us through what had changed in (web) font typography in the last 9 years since he published <a href="https://book.webtypography.net/"><em>The</em> Web Typography book</a>.</p>
<p>I'll be honest, I really didn't think the web type scene was either interesting or even moving. But by gosh there was a LOT. Tonnes of interesting tweaks that we can be making here and today, lots of features that were in Baseline, others that were coming. Of all the talks, Richard's had the most &quot;oooh - I really need to pinch this for my website&quot; slides. I'm hoping they'll be available (and I remember to find it) because it really was a treasure trove.</p>
<p>In the final straight was <a href="https://jakearchibald.com/">Jake Archibald</a> taking a zoomed in view on the customisable <code>select</code> element that is landing (or <em>landed</em>). A (typically) hilarious talk where he went all the way back to 1993 and went through all the events that needed to take place before developers (finally) laid their hands on a stylable select - whilst also showing us <em>how</em> to style the select today.</p>
<p>Highly enjoyable and difficult to ignore the contents of his slides! 😂</p>
<figure><img src="https://remysharp.com/images/jake_select.avif" alt="Jake's slide reads &quot;Look, this is just some placeholder text. You should be listening to whatever I'm saying rather than reading this. Seriously, stop! I might say something interesting. I mean, I hope I say something interesting. Oh god what if no one finds what I'm saying interesting? I'm going to have a panic attack.&quot;" decoding="async"></figure>
<p>Finally <a href="https://lolaslab.co/">Lola Odelola</a> talked about their experience approaching and implementing a missing browser feature - a fallout from their talk at State of the Browser in 2024 asking the question: <em>should there be a <code>prefers-alt-text</code> CSS feature?</em>. Very much from the land of how-the-sausage-is-made and makes me twitch to want to have a crack myself (lol - if I ever had time).</p>
<h2>What I came away with</h2>
<p>Look, if you've been to even a handful you'll already know it's the people between the talks that makes it all worthwhile. It's always lovely when there's talks that give me a little shove towards tinkering with code, doubly so if it's a friend up on stage (new or old) giving the talk.</p>
<p>For the first time in a long while I attended the post-event parties (I tend to miss out on Brighton or London event parties because home calls). In doing so I had some really, really lovely conversations with different people (though landing myself chats finishing at 3am and 1am respectively 🤦).</p>
<hr>
<p>As I sit in these events, I'm thinking about what I want from FFConf. A conundrum I face is that as I increasingly follow politics, the more I want to share what I learn about the world.</p>
<p>At some point during the last of the talks I realised actually what this web community is, is a microcosm of exactly the kind of progressive community that I identify with. A community that cares about representation, dignity for others, equality and sharing.</p>
<p>I've always known I was happier in (what I called) the hippy nation of webbies, but it makes me realise actually what we're doing in the corner of the world can be applied to how we conduct ourselves in the broader sense - and actually the politics I follow these days are very much aligned with the politics that I care about inside of the web community.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/03/18/web-of-state-of-the-browser-day-out">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>git recent: what branch did I work on?</title>
      <guid isPermaLink="false">git-recent</guid>
      <link>https://remysharp.com/2026/02/12/git-recent</link>
      <pubDate>Thu, 12 Feb 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[Mega short blog post, mostly for me to remember, but also might be useful to you.
In a project I'll often work on and move around different branches throughout the day, and as the years wear on it's rather dulled my memory - that's to say, I quickly forget what branch I was working on!]]></description>
      <content:encoded><![CDATA[
<p>Mega short blog post, mostly for me to remember, but also might be useful to you.</p>
<p>In a project I'll often work on and move around different branches throughout the day, and as the years wear on it's rather dulled my memory - that's to say, I quickly forget what branch I was working on!</p>
<h2><code>git recent</code></h2>
<p>I created a <code>git</code> command line alias that helps my failing memory. What it does is list all the local branches, oldest to newest with the relative time and the name:</p>
<pre><code>[alias]
  recent = !git for-each-ref --sort=committerdate --format='%(committerdate:relative) %(refname:short)' refs/heads/ | tail -10
</code></pre>
<p>It's a bit cumbersome with the <code>!git</code> part at the start, but it's because I want to limit it the most recent 10 results <em>and</em> I want it with the newest at the bottom.</p>
<pre><code>$ git recent
3 weeks ago fix/ai-gen-docs
2 weeks ago feat/not-available-not-404
2 weeks ago fix/bulk-download-timeout
8 days ago fix/real-null-in-bulk
8 days ago fix/real-404
2 days ago feat/bulk-schema
2 days ago fix/transcript-endpoint-potential-null
2 days ago fix/sanitise-sql-input
21 hours ago main
20 hours ago feat/api-analytics
</code></pre>
<p>Memory win for me. Neato.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/02/12/git-recent">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>JS Bin down in 2026</title>
      <guid isPermaLink="false">js-bin-down-in-2026</guid>
      <link>https://remysharp.com/2026/02/02/js-bin-down-in-2026</link>
      <pubDate>Mon, 02 Feb 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[January 27th I got an email notification saying that JS Bin had become unavailable. Then next day real life human beings were asking what's going on. By 11pm on the 30th the last of the issues were resolved.
Earlier today Jake asked me: what went wrong?
Fucking, everything.]]></description>
      <content:encoded><![CDATA[
<p>January 27th I got an email notification saying that JS Bin had become unavailable. Then next day real life human beings were asking what's going on. By <a href="https://github.com/jsbin/jsbin/issues/3583#issuecomment-3826171066">11pm on the 30th</a> the last of the issues were resolved.</p>
<p>Earlier today <a href="https://jakearchibald.com/">Jake</a> asked me: <em>what went wrong?</em></p>
<p><strong>Fucking, everything.</strong></p>
<h2>TL;DR</h2>
<p>I get it, this is a big wordy blog post. I got carried away and enjoyed telling the story.</p>
<p>The short version is: CloudFlare, probably best to upgrade everything to latest software (which I couldn't do), 520 status can actually be a mismatched TLS exchange between CloudFlare and the origin. Oh, and don't lean on LLMs too hard when the shit is really hitting the fan - try to take a big step back and make sure you take stock.</p>
<p>Otherwise, here's the too-long, do-read version -</p>
<h2>On being in maintenance mode</h2>
<p>For the last 5 ish/maybe quite a few years or more, JS Bin has run in a fairly automated maintenance mode. There's usually little flurries of wobbles that need my attention every 3/6 months. JS Bin is coming on to 18 years old, which is geriatric by web standards.</p>
<p>Typically it's <a href="https://remysharp.com/2015/09/15/jsbin-toxic-part-2">dodgy content</a> on the site that I need to put banning in place, sometimes it's take down requests that come through from Amazon (where JS Bin is hosted), sometimes it's a memory exception that takes me a little longer to recover from.</p>
<p>You can see from the last 11 years of status checks, there's been outages but nothing quite like what happened this time around (ignoring that big one on the far left…):</p>
<figure><img src="https://remysharp.com/images/jsbin-status-chart.avif" alt="A chart starting from 2014 with spots throughout showing when JS Bin was down" decoding="async"><figcaption>In 2017 the entire server fell over and need to be replaced entirely. Then this were quiet-ish, then…we have the red wall</figcaption></figure>
<p>The larger hours when the server goes down (as I found out recently) is memory running out on the machine, and the machine will respond by pretty much collapsing. The result of which is that I can't connect to the machine via ssh to attempt restore it. It usually requires a forced reboot via the AWS console.</p>
<p>Then there's the odd occasion that even reboots from the AWS console, a literal &quot;turn it on and off again&quot;, doesn't fly…</p>
<h2>It wouldn't come back from a reboot</h2>
<p>This latest outage just wouldn't come back from a reboot. I would trigger the reboot and then wait on the console attempting to connect over SSH so I could get some eyes on the situation but even then I couldn't get it.</p>
<p>JS Bin was rebooting and immediately locking up. So this tells me there's some external pressure on the machine that is not easing up.</p>
<p>The only option I have to hand is to shutdown the machine entirely for an hour or so, to let whatever is clawing at the door to go away for a bit.</p>
<p>When I eventually looked at the Cloud Watch logs, it was clear there was something absolutely smashing at the walls (and door, yes, I mix my metaphors). The amount of inbound network traffic is unprecedented for jsbin. From the chart below, it shows normal usage in the days prior, a trickle of network inbound traffic by comparison to what I was now seeing.</p>
<p>The dips that happen after the cliff edge of monitoring is the machine giving up and being unresponsive. From the chart, I can see that even the short outage wasn't enough to get this beast off jsbin's back.</p>
<figure><img src="https://remysharp.com/images/cloud-watch-chart.avif" alt="The Cloud Watch network traffic logs showing a huge increase of network inbound traffic, going from a typical 1mb to 100mb" decoding="async"><figcaption>The 100mb peak <em>incoming</em> traffic was the real problem.</figcaption></figure>
<h2>Killing the appropriate process</h2>
<p>When I finally managed to get into the machine, the first stop was <code>syslog</code> to find out what was responsible for the crash (or rather the symptom, not the cause, not yet).</p>
<p>I quickly found the garbage collection dump and stacktrace of node running out of memory. First thing's first: let's kill the offending process rather than having a full shit-the-bed approach.</p>
<pre><code># /etc/sysctl.conf and reload with `sudo sysctl -p`
vm.oom_kill_allocating_task=1
</code></pre>
<p>That would mean node (which was saturating all the memory) would be killed rather than anything the system could get it's hands on - which usually meant I couldn't ssh into the machine.</p>
<p>This change wouldn't stop things getting sluggish on the machine but it would mean that I could continue to diagnose even whilst the traffic was bombarding the server (albeit with a very slow terminal responding).</p>
<p>I could see the CPU usage was very, very high, and I could see node steadily increasing it's memory footprint (I use <code>htop</code>).</p>
<p>CPU usage of 100% is…okay because it means I'm using all the available process, but it leaves zero headroom and rather conspicuous that it wasn't dipping at any point.</p>
<p>Then chatgpt suggested upgrading node, which was weird because I'd never told Chatgpt what version of node I was running…</p>
<h2>Side quest: node is <em>really</em> old</h2>
<p>As with running in maintenance and things being in a status quo, I hadn't really touched node…at all.</p>
<p>As it happens, jsbin had been running on node 7 (not even &quot;stable&quot; node 8) for, I imagine, well over a decade.</p>
<p>To me, out of the blue, Chatgpt suggested I could upgrade node. This is a decent suggestion but at no point did I tell Chatgpt what version of node I was running.</p>
<p>On me querying, Chatgpt told me that <em>I had told the AI</em> which was a straight up lie.</p>
<p>After more prodding than I'd like, eventually it turned out that the version of node was in my terminal and at some point during the nginx tuning (in the next section), Chatgpt had helped itself to what was on the screen.</p>
<p>I've no idea if it has access to the scroll back history - I've got to assume it doesn't, but still pretty uncool.</p>
<p>I don't tend to use the &quot;application&quot; thing that Chatgpt has (I normally use the browser interface) but this definitely taught me a lesson: if there's sensitive data <em>anywhere</em> on the screen (ie. I had just <code>cat</code> an <code>.env</code> file) then it's entirely likely an LLM can see it too.</p>
<p>Anyway, I bumped from node 7 to node 22 and by some kind miracle it actually worked without any incident. It turned out that back in 2024 I had done some fairly significant work on the codebase so that I could run it on my local machine (which understandably didn't want to run node 7), so I had modernised the requisite parts.</p>
<p>Phew. At least the event loop is improved and going to be kinder on my CPU.</p>
<p>Except, it's still fucked and the adventure is far from over. Next I considered whether nginx (the proxy layer) could do with some optimisation.</p>
<h2>Fine tuning on zip-all resources</h2>
<p>Until now I've not shared what jsbin's main server runs on. It's a t2.micro AWS instance. Single CPU and 1GB of RAM. I'm often surprised it's managed so long on what I consider so little resources.</p>
<p>And yes, standing a beefier machine up (aka: throwing money at the problem) might help, but the time to reconfigure a new machine wasn't quick - I don't have an instant &quot;build a new jsbin&quot; script (remember, this server has been in maintenance for a long time and just happily running). It's also worth adding that although jsbin does has a &quot;pro&quot; offering, there's very little actual monetary resources. That's all to say: what can I do right now before creating a new machine (spoiler: I did make a new machine, and double-spoiler: it was terminated the following day as I didn't need it).</p>
<p>With the help of Chatgpt, Gemini <em>and</em> Claude (because somehow I have access to all of them, and I really don't have the full skills to know the ins and outs of nginx config) I looked at what could be tuned. I used multiple LLMs so that I could attempt to cross verify the advice (though I wasn't as diligent as I'd have liked).</p>
<p>The adjustments fell into these categories:</p>
<ul>
<li>workers spawned</li>
<li>proxy timeout</li>
<li>increase file descriptors</li>
<li>keep alive timings</li>
<li>remove http2 - to help with memory</li>
</ul>
<p>Here's some of the actual config:</p>
<pre><code class="language-yaml">worker_connections 1024;
worker_processes auto;

keepalive_timeout 10;
keepalive_requests 100;
</code></pre>
<p>With the kicking I was getting from the inbound network traffic, this made little tangible difference. I'm sure it helps in the long run, but not when the machine is struggling with over 1000 requests a second and many more trying to squeeze in.</p>
<p>And then there was this:</p>
<blockquote>
<p>Remy, have you considered CloudFlare?</p>
</blockquote>
<p>I'd thought about add CloudFlare a few times in the past, but I was worried that the configuration adjustment and changes I'd need would be either complicated or would cause some other issue.</p>
<p>Except, nothing was working, so now was the time to get CloudFlare involved.</p>
<p>And so begin new problems…</p>
<h2>Adding CloudFlare</h2>
<p>Props where due, putting CloudFlare in front of JS Bin was relatively easy. CloudFlare detected <em>most</em> of the domains and where they pointed (the important ones at least) and it was a matter of swapping over the name servers from pointing to AWS's Route 53 to CloudFlare's own name servers.</p>
<p>Close to midnight on 29-Jan, I started to see <a href="http://jsbin.com">jsbin.com</a> load in the browser.</p>
<p>I thought there might be a few snags around, but quickly I got replies on github and via email that people were still (mostly) seeing errors.</p>
<p>In particular <a href="https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-520/">CloudFlare's 520</a> error page, which, as it turns out can be any number of issues.</p>
<h2>Requests cutting through CloudFlare</h2>
<p>Although I did have some success in loading, I could also see traffic still hammering through on the server.</p>
<p>With some help from our LLM overlords (i.e. I would have google'd it…&quot;<a href="https://kagi.com/">kagied</a> it&quot;?), I got the <a href="https://www.cloudflare.com/en-gb/ips/">list of IP ranges</a> that meant the traffic was from CloudFlare.</p>
<p>There was still a lot of traffic that didn't match those IP ranges. So the next step was to drop non-CloudFlare traffic.</p>
<p>This is where using the LLM cost me time via newly introduced problems (which wouldn't surface until the next day). I think I was too buried in the thick of things otherwise I might have caught it earlier (and not at the end of the process).</p>
<p>The first method to blocking non-CloudFlare was to use an nginx rule and variable, and to drop all traffic that wasn't tagged.</p>
<p>This would mean adding this line to each <code>server {}</code> block - this would say <em>&quot;If the request doesn't have the CloudFlare header, then don't let it through&quot;</em>:</p>
<pre><code>if ($http_cf_ray = &quot;&quot;) {
    return 444;
}
</code></pre>
<p>Importantly there was more config directions that I followed (which I've tucked away, but you can read if you want), specifically <code>set_real_ip_from 173.245.48.0/20;</code> - this would trip me up later on.</p>
<details class="aside"><summary>The http_cf_ray config</summary>
<p>Via Captain GPT:</p>
<h4>The correct nginx-level approach (safe + reversible)</h4>
<h5>1. Tell nginx which IPs belong to Cloudflare</h5>
<p>Create a file:</p>
<pre><code>/etc/nginx/cloudflare.conf
</code></pre>
<p>Put exactly this in it (current CF IPv4 ranges):</p>
<pre><code>set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

real_ip_header CF-Connecting-IP;
real_ip_recursive on;
</code></pre>
<p>Include it near the top of <code>http {}</code>:</p>
<pre><code>http {
    include /etc/nginx/cloudflare.conf;
    ...
}
</code></pre>
<p>This ensures $remote_addr becomes the real client IP, not Cloudflare.</p>
<h5>2. Block non-Cloudflare traffic (this is the key)</h5>
<p>In each public <code>server {}</code> block (ports 80 and 443), add this at the very top:</p>
<pre><code>if ($http_cf_ray = &quot;&quot;) {
    return 444;
}
</code></pre>
<p>What this does:</p>
<ul>
<li>Cloudflare always sends CF-Ray</li>
<li>Direct clients never do</li>
<li>444 silently drops the connection (no response, no load)</li>
</ul>
<p>This is extremely effective.</p>
</details>
<p>This config was all well and good, but traffic was still coming in. This change would mean that nginx would have to process the traffic than ignoring (or &quot;dropping&quot;) it entirely (<em>#foreshadowing</em>).</p>
<p>When I ran command below, it would list the connections, and I could <em>still</em> see non-CloudFlare requests being able to complete request (at this specific point in time, I was in a frantic cycle of &quot;try a change - fail - worry a bit - ask LLM - repeat&quot;):</p>
<pre><code>ss -tan state established '( sport = :443 )'
</code></pre>
<p>The next change that I put in place was to drop the request if the IP didn't match the ones in the CloudFlare IP range (whereas before the change was looking for a CloudFlare specific header). Looking back as I write this, I'm not sure why the header method didn't work, but I was sure to add more petrol onto the fire:</p>
<pre><code>geo $is_cloudflare {
    default 0;

    173.245.48.0/20    1;
    103.21.244.0/22    1;
    # etc
}
</code></pre>
<p>This flags if the remote address is a CloudFlare IP, set the <code>$is_cloudflare=1</code>. Then in my server blocks:</p>
<pre><code>server {
    listen 443 ssl http2 default_server;
    if ($is_cloudflare = 0) { return 444; }
    if ($cf_valid = 0) { return 444; }

    # rest of config unchanged
}
</code></pre>
<p>What I didn't realise at this point, and took another 24 hours to figure out, is that the first CloudFlare nginx based change would say <em>&quot;if the IP is CloudFlare, then set the $remote_addr to the original request&quot;</em>. Then this code says <em>&quot;if the IP is NOT CloudFlare don't let them through&quot;</em>.</p>
<p>It's a mess. Now I've got reports from people on github telling me they're still seeing <em>mostly</em> 520 errors, and finally I realise I can replicate by using a VPN because for some reason it was still working for me.</p>
<p>There's two problems at this point:</p>
<ol>
<li>Traffic still coming in that isn't through CloudFlare that's causing a drain on resources</li>
<li>Real users coming through CloudFlare are, mostly, not getting the site</li>
</ol>
<p>Pretty much the worst of both worlds.</p>
<p>Finally, firewall rules spring to mind. Better late than never.</p>
<h2>Dropping traffic</h2>
<p>This was done on two fronts - just for <em>belt-and-braces</em> approach:</p>
<ol>
<li><code>ufw</code> (or as I knew it &quot;iptables&quot;) rules one the server to <code>DROP</code> or <code>ALLOW</code> traffic based on IP range</li>
<li>AWS security policy to do the same.</li>
</ol>
<p>Running the rules for <code>ufw</code> was relatively simple. A matter of allowing all the known IP ranges, then denying everything else:</p>
<pre><code class="language-shell">$ ufw allow from <span class="token number">173.245</span>.48.0/20 to any port <span class="token number">443</span>
$ ufw allow from <span class="token number">103.21</span>.244.0/22 to any port <span class="token number">443</span>
$ <span class="token comment"># etc</span>
$ ufw deny <span class="token number">443</span>
</code></pre>
<p>Then repeated for port 80. I did get tripped up during testing when I wanted to allow my IP through, but that was solved with <code>ufw status numbered</code> and then <code>ufw delete N</code>.</p>
<p>Adding all the IP ranges on AWS was not so simple. The web UI just doesn't allow for large changes - it's very, very clunky.</p>
<p>Thankfully I could automate some of it from the <a href="https://aws.amazon.com/cli/">command line</a> and script the work. The annoying thing was that the AWS command line doesn't let me bulk modify (or at least I didn't find it) and each command takes a good few seconds to run <em>and</em> requires me to press enter after the response comes back saying it had worked.</p>
<p>This was the command:</p>
<pre><code class="language-sh"><span class="token keyword">for</span> <span class="token for-or-select variable">CIDR</span> <span class="token keyword">in</span> <span class="token punctuation">\</span>
   <span class="token number">103.21</span>.244.0/22 <span class="token punctuation">\</span>
   <span class="token number">103.22</span>.200.0/22 <span class="token punctuation">\</span>
   <span class="token number">103.31</span>.4.0/22 <span class="token punctuation">\</span>
   <span class="token comment"># etc</span>
<span class="token keyword">do</span>
   aws ec2 authorize-security-group-ingress --group-id <span class="token variable">$SG_ID</span> <span class="token parameter variable">--protocol</span> tcp <span class="token parameter variable">--port</span> <span class="token number">80</span>  <span class="token parameter variable">--cidr</span> <span class="token variable">$CIDR</span> <span class="token parameter variable">--region</span> us-east-1
   aws ec2 authorize-security-group-ingress --group-id <span class="token variable">$SG_ID</span> <span class="token parameter variable">--protocol</span> tcp <span class="token parameter variable">--port</span> <span class="token number">443</span> <span class="token parameter variable">--cidr</span> <span class="token variable">$CIDR</span> <span class="token parameter variable">--region</span> us-east-1
<span class="token keyword">done</span>
</code></pre>
<p>With those in place, finally the server was breathing again. However, those pesky CloudFlare 520s were still preventing people from visiting <a href="http://jsbin.com">jsbin.com</a> (except me…somehow…).</p>
<h2>CloudFlare's 520</h2>
<p>I naively thought 520 was like a 503 (the entire server is failing to respond to CloudFlare) or a 504 (gateway timeout - usually when node doesn't come back to nginx or CloudFlare), but it's not. It's more like <em>&quot;CloudFlare made a request and the response is incompatible&quot;</em>.</p>
<p>The only real clue I had was that port 80, plain http wasn't affected. A <a href="https://github.com/jsbin/jsbin/issues/3583#issuecomment-3823361117">helpful comment from @robobuljan</a> showed that it was only the https version causing issues:</p>
<pre><code class="language-sh">$ <span class="token function">curl</span> jsbin.com             <span class="token comment"># (works!)</span>
$ <span class="token function">curl</span> http://jsbin.com      <span class="token comment"># (works!)</span>
$ <span class="token function">curl</span> https://jsbin.com     <span class="token comment"># "error code: 520"</span>
</code></pre>
<p>Although this digging took most of the day, it was the part that the LLMs really couldn't help with (and I had thankfully set them aside whilst I chewed on the meat of this problem).</p>
<p>As I looked for any clues. In the CloudFlare SSL/TLS page a section called &quot;Traffic Served Over TLS&quot; showed that there was a split in the supported TLS versions (I didn't capture a screenshot, but these numbers are from their API):</p>
<ul>
<li>TLSv1: 36 requests</li>
<li>TLSv1.1: 56 requests</li>
<li>TLSv1.2: 1,922,523 requests</li>
<li><strong>TLSv1.3: 5,216,795 requests</strong></li>
</ul>
<p>That's a lot on TLSv1.3, but I wondered how old (or actually new) v1.3 was given how old my rickety machine was. So I went back to my nginx config and found this line, repeated against each server block that ran SSL:</p>
<pre><code>ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
</code></pre>
<p>A conspicuous lack of TLSv1.3, and I wondered what would happen if the user asked for 1.3, CloudFlare tottered off wanting 1.3 and then got back 1.2… or perhaps nothing at all.</p>
<p>I first tried to add <code>TLSv1.3</code> to the list only for the <code>nginx -t</code> test to fail (the module wasn't installed and I couldn't get it without doing major upgrade work). So let's see if it can be turned off in CloudFlare.</p>
<p>The short answer is yes, but I struggled to find it.</p>
<p>The first place to confirm what's being support is the section under &quot;Speed, Settings&quot;. But to completely turn it off, it's located in &quot;SSL/TLS&quot; then &quot;Edge Certificates&quot; and towards the end of the page.</p>
<p>Disabling this unlocked a lot more genuine traffic and we were cooking again. Almost, nearly. Again, for some, now the static assets weren't loading nor was the frame that actually live runs the authored code (the <code>null.jsbin.com</code> domain).</p>
<h2>JS Bin was dropping some users</h2>
<p>It took me a few more hours to solve this last step, and I'm still not entirely sure how it all hung together. But if you recall, earlier in my nginx config, I had told nginx that if the IP of the request was from CloudFlare that we should use <code>set_real_ip_from</code>.</p>
<p>This specific command was writing the original IP of the requesting user into the <code>$remote_addr</code>, which is what nginx was now using to drop requests with the <code>return 444</code>. Somehow this wasn't in the main server block (the part that returns the index page), but was somehow in the <code>static.jsbin.com</code> and <code>null.jsbin.com</code>.</p>
<p>This kind of confusion is the result of working late hours, and working in crisis mode. I should have known better and I'm great at dolling out advice but sometimes don't tend to heed it myself.</p>
<p>After I finally removed the janky checks, IP swapping and quite a bit of the cruft that I had introduced with my pal ChatGPT, this let the last of the traffic come through correctly.</p>
<p>JS Bin was back. Entirely.</p>
<h2>The aftermath</h2>
<p>Now that CloudFlare is in front of the server, life is surprisingly chill on this 1GB single CPU machine. Way more calm than a normal day of traffic. Look at that CPU usage!</p>
<figure><img src="https://remysharp.com/images/jsbin-htop-2026.avif" alt="A screenshot of the &quot;htop&quot; program showing that the CPU is around 5% and memory usage is about 30% consumed" decoding="async"><figcaption>Just chilling out at an easy 4.6% CPU usage</figcaption></figure>
<p>I suspect if I hadn't leant on the LLMs so much during the scream-face-the-server-is-down moments, I might have caught the complexity that I was adding. Though equally I should have put CloudFlare in front of JS Bin years ago - and not at a time of crisis.</p>
<p>I definitely learnt a few gotchas, the TLS and 520 status codes are that for me.</p>
<p>The traffic has eased out from the AWS CloudWatch logs and I believe CloudFlare is now bouncing a lot of that away from me - and it seems like a lot of that is coming from Hong Kong (which I've set to have a JavaScript based check to get through CloudFlare):</p>
<figure><img src="https://remysharp.com/images/jsbin-hong-kong.avif" alt="" decoding="async"><figcaption>Hong Kong is so small in this map, you can't even see the deep blue of the chart</figcaption></figure>
<p>Since I took that screenshot, Hong Kong has 10 million requests in the last 24 hours.</p>
<p>Even though I don't think I'll ever really know what caused the amount of traffic that caused everything to fall over, my gut suspects scrapers for AI and LLMs just slurping up as much of the web it can. The only evidence I have <em>against</em> that theory is that the traffic didn't come from a single IP.</p>
<p>Whereas, weirdly, I saw a <a href="https://bsky.app/profile/remysharp.com/post/3mb55uwp3vk2j">single IP bot scraping this blog</a>, accounting for over 3GB of data and over 325,000 requests in a matter of hours. Thankfully this was running on Netlify and is entirely static, and not running on node 7 🤦.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/02/02/js-bin-down-in-2026">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Bytes I can delete after all this time</title>
      <guid isPermaLink="false">bytes-i-can-delete-after-all-this-time</guid>
      <link>https://remysharp.com/2026/01/13/bytes-i-can-delete-after-all-this-time</link>
      <pubDate>Tue, 13 Jan 2026 10:00:00 +0000</pubDate>
      <description><![CDATA[For the last few years my work-work has mostly focused on back end software (particularly around APIs). This meant that any front end work I was doing was for myself.
Being an long-in-the-tooth old dog, I tend to learn and trick, and roll it out again and again typically without taking the time to find whether I still need the trick. Case and point, I learnt about the JavaScript performance trick of ~~1.4 === 1 to floor a value (and the same float | 0) but really these days it's not &quot;faster&quot; than doing it the legible way (i.e. Math.floor(1.4)).
Given I've had a bit of time away from the backend, here's an unorganised list of things I've found I can use, and thusly remove extra code that I no longer need.]]></description>
      <content:encoded><![CDATA[
<p>For the last few years my work-work has mostly focused on back end software (particularly around APIs). This meant that any front end work I was doing was for myself.</p>
<p>Being an long-in-the-tooth old dog, I tend to learn and trick, and roll it out again and again typically without taking the time to find whether I still need the trick. Case and point, I learnt about the JavaScript performance trick of <code>~~1.4 === 1</code> to floor a value (and the same <code>float | 0</code>) but really these days it's not &quot;faster&quot; than doing it the legible way (i.e. <code>Math.floor(1.4)</code>).</p>
<p>Given I've had a bit of time away from the backend, here's an unorganised list of things I've found I can use, and thusly remove extra code that I no longer need.</p>
<h2>The list</h2>
<ol>
<li>CSS: <code>text-underline-offset</code> - the distance I can set the <span style="text-decoration: underline; font-family: 'Ubuntu Mono'; text-underline-offset: 12px">text-underline-offset: 12px</span></li>
<li>CSS: <code>gap</code> - no more faffing with margins in flexbox.</li>
<li>CSS: nested media queries on selectors (and nesting general):</li>
</ol>
<pre><code class="language-css"><span class="token selector">h1</span> <span class="token punctuation">{</span>
  <span class="token property">font-size</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span>
  <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 600px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
    <span class="token property">font-size</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<ol start="4">
<li>CSS: <code>clamp(min, variable, max)</code> - I used this extensively on FFConf 2025's <a href="https://github.com/leftlogic/ffconf2025/blob/fdb687636450058afbfb67c846a32667bdbe6c8e/style.css#L62">css</a>, the process of finding the right values is very much</li>
<li>CSS: <code>content: open-quote</code> can localised quotes <em>and</em> the <code>q</code> tag does this by default, <a href="https://www.stefanjudis.com/today-i-learned/how-to-use-language-dependent-quotes-in-css/">via this neat insight from Stefan Judis</a></li>
<li>JS: <code>catch</code> without catching the variable, let's me get past the <code>'error' is defined but never used</code>:</li>
</ol>
<pre><code class="language-js"><span class="token keyword">try</span> <span class="token punctuation">{</span>
  <span class="token function">doTheDodgyStuff</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">{</span>
  <span class="token comment">// nothing, it's fine</span>
<span class="token punctuation">}</span>
</code></pre>
<ol start="7">
<li>JS: pointer events have improved (though still from experience they're not 100% perfect) but are to replace the old double click &amp; touch handlers nonsense as seen in <a href="https://www.w3.org/TR/pointerevents/#example_1">the W3C spec</a></li>
<li>AVIF images are fully supported - I benefited from a train ride with <a href="https://jakearchibald.com/">Jake Archibald</a> and in chatting I discovered AVIF are very well supported. This means <a href="https://github.com/remy/remysharp.com/commit/19a32efcb719d1b125017577e48330606ef20ef7#diff-43c10c1d3f992c362c956760385e6ad7397a2345f90c0709e2d4da765ab2d255">easily getting 50% file size savings on JPEGs</a>. I'm regularly running the <a href="https://github.com/AOMediaCodec/libavif/">avidenc</a> command in directories:</li>
</ol>
<pre><code class="language-sh"><span class="token function">ls</span> *.jpg <span class="token operator">|</span> <span class="token function">xargs</span> <span class="token parameter variable">-P</span> <span class="token number">8</span> <span class="token parameter variable">-I</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> avifenc <span class="token parameter variable">-q</span> <span class="token number">50</span> <span class="token string">"{}"</span> <span class="token string">"{}"</span>.avif
</code></pre>
<p>So that's my little list.</p>
<p>Not even 10 things. I guess I've not learnt much yet, but even though I return to the server side of APIs in 2026, I'm sure I'll be kicking the tyres in the front again soon enough and adding to my measly list.</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/01/13/bytes-i-can-delete-after-all-this-time">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Books I read in 2025</title>
      <guid isPermaLink="false">books-i-read-in-2025</guid>
      <link>https://remysharp.com/2026/01/01/books-i-read-in-2025</link>
      <pubDate>Thu, 01 Jan 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[This post is mostly data driven (from my own web site's data) to give me a sense of the quality of the books I've read, otherwise individual reviews are all linked in this post or available on my books page.
Longest book: Empire of AI: Dreams and Nightmares in Sam Altman's OpenAI - 496 pages
Shortest book: The Time Machine - 107 pages
Quickest read: 3 days - The Radleys by Matt Haig (341 pages)
Longest read: 2 months, 2 days - Butter by Asako Yuzuki (464 pages)
Diversity of authors:

Women: 9
Men: 5

Rated books
5 stars

Minority Rule: Adventures in the Culture War - 319 pages

4 stars

Empire of AI: Dreams and Nightmares in Sam Altman's OpenAI - 496 pages
In Bloom (Sweetpea, #2) - 432 pages
Sweetpea (Sweetpea, #1) - 480 pages
The Radleys - 341 pages
The Man Who Died Twice (Thursday Murder Club, #2) - 422 pages
The Echo Wife - 347 pages

Books by decade
1890

1895: The Time Machine by H.G. Wells

1960

1969: The Left Hand of Darkness by Ursula K. Le Guin

2010

2010: The Radleys by Matt Haig
2013: Spike Milligan: Man of Letters by Spike Milligan
2017: Butter by Asako Yuzuki
2017: Sweetpea (Sweetpea, #1) by C.J. Skuse
2018: In Bloom (Sweetpea, #2) by C.J. Skuse
2019: Reasons to Be Cheerful by Nina Stibbe

2020

2021: The Man Who Died Twice (Thursday Murder Club, #2) by Richard Osman
2021: The Echo Wife by Sarah Gailey
2022: The Satsuma Complex (Gary Thorn, #1) by Bob Mortimer
2025: Empire of AI: Dreams and Nightmares in Sam Altman's OpenAI by Karen Hao
2025: Minority Rule: Adventures in the Culture War by Ash Sarkar
2025: Making a Killing (DI Fawley #7) by Cara Hunter]]></description>
      <content:encoded><![CDATA[
<p>This post is mostly data driven (from my own web site's data) to give me a sense of the quality of the books I've read, otherwise individual reviews are all linked in this post or available on my books page.</p>
<p><strong>Longest book</strong>: <a href="https://remysharp.com/books/2025/empire-of-ai-dreams-and-nightmares-in-sam-altmans-openai">Empire of AI: Dreams and Nightmares in Sam Altman's OpenAI</a> - 496 pages</p>
<p><strong>Shortest book</strong>: <a href="https://remysharp.com/books/2025/the-time-machine">The Time Machine</a> - 107 pages</p>
<p><strong>Quickest read</strong>: 3 days - <a href="https://remysharp.com/books/2025/the-radleys">The Radleys</a> <em>by Matt Haig</em> (341 pages)</p>
<p><strong>Longest read</strong>: 2 months, 2 days - <a href="https://remysharp.com/books/2025/butter">Butter</a> <em>by Asako Yuzuki</em> (464 pages)</p>
<p><strong>Diversity of authors</strong>:</p>
<ul>
<li>Women: 9</li>
<li>Men: 5</li>
</ul>
<h2>Rated books</h2>
<h3>5 stars</h3>
<ul>
<li><a href="https://remysharp.com/books/2025/minority-rule-adventures-in-the-culture-war">Minority Rule: Adventures in the Culture War</a> - 319 pages</li>
</ul>
<h3>4 stars</h3>
<ul>
<li><a href="https://remysharp.com/books/2025/empire-of-ai-dreams-and-nightmares-in-sam-altmans-openai">Empire of AI: Dreams and Nightmares in Sam Altman's OpenAI</a> - 496 pages</li>
<li><a href="https://remysharp.com/books/2025/in-bloom-sweetpea-2">In Bloom (Sweetpea, #2)</a> - 432 pages</li>
<li><a href="https://remysharp.com/books/2025/sweetpea-sweetpea-1">Sweetpea (Sweetpea, #1)</a> - 480 pages</li>
<li><a href="https://remysharp.com/books/2025/the-radleys">The Radleys</a> - 341 pages</li>
<li><a href="https://remysharp.com/books/2025/the-man-who-died-twice-thursday-murder-club-2">The Man Who Died Twice (Thursday Murder Club, #2)</a> - 422 pages</li>
<li><a href="https://remysharp.com/books/2025/the-echo-wife">The Echo Wife</a> - 347 pages</li>
</ul>
<h2>Books by decade</h2>
<p><strong>1890</strong></p>
<ul>
<li>1895: <a href="https://remysharp.com/books/2025/the-time-machine">The Time Machine</a> <em>by H.G. Wells</em></li>
</ul>
<p><strong>1960</strong></p>
<ul>
<li>1969: <a href="https://remysharp.com/books/2025/the-left-hand-of-darkness">The Left Hand of Darkness</a> <em>by Ursula K. Le Guin</em></li>
</ul>
<p><strong>2010</strong></p>
<ul>
<li>2010: <a href="https://remysharp.com/books/2025/the-radleys">The Radleys</a> <em>by Matt Haig</em></li>
<li>2013: <a href="https://remysharp.com/books/2025/spike-milligan-man-of-letters">Spike Milligan: Man of Letters</a> <em>by Spike Milligan</em></li>
<li>2017: <a href="https://remysharp.com/books/2025/butter">Butter</a> <em>by Asako Yuzuki</em></li>
<li>2017: <a href="https://remysharp.com/books/2025/sweetpea-sweetpea-1">Sweetpea (Sweetpea, #1)</a> <em>by C.J. Skuse</em></li>
<li>2018: <a href="https://remysharp.com/books/2025/in-bloom-sweetpea-2">In Bloom (Sweetpea, #2)</a> <em>by C.J. Skuse</em></li>
<li>2019: <a href="https://remysharp.com/books/2025/reasons-to-be-cheerful">Reasons to Be Cheerful</a> <em>by Nina Stibbe</em></li>
</ul>
<p><strong>2020</strong></p>
<ul>
<li>2021: <a href="https://remysharp.com/books/2025/the-man-who-died-twice-thursday-murder-club-2">The Man Who Died Twice (Thursday Murder Club, #2)</a> <em>by Richard Osman</em></li>
<li>2021: <a href="https://remysharp.com/books/2025/the-echo-wife">The Echo Wife</a> <em>by Sarah Gailey</em></li>
<li>2022: <a href="https://remysharp.com/books/2025/the-satsuma-complex-gary-thorn-1">The Satsuma Complex (Gary Thorn, #1)</a> <em>by Bob Mortimer</em></li>
<li>2025: <a href="https://remysharp.com/books/2025/empire-of-ai-dreams-and-nightmares-in-sam-altmans-openai">Empire of AI: Dreams and Nightmares in Sam Altman's OpenAI</a> <em>by Karen Hao</em></li>
<li>2025: <a href="https://remysharp.com/books/2025/minority-rule-adventures-in-the-culture-war">Minority Rule: Adventures in the Culture War</a> <em>by Ash Sarkar</em></li>
<li>2025: <a href="https://remysharp.com/books/2025/making-a-killing-di-fawley-7">Making a Killing (DI Fawley #7)</a> <em>by Cara Hunter</em></li>
</ul>
<p><em>Originally published on <a href="https://remysharp.com/2026/01/01/books-i-read-in-2025">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>My 2025</title>
      <guid isPermaLink="false">my-2025</guid>
      <link>https://remysharp.com/2025/12/31/my-2025</link>
      <pubDate>Wed, 31 Dec 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[I've been doing these posts ending my years, aiming to publish on the 31st, so I'm pleased that I've managed to get this post out the door. Mostly for my own reading, but perhaps yours too.]]></description>
      <content:encoded><![CDATA[
<p>I've been doing these posts ending <a href="https://remysharp.com/my-years">my years</a>, aiming to publish on the 31st, so I'm pleased that I've managed to get this post out the door. Mostly for my own reading, but perhaps yours too.</p>
<h2>Reflecting and comparing</h2>
<p>I've just read through my opening to the <a href="https://remysharp.com/2024/12/31/my-2024">end of 2024</a> and wow, it doesn't feel better (to me). Though I do appreciate two things that factor into this: 1. I'm a glass half empty fella, 2. the coverage (I follow) of events doesn't often highlight the positive things happening.</p>
<p>The world is on fire. Still.</p>
<p>With this particular phrase in mind, I saw The Holdovers last week for the first time, and <a href="https://www.imdb.com/title/tt14849194/quotes/?item=qt7172147">this quote</a> struck me, for a movie set in 1970:</p>
<blockquote>
<p>the world doesn't make sense anymore. I mean, it's on fire. The rich don't give a shit. Poor kids are cannon fodder. Integrity is a punch line. Trust is just a name on a bank.</p>
</blockquote>
<p>The response to which is:</p>
<blockquote>
<p>…if that's all true, then now is when they most need someone like you.</p>
</blockquote>
<p>I think that's why we need to still feel like it's burning so that we can find that fire and help as do what we/I can.</p>
<p>Two particular legal cases that I'm trying to follow (though struggling because I suspect these legal things are <em>slow</em>):</p>
<ul>
<li>Palatine Action having been <a href="https://en.wikipedia.org/wiki/Palestine_Action#Proscription">proscribed</a>, and the <a href="https://en.wikipedia.org/wiki/Palestine_Action#Legal_challenge">legal challenge</a>, at time of writing, <a href="https://gcnchambers.co.uk/final-submissions-made-before-judgment-in-high-court-challenge-to-proscription-of-palestine-action/">&quot;Judgment is awaited&quot;</a>.</li>
<li>The ECHR &quot;interim guidance&quot; (quoted due to the lack-thereof) on &quot;woman&quot; and &quot;sex&quot; refer to the biological sex - and the resulting <a href="https://goodlawproject.org/case/were-challenging-the-ehrcs-interim-guidance/">challenge</a> showing that it's transphobic and legally wrong.</li>
</ul>
<p>A few other highlights being Labour did not swoop in and do what I had hoped (i.e. do right by people) and instead has spent a lot of their time trying to out-Reform Reform. The Giant Cheese Puff across the pond is tearing apart democracy, <a href="https://youtu.be/dergOG7vpJg?si=Oq5VTYDcVAhs3laO&amp;t=105">literally and figuratively</a> and the <a href="https://www.aljazeera.com/search/genocide?sort=date">genocide continues</a> even after a &quot;ceasefire&quot; and the <a href="https://www.theguardian.com/uk-news/2025/dec/29/number-people-britons-must-be-born-in-uk-rising-study">far right continue to raise it's ugly head</a> in the UK. Oh, an AI eating the world and people <em>actually</em> <a href="https://wapo.st/49uy2q1">dying from the companies pushing engagement</a> (<abbr title="Content warning">CW</abbr>: suicide).</p>
<p>So… with that floating around in the background, I have found it hard to muster the motivation to care about the web, about bits of code and about writing on my blog.</p>
<p>I will say that I've found a little light in the political sphere. I regularly watch <a href="https://crooked.com/podcast-series/pod-save-the-uk/">Pod Save the UK</a> on YouTube, and found Zack Polanski's campaign to lead the greens - and <em>a lot</em> of what I believed. I've joined the Greens specifically to cast a vote and not only did he win, but the Greens have seen a huge surge adding over 120,000 new members (me included).</p>
<p>In a similar vein to doom scrolling, I found myself watching Polanski's videos and interviews online to drop a pipette's worth of hope back into my system. This was a <a href="https://www.youtube.com/shorts/4nNJYpHk6Co">particular highlight</a> for me earlier this year.</p>
<p>With that out of the way, what have I been up to?</p>
<h2>Work</h2>
<p>Work-work (or as most people know it: <em>paid work</em>) has really been two main items: working with <a href="https://www.thenational.academy/">Oak National</a> and working on FFConf 2025.</p>
<p>I first started working for Oak in early 2021 and have worked with them for just over 3 years in total. Ranging from rebuilding their question and answer system, to designing and building their public facing content API (where you, Jo Public, can access any part of their curriculum).</p>
<p>I really like working with the people at Oak, and it's the first place I encountered the concept of working in squads (which I see as like mini movie production crews - ask me in person what I mean!).</p>
<p>My last contract ended in mid-August this year, but I'm looking forward to returning to work with them at the start of 2026.</p>
<p>Then onto FFConf, and with my Oak contract ending, allowed me much more time to focus on the event, planning and new software.</p>
<h3>FFConf</h3>
<p>This year's lead up to FFconf was very, very different to 2024's lead up.</p>
<p>In 2024, the stress of trying to sell tickets, trying to find sponsors, and feeling like we were spinning our tyres and getting no-where was bad. Very, very bad. Like &quot;I want to throw this all in&quot;-bad.</p>
<p>When Julie and I returned to FFConf's planning for 2025 we knew this wasn't the last one, but we also knew we needed to do something different at the very least for my own mental health (and so Julie wasn't worried about me the whole year).</p>
<p>The relatively small and simple change we made was that we decided we would send a newsletter every week. The same structure, but weekly on Thursday.</p>
<p>This seemingly obvious change, completely changed the experience for me. There was none of the stress and terror that I experienced in 2024.</p>
<p>Weirdly, tickets sold <em>mostly</em> similarly to 2024, but it <em>felt</em> completely different.</p>
<p>As for the day. It was wonderful. I had a smile throughout the day, and I felt full of love from the people who attended. The talks were amazing and inspiring and fun. I <a href="https://remysharp.com/2025/11/22/ffconf-2025">wrote about the day</a> too.</p>
<h2>Projects</h2>
<p>As I mentioned earlier, I've gone through a lot of the year not entirely excited about web stuff. I want to be, but… I'm just not right now.</p>
<p>A side effect of that feeling is that I've not really put much out into the world. I know I don't have to, but I also know that it's my creative outlet, and it's a nice thing for me to do (for me).</p>
<p>AI has been a dominant feature of the tech industries in 2025 - <a href="https://bsky.app/profile/remysharp.com/post/3mavgppf5bc2k">unavoidable</a> in a lot of cases - but I have been dabbling in lots of little personal tools and toys using (effectively) vibe coding.</p>
<p>I find myself using this vibe coding method to create personal items that make something really specific to me, like: a tracker to help me and Julie to remember who chose the movie last week <em>and</em> what the hell we watched, or, a command line tool to scan my network and try to work out where my <a href="https://esphome.io/">esphome</a> devices are, or, a percentage calculate (because I <em>keep</em> forgetting how it works), or, a tool to modify subtitle files to offset the timing, or, a stupid toy that let's me upload a photo, cut out a mouth, then it uses speech API to sing whatever you type in.</p>
<p><a href="https://remysharp.com/2025/07/18/vibe-coding-and-robocop">But these are personal toys</a>. Prototypes. Things that are not meant for more than one user.</p>
<p>I'd like to list and screenshot some of these in the future to preserve them a little better.</p>
<h3>Donations</h3>
<p>Each year, since 2021, I've been trying to look at donating <em>at least</em> 10% of the business profits to charities.</p>
<p>I put a call out on socials for anyone's lived experience with any trans support charities and the replies grew and grew (as much as I wanted to give to them all, I simply couldn't).</p>
<p>When reviewing which charities to donate to, we always check their income and want to see that they're also using their funds.</p>
<ul>
<li><a href="https://www.choirwithnoname.org/">Choir with No Name</a> - homelessness and marginalised groups</li>
<li><a href="https://www.forwardfacing.co.uk/">Forward Facing</a> - support for families with critical illness or complex disabilities</li>
<li><a href="https://clareproject.org.uk/">The Clare Project</a> - support for trans, non-binary and intersex adults</li>
</ul>
<p>Separately, I also donated to these companies (which aren't charities) from my personal donations:</p>
<ul>
<li><a href="https://transkidsdeservebetter.org/">Trans Kids Deserve Better</a></li>
<li><a href="https://transactual.org.uk/">TransActual</a></li>
<li><a href="https://goodlawproject.org/">The Good Law Project</a></li>
</ul>
<p>I hope to continue this &quot;tradition&quot; while I can.</p>
<h2>Personal</h2>
<p>Having touched on it already in the opening of this post, a lot of my personal headspace is in politics right now.</p>
<p>Outside of that particular stress, I've got my <a href="https://retrobyrem.uk/">Gameboy and retro hardware that I restore</a> (though much less often these days).</p>
<p>I also regularly tinker with my Home Assistant set up (something I've been meaning to write a little more about - there's some fun and dumb automations I have set up). A few of my favourites are: when I go to the gym, the bathroom radiator starts heating up (whereby my undies start getting warm in lieu of my shower when I return), or perhaps the increasingly <a href="https://www.collinsdictionary.com/dictionary/english/aggro">aggro</a> announcement to remind me to empty the washing machine.</p>
<p>The family keep growing (physically, thankfully the numbers have levelled off now!). The pets (3 cats and 1 dog) are happy, and the kids (one teenager and one pre-teen) keep sprouting and amazing (and <em>testing</em> us) each day.</p>
<p>The best books I read this year were both non-fiction (and thusly took me ages to read), but I highly recommend both:</p>
<ol>
<li><a href="https://remysharp.com/books/2025/empire-of-ai-dreams-and-nightmares-in-sam-altmans-openai">Empire of AI: Dreams and Nightmares in Sam Altman's OpenAI</a></li>
<li><a href="https://remysharp.com/books/2025/minority-rule-adventures-in-the-culture-war">Minority Rule: Adventures in the Culture War</a></li>
</ol>
<p>I'll see if I can rustle up my reading summary in the new year - though at 14 books, it's rather brief!</p>
<h3>Next bits - the retrospective</h3>
<p><a href="https://remysharp.com/2024/12/31/my-2024">Last year</a> I said I wanted to:</p>
<ul>
<li><strong>Increase my reading</strong> ✅ - in 2024 I read 13 books, this year I've read 14 (highly unlikely I'll finish the 15th in time!)</li>
<li><strong>Find the motivation to blog some more</strong> ❌ - I didn't find the motivation, though I did actually manage to blog more than 2024 (though still not my target of twice a month, every month)</li>
<li><strong>Hopefully some journalling will help</strong> 🤔 - I started strong, then slowed down to when I needed to put thoughts somewhere, to now… I can't remember the last time I wrote (and writing by pen is a slow process ofr me, my fingers can't keep up with the thoughts) - this one is a semi-fail</li>
<li><strong>Keep an eye on my health</strong> ✅ - this is a mixed bag really. The <abbr title="Multiple Sclerosis">MS</abbr> support here in Brighton is really good, so they regularly meet me to check things are okay (<abbr title="Magnetic resonance imaging">MRI</abbr> scans, drugs, etc). I <em>have</em> managed to return to the gym on a 2/3 times a week schedule. I had some physio that actually helped with some of my finger tip nerve issues (from <abbr title="Multiple Sclerosis">MS</abbr>) and it's improved the sensitivity for the first time in probably a decade. My diet…isn't terrible, but it's a far cry from dry chicken every day - I just think age is a reality I can't just ignore. On the other hand, my <a href="https://remysharp.com/2024/07/05/say-what-on-tinnitus-and-hearing">hearing aids</a> are superb and I love them.</li>
</ul>
<p>But hell, like I read somewhere on the web in the last week: 2026 is probably going to be a shitshow too, so don't be too hard on yourself.</p>
<p>This year, I'd like it if I found some way to get myself to write some more. I do wonder what software development is going to look like for me (particularly after how little code I've written from the ground up in the last 3 months). I'd like to keep up the reading and gym'ing. I'd like to get to bed earlier, and perhaps be a bit kinder inside my own head (to myself primarily). I want to ask &quot;can I help&quot; a lot more (if only to Julie) - it seems like a good mantra. I want to have fun with some stupid home assistant things. I want to design and build more PCBs and electronics projects (and perhaps improve my understanding).</p>
<p>And I'd really like to go to the cinema more. I miss the cinema. Maybe that'll be my thing in 2026 🤔</p>
<p>If you made it this far, thanks. If you're Remy reading this from the future - I hope things are looking up.</p>
<p>I'm now looking forward to reading your year-in-review blog posts, and I'll see you in The Future / 2026 💜</p>
<figure><img src="https://remysharp.com/images/family-2025.avif" alt="The family, growing up(wards)!" decoding="async"></figure>
<p><em>Originally published on <a href="https://remysharp.com/2025/12/31/my-2025">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>An opportunity to learn: Advent of Code</title>
      <guid isPermaLink="false">an-opportunity-to-learn-advent-of-code</guid>
      <link>https://remysharp.com/2025/12/03/an-opportunity-to-learn-advent-of-code</link>
      <pubDate>Wed, 03 Dec 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[I've written about Advent of Code in the past, but that was 5 years ago, so this warrants a new post, and there's an extra opportunity, I think.]]></description>
      <content:encoded><![CDATA[<p>I've written about Advent of Code in <a href="https://remysharp.com/2020/12/01/the-advent-of-code">the past</a>, but that was 5 years ago, so this warrants a new post, and there's an extra opportunity, I think.</p>
<h2>The code calendar</h2>
<p>The advent of code is a daily code challenge, comprising of two parts based around the same problem, with increasing complexity. There's a wonderful little story that's weaved through the days, usually relating to Christmas and rouge elves that you can help.</p>
<p>I use it as a yearly chance to try to bend the <a href="https://jqlang.org/">jq language</a> into shapes it definitely wasn't intended for. It's another way of clearing out some of the cobwebs that gather in my head (<a href="https://github.com/remy/advent-of-code-solved/tree/main/2025">these are my attempts so far</a>).</p>
<p>Perhaps you're new to software development or a particular language, then this is a great way of having real problems to solve where there's an absolute answer. There's also a wealth of solutions posted up on the <a href="https://www.reddit.com/r/adventofcode/">Reddit channel</a> if you (or I) get stuck and need inspiration.</p>
<h2>Changes to the programme</h2>
<p>As of this year, there's now only 12 challenge days (previously it was 24 - one for each day until Christmas eve). For me this is welcomed, partly because I'd usually break by day 16, but also, I'm not sure I want to spend hours tinkering on a hard problem on Christmas eve (having never gotten past 16, I hadn't yet!).</p>
<p>The global leaderboard has also been removed. I personally never made it anywhere near the leaderboard, but some people would obsess over it. Sadly, with the rise and ease of access of AI tools, it meant quickly the time from challenge release to posting a correct answer started turning up in seconds long. That's single digit seconds to solve the problem - which really isn't in the spirit at all.</p>
<p>AI was banned/asked to not join in with the leaderboard, but completely removing the global leaderboard completely takes the pressure off (and that idea that &quot;why can't I solve it that fast&quot;).</p>
<p>You <em>can</em> still create a leaderboard for your friends or team - which makes sense.</p>
<h2>Speaking of AI</h2>
<p>Here's where my final suggestion might be controversial: why not try to solve using AI?</p>
<p>By that, I mean, if you're in a similar camp as me and have been sceptical of AI and a little wary, this might be a good opportunity to dip your toe in. I don't mean to paste in the challenge and have AI spit out the answer - that doesn't help anyone.</p>
<p>It could be seen as a chance to practise controlling the context and the problem space with AI doing the work. Perhaps testing different models from providers, but perhaps trying out local LLM models to see if they can do the work (so perhaps we could be a little more in control of the power usage).</p>
<p>As for me, I'm having to <em>actively</em> disable copilot in VS Code when solving the advent of code, because it's so desperate to help me!</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/12/03/an-opportunity-to-learn-advent-of-code">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Handing over to the AI for a day</title>
      <guid isPermaLink="false">handing-over-to-the-ai-for-a-day</guid>
      <link>https://remysharp.com/2025/11/29/handing-over-to-the-ai-for-a-day</link>
      <pubDate>Sat, 29 Nov 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[Context: back in March 2025 I decided to put aside my scepticism and try AI driven development for the day. I appreciate that in 8 months, the AI landscape, particularly around agentic software dev has moved along, and perhaps this should have been posted originally back in March. All the same, maybe this is useful to some degree, if only to capture what it was like in time.

Whilst I sit squarely in my AI-sceptic seat, I was recently prompted to try a different tact by two post I read a few weeks back.
The first was Bruce's colonoscopy post, yes, that one. It was in fact that he was using a local LLM to create a generative image to commemorate his visit. I'd been using chatgpt (for code and electronics, basically a NLP version of a search engine), and hadn't considered that perhaps I could host myself to take some responsibility for one of the two problematic aspects of LLMs today (first being power consumption, second being the global theft).
The second post was Simon Willison's post on how he created a mini tool (the post is much broader than that, a useful read). It wasn't the post so much but the tool he was using: Claude Code. So far with chatgpt I'd copy back and forth errors and tweaks when it was helping me. Although I also have copilot enabled in VS Code it really holds down to autocomplete at a pretty junior level.
I'll often have typescript errors (for work) that copilot claims it can help fix, only resulting in even more typescript errors - so as a rule, I tend to avoid generating code fixes with copilot.
But what Simon showed with a shared transcript between himself and Claude Code was the software making the changes and offering diffs.
So that was what prompted a mini journey, and here's how it went.
Offline LLMs
Previously I had installed command line tools and even Ollama on my Mac without having the faintest idea how to use them effectively - so they sat idle and unused.
I'm not sure how, but I came across Msty, a tool that purported to make using local LLMs very easy. For a change, that seems to be true.
Since I still had Ollama running (though I should have probably ejected it), Msty quickly linked up to this and discovered (though I'd forgotten) that the DeepSeek R1 model was loaded already.
I figured since I was primarily using LLMs for software development, it made sense to find a model that suited those requirements.
Not quite that simple, or certainly not if you (ie. me) doesn't have the LLM lingo and terminology down.
I did some searching online, found a few articles, which themselves read as if they had been generated by AI, offering feature reviews of LLM models, but I couldn't find anything that said &quot;model X is excellent for Y software development&quot;.
Additionally, I found some models on huggingface.io, and figured that 7B was going to be easier/more viable to run than a 70B (big numbers need big machines? I was guessing…). However putting the name of the model into Myst yielded a lot of results and I couldn't really tell you what the difference was. More importantly, I couldn't quite be sure of the provenance (and whether that even matters…I'd assume it did).
I ended up installing the following models:

llama 3.2 (2gb)
deepseek coder v2 (which blew up)
codellama (3.8gb)
codellama 13b
WizardCoder (I think the 3gb one?) (it just didn't reply on the query)
Wavecoder-ultra-6.7b (literally never worked for me)

I tried a number of technical problems without much success and did capture the output for a number of these models to try to solve a Home Assistant automation process (which you can see here).
It's entirely likely there's better models out there for what I wanted to do. I did know I didn't need a model to know everything, so some focus (and thus: reduced size) would make sense.
However, not one of these gave useful replies to my questions.
My favourite (fail) was giving code-llama a question that read: this query gets all the lessons matching these lesson slugs (I gave it the actual SQL query), how do I get a list of all the slugs that didn't match?.
The LLM happily reported that I was trying to learn Spanish:

The provided code contains a list of sentences that are written in Spanish, and the goal is to translate them into English. However, there are some errors in the provided code that need to be fixed before it can function properly. Here are some issues with the current code:
Missing curly braces around the translate function definition: The translate function should be defined as a separate named function expression, like this: { translate } = require(&quot;google-translate&quot;);. This will ensure that the function is properly closed and can be used in other parts of the code.

For some reason it was using the slugs in the query as the most important part and completely ignoring the technical query.
For the most part, the responses I got were fairly hand wavy, text heavy (which I didn't want since I was asking about code), and in most cases irrelevant to my task.
I think em0ry42 on BlueSky sums up what I was seeing:

Smaller self hosted models will always under perform the larger ones. I think your experience and those of the other commenters are consistent with the current reality of these technologies. They can't do what people are promising. Yet.

I'm sure there are people who can tune the hell out of their setup, but sadly, running any decent LLM locally as a useful code assistant, is just not here for the rest of us.
So I parked that for a while, and turned my attention to Claude Code.
Coding without touching code
I've no clue how new Claude Code was at the time, though I've gathered it's fairly new. It's a solid product from my experience (where I even managed to lose track as to which company owns which weirdly named AI thingy).
Setup and interface is entirely on the command line, so already we're speaking my language.
I'd seen demos of developers who've been able to join up their entire codebase to the LLM but each time I'd dabbling, I would quickly get lost and give up.
Claude Code does exactly this without the walls I'd experienced in the past.
I am however, acutely wary that Claude is running on a remote machine, and likely to be chonking through so much power that we're just throwing away water to keep machines from burning up. Let's stick a pin in that (and gosh, I loath myself already for that).
The very first problem I wanted it to solve was where I was trying to download 1,000s of videos and they all needed to be added to one massive tarball (context: this is for work, to allow users to bulk download our assets).
I'd hit a problem where the tar process kept throwing an unhelpful exception the evening before and no amount of documentation on the library I was using helped me.
Overnight I had a suspicion as to the cause and it gave me an idea to try - but I thought I'd let Claude try first, see what it does.
Without any specific direction (ie. my idea for the fix), and only the name of the file and the function the problem happened, Claude Code suggested the same solution I had in my head.
The UI then offered a syntax highlighted diff of the change it wanted to commit to disk. I was able to review it (very much how I'd approach a code review) and all I then needed to do was hit enter to accept.
I tested the code in a separate terminal and indeed the change worked.
Given this positive start, I then spent most of the working time split between the Claude Code UI and in the terminal to run the main program (which was sequencing a very large dataset).
The code changes for the most part were always good and code that I accepted.
The experience was…weird. I'd heard of LLMs being referred to as junior developers but when I was going back and forth between chatgpt and vscode (again, for me, copilot never really came in useful), because of the amount of interaction that was required from me, it felt even less than working with a junior.
But this was a much closer experience. I'd describe the change and logic, sometimes pointing to filenames that would offer useful context, and Claude would spend some time (and money) thinking, then it would ask me to check a diff.
Weirdly I spent more time sitting and staring out the window waiting for code to come back than I did looking at code. It was a weirdly hands off experience. I can't tell where I sit on that.
The main criticism that I had is that, because we use specific rules for typescript (no any and types are defined, which I think seems okay), Claude wouldn't really follow those strict rules, so I needed to go in at the end to clean that part up.
The secondary criticism is more a matter of taste. The code (and logging) was verbose to my taste. Additionally, being outside the code for the majority of the work period felt really strange. Sort of like a self-driving car took me the majority of my journey, deciding itself the navigation, for me only to be needed for the final arrival through some tight country lanes. Or something!
A cost
Since I was freestyling my way on Claude Code, I did manage to rattle through $5 of credit. I did think this was (somehow) linked to my Google business account, but I'm now suspecting it was free credit to introduce me to their API.
After running through this credit now twice (I switched to my personal account for a second run) I've discovered there's tools to help manage that sprawling cost (such as /compact and /clear to reduce how much context the LLM is fed before giving me a result). I'd like to play with this more to get an idea of how much I'm really prepared to pay.
Also after writing (most of) this post, I came across an interesting project that takes the Claude UI and lets you connect up your own backend. I've not tried it yet, but I'd be interested to see if I can connect to a local LLM and try out results (though going by the current experience, it's going to have a hard time competing).
Since it was conversational...
I decided to hack together a simple keyboard keycap (I had spare) with an ESP32 board to emulate a keyboard.
Then this would send a (fairly) unique keycode that then launched a python command which started a whisper based script that let me talk, then pasted the text into whatever was focused.
This meant I had: press button, say the thing, press the button, wait for it to be done.
It wasn't great because it was a little clunky, but it definitely felt futuristic!
How I felt afterwards
(I'm now writing this 8 months late, but I remember how it felt on the day).
Even though I was surprised at the progress of the work, both for how terrible the local code solving was and how impressed I was with Claude Code - it did leave me with a feeling of disconnect.
There's certainly the issue with the maintainability of pure vibe-coded software, but this was something more.
There's a creative input that I put into my coding process. A sense of purpose and achievement in solving some complicated problem, or writing a line of code that I'm particularly pleased with. There wasn't really of that feeling of connection with the output.
Having written this retrospectively I know that my perspective has changed somewhat, but I do remember have this weird dissonance between the outcome and the experience of getting there.]]></description>
      <content:encoded><![CDATA[
<p><em><strong>Context</strong>: back in March 2025 I decided to put aside my scepticism and try AI driven development for the day. I appreciate that in 8 months, the AI landscape, particularly around agentic software dev has moved along, and perhaps this should have been posted originally back in March. All the same, maybe this is useful to some degree, if only to capture what it was like in time.</em></p>
<hr>
<p>Whilst I sit squarely in my AI-sceptic seat, I was recently prompted to try a different tact by two post I read a few weeks back.</p>
<p>The first was Bruce's <a href="https://brucelawson.co.uk/2025/colonoscopy-fun/">colonoscopy post</a>, yes, that one. It was in fact that he was using a <em>local</em> LLM to create a generative image to commemorate his visit. I'd been using chatgpt (for code and electronics, basically a NLP version of a search engine), and hadn't considered that perhaps I could host myself to take some responsibility for one of the two problematic aspects of LLMs today (first being power consumption, second being the global theft).</p>
<p>The second post was Simon Willison's post on <a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#a-detailed-example">how he created a mini tool</a> (the post is much broader than that, a useful read). It wasn't the post so much but the tool he was using: Claude Code. So far with chatgpt I'd copy back and forth errors and tweaks when it was helping me. Although I also have copilot enabled in VS Code it really holds down to autocomplete at a pretty junior level.</p>
<p>I'll often have typescript errors (for work) that copilot claims it can help fix, only resulting in even more typescript errors - so as a rule, I tend to avoid generating code fixes with copilot.</p>
<p>But what Simon showed with a shared transcript between himself and Claude Code was the software making the changes and offering diffs.</p>
<p>So that was what prompted a mini journey, and here's how it went.</p>
<h2>Offline LLMs</h2>
<p>Previously I had installed command line tools and even Ollama on my Mac without having the faintest idea how to use them effectively - so they sat idle and unused.</p>
<p>I'm not sure how, but I came across <a href="https://msty.app/">Msty</a>, a tool that purported to make using local LLMs very easy. For a change, that seems to be true.</p>
<p>Since I still had Ollama running (though I should have probably ejected it), Msty quickly linked up to this and discovered (though I'd forgotten) that the DeepSeek R1 model was loaded already.</p>
<p>I figured since I was primarily using LLMs for software development, it made sense to find a model that suited those requirements.</p>
<p>Not quite that simple, or certainly not if you (ie. me) doesn't have the LLM lingo and terminology down.</p>
<p>I did some searching online, found a few articles, which themselves read as if they had been generated by AI, offering feature reviews of LLM models, but I couldn't find anything that said &quot;model X is excellent for Y software development&quot;.</p>
<p>Additionally, I found some models on <a href="http://huggingface.io">huggingface.io</a>, and figured that 7B was going to be easier/more viable to run than a 70B (big numbers need big machines? I was guessing…). However putting the name of the model into Myst yielded <em>a lot</em> of results and I couldn't really tell you what the difference was. More importantly, I couldn't quite be sure of the provenance (and whether that even matters…I'd assume it did).</p>
<p>I ended up installing the following models:</p>
<ol>
<li>llama 3.2 (2gb)</li>
<li>deepseek coder v2 (which blew up)</li>
<li>codellama (3.8gb)</li>
<li>codellama 13b</li>
<li>WizardCoder (I think the 3gb one?) (it just didn't reply on the query)</li>
<li>Wavecoder-ultra-6.7b (literally never worked for me)</li>
</ol>
<p>I tried a number of technical problems without much success and did capture the output for a number of these models to try to solve a Home Assistant automation process (which <a href="https://gist.github.com/remy/76d5761b57a5d42f781b74c072d22139">you can see here</a>).</p>
<p>It's entirely likely there's better models out there for what I wanted to do. I did know I didn't need a model to know everything, so some focus (and thus: reduced size) would make sense.</p>
<p>However, not one of these gave useful replies to my questions.</p>
<p>My favourite (fail) was giving code-llama a question that read: this query gets all the lessons matching these lesson slugs (I gave it the actual SQL query), how do I get a list of all the slugs that <strong>didn't</strong> match?.</p>
<p>The LLM happily reported that I was trying to learn Spanish:</p>
<blockquote>
<p>The provided code contains a list of sentences that are written in Spanish, and the goal is to translate them into English. However, there are some errors in the provided code that need to be fixed before it can function properly. Here are some issues with the current code:</p>
<p>Missing curly braces around the translate function definition: The translate function should be defined as a separate named function expression, like this: { translate } = require(&quot;google-translate&quot;);. This will ensure that the function is properly closed and can be used in other parts of the code.</p>
</blockquote>
<p>For some reason it was using the slugs in the query as the most important part and completely ignoring the technical query.</p>
<p>For the most part, the responses I got were fairly hand wavy, text heavy (which I didn't want since I was asking about code), and in most cases irrelevant to my task.</p>
<p>I think <a href="https://bsky.app/profile/em0ry42.penney-family.net/post/3lkdpqaouc22h">em0ry42 on BlueSky</a> sums up what I was seeing:</p>
<blockquote>
<p>Smaller self hosted models will always under perform the larger ones. I think your experience and those of the other commenters are consistent with the current reality of these technologies. They can't do what people are promising. Yet.</p>
</blockquote>
<p>I'm sure there are people who can tune the hell out of their setup, but sadly, running any decent LLM locally as a useful code assistant, is just not here for the rest of us.</p>
<p>So I parked that for a while, and turned my attention to <a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview">Claude Code</a>.</p>
<h2>Coding without touching code</h2>
<p>I've no clue how new Claude Code was at the time, though I've gathered it's <em>fairly</em> new. It's a solid product from my experience (where I even managed to lose track as to which company owns which weirdly named AI thingy).</p>
<p>Setup and interface is entirely on the command line, so already we're speaking my language.</p>
<p>I'd seen demos of developers who've been able to join up their entire codebase to the LLM but each time I'd dabbling, I would quickly get lost and give up.</p>
<p>Claude Code does exactly this without the walls I'd experienced in the past.</p>
<p>I am however, acutely wary that Claude <em>is</em> running on a remote machine, and likely to be chonking through so much power that we're just <a href="https://m.youtube.com/shorts/PSe6GSwJ0cI">throwing away water</a> to keep machines from burning up. Let's stick a pin in that (and gosh, I loath myself already for that).</p>
<p>The very first problem I wanted it to solve was where I was trying to download 1,000s of videos and they all needed to be added to one massive tarball (context: this is for work, to allow users to bulk download our assets).</p>
<p>I'd hit a problem where the tar process kept throwing an unhelpful exception the evening before and no amount of documentation on the library I was using helped me.</p>
<p>Overnight I had a suspicion as to the cause and it gave me an idea to try - but I thought I'd let Claude try first, see what it does.</p>
<p>Without any specific direction (ie. my idea for the fix), and only the name of the file and the function the problem happened, Claude Code suggested the same solution I had in my head.</p>
<p>The UI then offered a syntax highlighted diff of the change it wanted to commit to disk. I was able to review it (very much how I'd approach a code review) and all I then needed to do was hit enter to accept.</p>
<p>I tested the code in a separate terminal and indeed the change worked.</p>
<p>Given this positive start, I then spent most of the working time split between the Claude Code UI and in the terminal to run the main program (which was sequencing a very large dataset).</p>
<p>The code changes for the most part were always good and code that I accepted.</p>
<p>The experience was…weird. I'd heard of LLMs being referred to as junior developers but when I was going back and forth between chatgpt and vscode (again, for me, copilot never really came in useful), because of the amount of interaction that was required from me, it felt even less than working with a junior.</p>
<p>But this was a much closer experience. I'd describe the change and logic, sometimes pointing to filenames that would offer useful context, and Claude would spend some time (and money) thinking, then it would ask me to check a diff.</p>
<p>Weirdly I spent more time sitting and staring out the window waiting for code to come back than I did looking at code. It was a weirdly hands off experience. I can't tell where I sit on that.</p>
<p>The main criticism that I had is that, because we use specific rules for typescript (no <code>any</code> and types are defined, which I think seems okay), Claude wouldn't really follow those strict rules, so I needed to go in at the end to clean that part up.</p>
<p>The secondary criticism is more a matter of taste. The code (and logging) was verbose to my taste. Additionally, being outside the code for the majority of the work period felt really strange. Sort of like a self-driving car took me the majority of my journey, deciding itself the navigation, for me only to be needed for the final arrival through some tight country lanes. Or something!</p>
<h2>A cost</h2>
<p>Since I was freestyling my way on Claude Code, I did manage to rattle through $5 of credit. I did think this was (somehow) linked to my Google business account, but I'm now suspecting it was free credit to introduce me to their API.</p>
<p>After running through this credit now twice (I switched to my personal account for a second run) I've discovered there's tools to help manage that sprawling cost (such as <code>/compact</code> and <code>/clear</code> to reduce how much context the LLM is fed before giving me a result). I'd like to play with this more to get an idea of how much I'm <em>really</em> prepared to pay.</p>
<p>Also after writing (most of) this post, I came across an interesting project that takes the <a href="https://github.com/dnakov/anon-kode">Claude UI and lets you connect up your own backend</a>. I've not tried it yet, but I'd be interested to see if I can connect to a local LLM and try out results (though going by the current experience, it's going to have a hard time competing).</p>
<h2>Since it was conversational...</h2>
<p>I decided to hack together a simple keyboard keycap (I had spare) with an ESP32 board to emulate a keyboard.</p>
<p>Then this would send a (fairly) unique keycode that then launched a python command which started a whisper based script that let me talk, then pasted the text into whatever was focused.</p>
<p>This meant I had: press button, say the thing, press the button, wait for it to be done.</p>
<p>It wasn't great because it was a little clunky, but it definitely felt futuristic!</p>
<h2>How I felt afterwards</h2>
<p>(I'm now writing this 8 months late, but I remember how it felt on the day).</p>
<p>Even though I was surprised at the progress of the work, both for how terrible the local code solving was and how impressed I was with Claude Code - it did leave me with a feeling of disconnect.</p>
<p>There's certainly the issue with the <a href="https://remysharp.com/2025/07/18/vibe-coding-and-robocop">maintainability of pure vibe-coded software</a>, but this was something more.</p>
<p>There's a creative input that I put into my coding process. A sense of purpose and achievement in solving some complicated problem, or writing a line of code that I'm particularly pleased with. There wasn't really of that feeling of connection with the output.</p>
<p>Having written this retrospectively I know that my perspective has changed somewhat, but I do remember have this weird dissonance between the outcome and the experience of getting there.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/11/29/handing-over-to-the-ai-for-a-day">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>FFConf 2025</title>
      <guid isPermaLink="false">ffconf-2025</guid>
      <link>https://remysharp.com/2025/11/22/ffconf-2025</link>
      <pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[I've been wanting to write and share my experience of this year's event but a number of things have slowed me down - not least of all that it was Julie's birthday the following Tuesday (the first year her birthday was entirely swallowed by the event).
So now as I sit writing this a full eight days later - sat on the side of the swimming pool as many kids, including my own, do their swimming lessons - I'm trying to collect my thoughts on the day.]]></description>
      <content:encoded><![CDATA[
<p>I've been wanting to write and share my experience of this year's event but a number of things have slowed me down - not least of all that it was Julie's birthday the following Tuesday (the first year her birthday was entirely swallowed by the event).</p>
<p>So now as I sit writing this a full eight days later - sat on the side of the swimming pool as many kids, including my own, do their swimming lessons - I'm trying to collect my thoughts on the day.</p>
<h2>The talks</h2>
<p>Every year it's always an amazing experience, hearing the stories of speakers from across different walks of life.</p>
<p>The videos of the talks will be released in the coming weeks and <a href="https://ffconf.org/news">our newsletter</a> will highlight each in turn.</p>
<p>I curated the talks so that up front we had the heavier subjects: diversity from <a href="https://2025.ffconf.org/hellen">Hellen</a> and data privacy from <a href="https://2025.ffconf.org/chetan">Chetan</a>.</p>
<p>From the evening before we were talking about JavaScript, React, standards and new developers coming to the industry. We were talking specifically about whether the lessons had been learnt from the past or needed to be repeated.</p>
<p>I'm of the belief (though only in the last decade) that we need to repeat the learnings over and over and over. There's no point when the teaching is done. A fun example of this is seeing the CSS creative coders having their hay-day whilst hearing from the sidelines that the Flash developers have already done a lot of this. It's not new. But if you don't continue to pass down your lessons, the next generations will make the same mistakes and have to learn from scratch.</p>
<p>That's a very long way to say that we've covered the subjects of diversity and privacy before, but new perspectives and different experiences keep the subjects fresh and not a &quot;done&quot; task in the world of the web worker.</p>
<p>The next segment talked about web components from <a href="https://2025.ffconf.org/hannah">Hannah</a> and about cults from <a href="https://2025.ffconf.org/serges">Sèrges</a>.</p>
<p>Hannah talked about her experience bringing web components to a React based team and the process integration both from the software side but also from the team adoption perspective.</p>
<p>Unsurprisingly (see previous notes on curation 😉) Sèrges brilliant (fun and funny) talk about cults dovetailed perfectly into Hannah's talk. She argued that we're all in a cult (to some degree) and touched on the gatekeeping that was also touched on in Helen's talk about team diversity.</p>
<p>Post lunch was time to discuss AI. When friends of mine who know me from outside the web, as about the conference, they understandably assume it's all about AI. It's all we hear about these days.</p>
<p>The two talks were very different perspectives on AI. <a href="https://2025.ffconf.org/asim">Asim</a> gave a talk (obscurely but made sense in the end) entitled Don't be an Idiot on how AI can, and is, being used to democratise big decisions, putting much more nuance in the decisions offered to people.</p>
<p>The following was <a href="https://2025.ffconf.org/jessica-eda">Jess and Eda</a> on AI coding tools and skills. Told from the perspective of a senior and junior and what are the lasting impacts on relying (or being pushed into) AI tools. Very much the argument of shallow learning versus deep (human) learning through experience.</p>
<p>Finally to close the event two talks from the vast spread of experience starting with <a href="https://2025.ffconf.org/surya">Surya</a> on the journey of 6 to 16 years old and coding. As our industry continues to exist, it's the young people who will breathe life into it, so I wanted to hear about Surya's experience (even if he is a bit of a black swan - making many of us old hats feel rather like underachievers!).</p>
<p>Then to close, we (FFConf) were lucky to have <a href="https://2025.ffconf.org/sacha">Sacha Judd</a> (because she resides in New Zealand and it's a long old journey) filling our hearts with the joy of what the Good Internet is. Not just a nostalgic memory but alive today and just as weird as always.</p>
<p>Here are all <a href="https://www.flickr.com/photos/remysharp/albums/72177720330509685/">the photos from FFConf 2025</a> if you'd like to see the lovely time people had.</p>
<figure><img src="https://remysharp.com/images/ffconf-2025.avif" alt="The people attending FFConf waving for the camera" decoding="async"></figure>
<h2>Looking for meaning</h2>
<p>The last few years of FFconf have been hard for me to put into context in a world that honestly frightens me. War. Genocide. Famine. Racism. Fascism. Hate. They're alive and well.</p>
<p>Sometimes I think it's ridiculous to put on a web event when things outside are so hard on people. In part it feels like a horrible privilege to be able to sink my head down and listen to these amazing speakers.</p>
<p>On reflection though, I do believe we need to recharge. That we need a little self care if we're going to continue fighting for a fair and equal world, however we achieve it. So maybe that's important for FFConf.</p>
<p>At the top of the event, I asked everyone to try to find one thing that you'll take away. There's so much in a day of talks - it's hard to remember what got you excited in the earlier talks let alone make sweeping changes.</p>
<p>So the (first) one thing I decided to do, relatively simple, was to move to more paid services (I've switched to <a href="https://kagi.com/">Kagi</a> already). I've already started paying for my news (<a href="https://www.404media.co/">404 Media</a>, <a href="https://novaramedia.com/">Novara Media</a>, Guardian and a few others) so I'm continuing to look for these. I also know that this works if you have the funds. For me, it's part of my work, so my business pays for it. I get that it's not as simple for others (that's to say: this is <em>my</em> one thing!).</p>
<p>In my back pocket I'm going to make myself some either silly or partially useful little toys. I may or may not share these; I like that these can be for me and family/friends: low pressure nonsense.</p>
<h2>Next</h2>
<p>FFConf will return next year, Friday 13 November. I've already got a couple of speakers in mind so I need to approach them with my proposals.</p>
<p>I'll be attending more events myself next year too, including <a href="https://2026.stateofthebrowser.com/">State of the Browser</a>, <a href="https://webdayout.com/">Web Day Out</a>, (the final) <a href="https://heypresents.com/conferences/2026">All Day Hey</a>, <a href="https://beyondtellerrand.com/events/dusseldorf-2026">Beyond Tellerand</a> and <a href="https://webdevconf.com/events/">WDC</a> (tickets aren't live yet). I'm on the lookout for more events and meet ups in particular (though I have a stinking record of staying at home).</p>
<p>I know this might read as a hopeful note, and I do want it to be, but I also know that I don't 100% buy my written down optimism (I should!). Still, I hope to see you out there.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/11/22/ffconf-2025">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Syntax Highlighting in Web Component Templates</title>
      <guid isPermaLink="false">syntax-highlighting-in-web-component-templates</guid>
      <link>https://remysharp.com/2025/11/12/syntax-highlighting-in-web-component-templates</link>
      <pubDate>Wed, 12 Nov 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[A simple but effective fix to working with web components and VS Code. I wanted to get syntax highlighting and prettier support (to auto fix indenting, quotes, etc) in my component's templates.
The extremely quick read is, add /* HTML */ to the front of the template. Case sensitive and space sensitive (though hopefully one day it won't be so strict). Now highlighting and prettier (with save and fix) works.
Note that you need the es6-string-html VS Code extension for this to highlight correct (something I had forgotten I had installed).]]></description>
      <content:encoded><![CDATA[
<p>A simple but effective fix to working with web components and VS Code. I wanted to get syntax highlighting <em>and</em> prettier support (to auto fix indenting, quotes, etc) in my component's templates.</p>
<p>The extremely quick read is, add <code>/* HTML */</code> to the front of the template. Case sensitive <em>and</em> space sensitive (though hopefully one day it won't be so strict). Now highlighting and prettier (with save and fix) works.</p>
<p>Note that you need the <a href="https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html">es6-string-html</a> VS Code extension for this to highlight correct (something I had forgotten I had installed).</p>
<h2>The slightly longer read</h2>
<p>What I wanted to achieve was that I could include my template in the source of the web component (or at least in the same directory - i.e. physically near to the application logic).</p>
<p>The problem is that unless I use a build function (or tools), the markup for the template is a string.</p>
<p>Here's the starting point:</p>
<pre><code class="language-js">customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span>
  <span class="token string">'hello-world'</span><span class="token punctuation">,</span>
  <span class="token keyword">class</span> <span class="token class-name">extends</span> HTMLElement <span class="token punctuation">{</span>
    <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
      <span class="token keyword">const</span> n <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'name'</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token string">'World'</span><span class="token punctuation">;</span>
      <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">attachShadow</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">mode</span><span class="token operator">:</span> <span class="token string">'open'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
      &lt;style>:host{font:600 16px system-ui;color:#222}&lt;/style>
      &lt;div>Hello, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>n<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> 👋&lt;/div></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>Seen here with VS Code's syntax highlighting - note that the template itself is just green, plain text:</p>
<figure><img src="https://remysharp.com/images/template-highlight-0.png.avif" alt="Although the JavaScript is highlighted, the string of markup applied to the innerHTML is all in green" decoding="async"></figure>
<p>Using a tag function solves the main issue, but requires extra code or a <em>magic</em> build process which I really don't want or need - and look at that nasty red snake telling me I need to write more code:</p>
<figure><img src="https://remysharp.com/images/template-highlight-2.png.avif" alt="The syntax is nice and tidy now using a tag function, but my linting is highlighted that the html function is missing" decoding="async"></figure>
<p>VS Code does support highlighting if you give it a hint using a comment: <code>/*html*/</code>, but prettier doesn't format it - close, but still no dice:</p>
<figure><img src="https://remysharp.com/images/template-highlight-1.png.avif" alt="The syntax is now fully colourised, but not structured in a way that's nice and easy to read" decoding="async"></figure>
<p>Finally, if you get the comment syntax <em>exactly</em> right, that's uppercase and with spaces around the text, <code>/* HTML */</code>, then you'll get both highlighting and syntax tidy support without the need for build tools:</p>
<figure><img src="https://remysharp.com/images/template-highlight-3.png.avif" alt="The syntax is now fully colourised and it's tidy" decoding="async"></figure>
<p>I do have the VS Code option <code>&quot;prettier.embeddedLanguageFormatting&quot;: &quot;auto&quot;</code> in my settings, but if I've understood correctly, this should be the default for prettier and not required.</p>
<p>A large hat tip to <a href="https://front-end.social/@zzzzBov/115535106033886819">Timothy Leverett on Mastodon</a> for helping me.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/11/12/syntax-highlighting-in-web-component-templates">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>How has FFConf changed since it was known as Full Frontal?</title>
      <guid isPermaLink="false">how-has-ffconf-changed-since-it-was-known-as-full-frontal</guid>
      <link>https://remysharp.com/2025/10/27/how-has-ffconf-changed-since-it-was-known-as-full-frontal</link>
      <pubDate>Mon, 27 Oct 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[Besides the name, the entire core foundation has changed.
This question came up recently by someone who had either attended back in the early days or that knew of our event, but they (understandably) saw us as it was in 2009.
I think the vibes you came away with in the 2009 and 2010 events would be recognised, but content and the core messaging has changed quite significantly.]]></description>
      <content:encoded><![CDATA[
<p>Besides the <a href="https://remysharp.com/2016/07/22/whats-in-a-name">name</a>, the entire core foundation has changed.</p>
<p>This question came up recently by someone who had either attended back in the early days or that knew of our event, but they (understandably) saw us as it was in 2009.</p>
<p>I think the vibes you came away with in the 2009 and 2010 events would be recognised, but content and the core messaging has changed quite significantly.</p>
<p>The event was founded on the premise that there weren’t any (dedicated) JavaScript conferences in the UK at the time, and I wanted <em>something</em> to fulfil my own desire to see that change. I wanted an event that I would want to attend.</p>
<p>The first line up in 2009 was mostly brought about by asking the question: who do I know or could reach that talk about JavaScript things? I can’t say for certain, but I suspect the outreach was &quot;can you speak&quot; rather than &quot;can you speak about…&quot;.</p>
<p>Immediately I knew I wanted a rule that prevented speakers from repeating (this was a preventative measure to get me to reach outside of my circles).</p>
<p>The follow 2010 event still had a JavaScript flavour but was already showing signs of morphing into something different (albeit I wouldn’t start working on the diversity until 2012).</p>
<p>I remember Dan Webb (who spoke in 2010) saying that he realised at the end of the event he was the only speaker to show code in his slides.</p>
<p>By 2018, the 10th conference, I think the entire shape had changed. Talks like <a href="https://ffconf.org/talks/mentoring/">&quot;Mentoring: Being the help you wish you'd had&quot;</a>, <a href="https://ffconf.org/talks/dear-developer/">&quot;Dear Developer, the Web Isn't About You&quot;</a> and <a href="https://ffconf.org/talks/weird-web">&quot;Weird Web &amp; Curious Creation&quot;</a> were all based in the idea of helping others and ourselves to become better Web Citizens.</p>
<p>People who had attended told me that the talks they saw at FFConf were talks they never expected to see - in a positive way. <a href="https://ffconf.org/talks/2022_heydonworks_talk/">&quot;Capitalism, The Web, And You&quot;</a> was an excellent example of that in 2019 along with <a href="https://ffconf.org/talks/2022_lil_natw_talk/">&quot;Working towards a greener world from behind the keyboard&quot;</a> or <a href="https://ffconf.org/talks/2022_lily_2point0_talk/">&quot;Programming with Yarn&quot;</a> (although I've only included 3 talks, they're all superb, or certainly to me).</p>
<p>The talks at FFConf continue along this line of thinking. How can we learn to be better versions of ourselves - and so that we can help others.</p>
<p>From the talks on the day, you won't learn how to add aria roles to a tabbing system, but you will learn how your junior developers are being affected by learning from LLMs spouting code at them. You won't learn what the latest additions to JavaScript syntax is, but you will learn how the latest technology has been used to exploit your privacy and the ethics of how developers' decisions landed them there.</p>
<p>You won't learn what a blog post, a tiktok or chatgpt can give you. You'll come away inspired and fired up and (hopefully, usually!) excited to work. It's a hard one to measure, and hard to explain to a business &quot;what's the ROI?&quot;, but if we are happy, keen or even excited to work, then the ROI is <em>always</em> a net positive.</p>
<p>So that's what FFConf is in 2025. It's an event chooses people over technology, that aims to inspire and motivate, and that champions a better web.</p>
<figure><img src="https://remysharp.com/images/combined-ffconf-group.avif" alt="The audience over the years, looking happy with their hands raised high" decoding="async"></figure>
<p><em>Originally published on <a href="https://remysharp.com/2025/10/27/how-has-ffconf-changed-since-it-was-known-as-full-frontal">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Signal Pollution</title>
      <guid isPermaLink="false">signal-pollution</guid>
      <link>https://remysharp.com/2025/09/25/signal-pollution</link>
      <pubDate>Thu, 25 Sep 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[Very recently I was forced to sign up to Meta due to a product purchase (don't at-me!) and I had forgotten what it was like to be part of the algorithms. Our entire family browse the internet (the web and internet) from behind a DNS proxy that blocks a lot of social media including Facebook/Meta/Insta/whatever it's actually called.]]></description>
      <content:encoded><![CDATA[
<p>Very recently I was forced to sign up to Meta due to a product purchase (don't at-me!) and I had forgotten what it was like to be part of the algorithms. Our entire family browse the internet (the web <em>and</em> internet) from behind a DNS proxy that blocks a lot of social media including Facebook/Meta/Insta/whatever it's actually called.</p>
<p>Unblocking these different services just to get a physical product to work (naively I thought it worked mostly offline) felt like I was exposing myself to a pretty gross network.</p>
<p>Then had to make an account for my child, whom if there were over the age of 10 and under the age of 13, could be managed under my account. Once they turned 13 (in the eyes of Meta) they were completely unrestricted (which I've got A LOT to say about, but I digress).</p>
<p>This combined with having already having to lift the protection I had in place was too much. I quickly deleted both accounts and parked the problem for the day.</p>
<p>After some reflection, I decided that I would create a single account, that we'd all share that way at least I could monitor all activity <em>and</em> the data Meta collected would be (hopefully) extremely mixed due to different people using the account.</p>
<hr>
<p>Which reminded me of a story I heard about <em>years</em> ago where villagers in some areas of Africa (and likely other places in the Global South) were sharing a single device to visit Facebook and the like, which made it nigh impossible for algorithms to profile the user and made targetting and ads effectively useless.</p>
<p><strong>Why don't we do this now?</strong> Why don't we share accounts to large social media sites to pollute the signal we're putting out?</p>
<p>I understand if you want to create an account so you can build your brand or post what you're up to, this doesn't really work. But if you're visiting these sites to keep in touch with your communities and post short comments or questions - then it seems to me that this is a viable solution.</p>
<p>If I were to propose implementing this, it would be that each shared-user would have 10-20 real people using the account, I can see how it might be chaotic with more people - plus there's an aspect of trust required in your group. If someone decides to change the password you're all of a sudden blown up.</p>
<p>Funnily enough, I've written about this same thought some <a href="https://remysharp.com/2019/06/27/a-thought-privacy-pollution">6 years ago</a>, prompted by this fun <a href="https://trackthis.link/">pollution tool</a>.</p>
<p>What do you think? What downsides am I missing here?</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/09/25/signal-pollution">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Fifteen</title>
      <guid isPermaLink="false">fifteen</guid>
      <link>https://remysharp.com/2025/08/30/fifteen</link>
      <pubDate>Sat, 30 Aug 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[I'd been waiting for the grief to find me. I wasn't actively looking for it, I know which memories to poke to feel real pain, but I wanted to create space for it to find me, and throughout the month of August, this year, it couldn't find me. Until today. 30th August. This day is the knife edge. The day, 15 years ago, that Tia still had a heartbeat, still kicked, was on her way. Julie was in (long) labour. On this same day, at some point, her heart gave out, she died before she could take her first breath and the midwives had to tell us that they couldn't find that heartbeat any more. Julie was in labour, so  Tia was coming. Except that her delivery at 3am on 31st August would be the other side of our lives. The side we live on today: our derailed and rebuilt lives that exist in now.]]></description>
      <content:encoded><![CDATA[<p>I'd been waiting for the grief to find me. I wasn't actively looking for it, I know which memories to poke to feel real pain, but I wanted to create space for it to find me, and throughout the month of August, this year, it couldn't find me. Until today. 30th August. This day is the knife edge. The day, 15 years ago, that <a href="https://remysharp.com/search?q=tia">Tia</a> still had a heartbeat, still kicked, was on her way. Julie was in (long) labour. On this same day, at some point, her heart gave out, she died before she could take her first breath and the midwives had to tell us that they couldn't find that heartbeat any more. Julie was in labour, so  Tia was coming. Except that her delivery at 3am on 31st August would be the other side of our lives. The side we live on today: our derailed and rebuilt lives that exist in now.</p>
<p>So today, the 30th of August, grief wakes me up in the morning. It presses hard downwards on me. And I need it, even if it's just for a day.</p>
<p>15 years didn't fix us, or heal us, <a href="https://remysharp.com/2014/08/11/time-doesnt-heal">time doesn't do that</a>, but it does allow for new memories. For more memories. For life to fill in the space. It's that time, and that life, and our amazing two children who create the life and family we needed. It keeps us busy and it keeps us alive.</p>
<p>We only got one day with Tia, to hold her. Then we had to leave her. Alone in the hospital. That's my memory that's fraught with pain and longing, and all kinds of complicated feelings. For a long time, in those early years, it was a dark memory, a memory I hated myself for, for leaving her. It took a year of therapy to put light into that room again, to not cast judgement on myself, to leave the memory as it is. Just that one day. The one day that she was real.</p>
<p>But that's why I write these post each year. It's why I write this for myself, and in part to share with you. Tia is a memory to me, but she was real. I have to write about that to bring her existence into reality, into the world I still inhabit.</p>
<hr>
<p>I was worried this year I wouldn't find the words, that the grief wouldn't find me. I know that there's an instinct in me to shove the grief away, to busy myself, but I also know that every other day of the year can do that with no effort at all. The little one starting secondary, the big one starting his options, new achievements, new problems, new arguments, new memories. So, I'll try my best to sit in the grief today. Share it with Julie, and try to be gentle on ourselves.</p>
<p>And I'll remember Tia today.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/08/30/fifteen">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Getting my highlights &amp; notes from KOReader</title>
      <guid isPermaLink="false">getting-my-highlights-and-notes-from-koreader</guid>
      <link>https://remysharp.com/2025/07/22/getting-my-highlights-and-notes-from-koreader</link>
      <pubDate>Tue, 22 Jul 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[It's not an intuitive process and requires a few speciality commands to work, so it made sense that I write up the process so I can duck myself later on.]]></description>
      <content:encoded><![CDATA[
<p>It's not an intuitive process and requires a few speciality commands to work, so it made sense that I write up the process so I can <a href="https://duckduckgo.com/?q=site%3Aremysharp.com+kindle&amp;t=newext">duck myself</a> later on.</p>
<p>Since I'm getting my <a href="https://remysharp.com/2025/05/01/showing-book-clippings-on-my-blog">Amazon Kindle book highlights</a> already but am also actively trying to buy books from <a href="https://remysharp.com/2025/06/29/unhooking-from-amazon-ebooks">anywhere that isn't Amazon</a> it means my highlights are in KOReader.</p>
<p>There's a multi-step process that requires special command line tools <em>and</em> navigating KOReader into the &quot;right&quot; state to allow for syncing (I'm certain there's other ways to do this process, but I've not found it yet myself).</p>
<h2>1. Export the highlights</h2>
<p>Probably the simplest part, and if you're happy to connect the Kindle via a USB port, probably a lot easier to get the exported notes off (but I'm taking the wireless route, so it's trickier!).</p>
<p>Whilst the current book is open, open the &quot;tools&quot; menu (the spanner and screwdriver icon), then <em>Export highlights</em> and <em>Export all notes in current book</em>.</p>
<p>I've already selected the format as JSON and selected my export folder (arbitrarily my exports are going to <code>/mnt/us/koreader/clipboard</code>).</p>
<p>Now the file is waiting to be lifted to my computer.</p>
<h2>2. WebDAV</h2>
<p>From the machine I want to upload the JSON file to, I need to run a WebDAV server (a technology I've always known about but never really had an need or use for until now).</p>
<p>There's quite a few options for WebDAV servers, but I wanted something very lightweight and that I could run on the command line. I ended up using a <a href="https://github.com/hacdias/webdav">Go based server</a>, via <code>brew install webdav</code>.</p>
<p>Which ever server you use, you'll need to make sure it has write permissions. To run this go version in write, in the directory I want the JSON file uploaded to, I run:</p>
<pre><code class="language-sh"><span class="token assign-left variable">WD_PERMISSIONS</span><span class="token operator">=</span>CRUD webdav <span class="token parameter variable">-p</span> <span class="token number">8181</span>
</code></pre>
<p>This says:</p>
<ol>
<li>Run on port 8181</li>
<li>Run with create, read, update and delete permissions</li>
</ol>
<p>Once I'm done with the process, I then terminate the WebDAV server.</p>
<p>However, before I shut the server down, I need to upload my JSON notes from the Kindle.</p>
<h2>3. Uploading to WebDAV from KOReader</h2>
<p>This is where KOReader's UI gets clunky again. You need to use the &quot;Cloud Storage&quot; option.</p>
<p>Cloud Storage is <em>only</em> visible in the tools menu when you're in the file browser (not in the book reading mode).</p>
<p>Once the Cloud Storage is open, tap the plus icon on the top right, then fill out the details. Assuming the Kindle is on the same network as the computer (because it needs to be), the URL/host is <code>http://{IP}:8181</code> and the folder is just <code>/</code>.</p>
<p>Once this is saved, tap on the connection name. If the directory is empty, you'll see a new screen with no files. If there's any <code>.pub</code> ebooks, then those will be listed (and could be downloaded).</p>
<p>From this new screen, tap the top left plus icon to upload a file. Navigate to the file, and long press on the file to &quot;choose&quot; the file to be uploaded.</p>
<p>If all is successful, KOReader should say so. If it fails, it could be related to permissions. I had earlier luck with a <a href="https://wsgidav.readthedocs.io/en/latest/">python WebDAV server</a>.</p>
<h2>4. Transform</h2>
<p>The JSON structure is fairly simplistic, but I have this specific <a href="https://jqterm.com">jq transform</a> to get into the format I use:</p>
<pre><code class="language-jq"><span class="token keyword">def</span> <span class="token function">slug</span><span class="token punctuation">:</span>
  <span class="token keyword">ascii_downcase</span>
  <span class="token operator pipe">|</span> <span class="token keyword">gsub</span><span class="token punctuation">(</span><span class="token string">"[^a-z0-9]+"</span><span class="token punctuation">;</span> <span class="token string">"-"</span><span class="token punctuation">)</span>
  <span class="token operator pipe">|</span> <span class="token keyword">gsub</span><span class="token punctuation">(</span><span class="token string">"(^-|-$)"</span><span class="token punctuation">;</span> <span class="token string">""</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token dot important">.</span> <span class="token operator">+</span>  <span class="token punctuation">{</span> <span class="token property-literal property">slug</span><span class="token punctuation">:</span> <span class="token punctuation">.</span>title <span class="token operator pipe">|</span> slug <span class="token punctuation">}</span> <span class="token operator pipe">|</span> <span class="token punctuation">{</span>
  <span class="token property">"<span class="token interpolation"><span class="token punctuation">\(</span><span class="token content"><span class="token punctuation">.</span>slug</span><span class="token punctuation">)</span></span>"</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>
    title<span class="token punctuation">,</span>
    author<span class="token punctuation">,</span>
    <span class="token property-literal property">highlights</span><span class="token punctuation">:</span> <span class="token punctuation">.</span>entries <span class="token operator pipe">|</span> <span class="token keyword">map</span><span class="token punctuation">(</span><span class="token punctuation">{</span> text<span class="token punctuation">,</span> page<span class="token punctuation">,</span> note <span class="token punctuation">}</span><span class="token punctuation">)</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Alternatively, if I'm lifting the annotations (highlights) directly from Calibre, this works:</p>
<pre><code class="language-jq"><span class="token keyword">def</span> <span class="token function">clean</span><span class="token punctuation">:</span> <span class="token keyword">tostring</span> <span class="token operator pipe">|</span> <span class="token keyword">gsub</span><span class="token punctuation">(</span><span class="token string">"\\\\\\n"</span><span class="token punctuation">;</span> <span class="token string">"\n\n"</span><span class="token punctuation">)</span> <span class="token operator pipe">|</span> <span class="token keyword">gsub</span><span class="token punctuation">(</span><span class="token string">"[’‘]"</span><span class="token punctuation">;</span> <span class="token string">"'"</span><span class="token punctuation">;</span> <span class="token string">"g"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">def</span> <span class="token function">percent</span><span class="token punctuation">(</span><span class="token variable">$total</span><span class="token punctuation">)</span><span class="token punctuation">:</span> <span class="token dot important">.</span> <span class="token operator">/</span> <span class="token variable">$total</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">;</span>

<span class="token keyword">def</span> <span class="token function">slug</span><span class="token punctuation">:</span>
  <span class="token keyword">ascii_downcase</span>
  <span class="token operator pipe">|</span> <span class="token keyword">gsub</span><span class="token punctuation">(</span><span class="token string">"[^a-z0-9]+"</span><span class="token punctuation">;</span> <span class="token string">"-"</span><span class="token punctuation">)</span>
  <span class="token operator pipe">|</span> <span class="token keyword">gsub</span><span class="token punctuation">(</span><span class="token string">"(^-|-$)"</span><span class="token punctuation">;</span> <span class="token string">""</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token dot important">.</span> <span class="token keyword">as</span> <span class="token variable">$_</span> <span class="token operator pipe">|</span> <span class="token punctuation">{</span> <span class="token property-literal property">slug</span><span class="token punctuation">:</span> <span class="token punctuation">.</span>stats<span class="token punctuation">.</span>title <span class="token operator pipe">|</span> slug <span class="token punctuation">}</span> <span class="token operator pipe">|</span> <span class="token punctuation">{</span>
  <span class="token property">"<span class="token interpolation"><span class="token punctuation">\(</span><span class="token content"><span class="token punctuation">.</span>slug</span><span class="token punctuation">)</span></span>"</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>
    <span class="token property-literal property">title</span><span class="token punctuation">:</span> <span class="token variable">$_</span><span class="token punctuation">.</span>stats<span class="token punctuation">.</span>title<span class="token punctuation">,</span>
    <span class="token property-literal property">author</span><span class="token punctuation">:</span> <span class="token variable">$_</span><span class="token punctuation">.</span>stats<span class="token punctuation">.</span>authors<span class="token punctuation">,</span>
    <span class="token property-literal property">highlights</span><span class="token punctuation">:</span> <span class="token variable">$_</span><span class="token punctuation">.</span>annotations <span class="token operator pipe">|</span> <span class="token keyword">to_entries</span> <span class="token operator pipe">|</span> <span class="token keyword">map</span><span class="token punctuation">(</span><span class="token punctuation">.</span>value <span class="token operator pipe">|</span> <span class="token punctuation">{</span> <span class="token property-literal property">text</span><span class="token punctuation">:</span> <span class="token punctuation">.</span>text <span class="token operator pipe">|</span> clean<span class="token punctuation">,</span> <span class="token property-literal property">page</span><span class="token punctuation">:</span> <span class="token string">"<span class="token interpolation"><span class="token punctuation">\(</span><span class="token content"><span class="token punctuation">.</span>pageno</span><span class="token punctuation">)</span></span>/<span class="token interpolation"><span class="token punctuation">\(</span><span class="token content"><span class="token variable">$_</span><span class="token punctuation">.</span>stats<span class="token punctuation">.</span>pages</span><span class="token punctuation">)</span></span>"</span><span class="token punctuation">,</span> <span class="token property-literal property">note</span><span class="token punctuation">:</span> <span class="token punctuation">(</span><span class="token punctuation">.</span>note <span class="token operator pipe">|</span> clean<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<hr>
<p>That's how I get my notes from non-Amazon bought books into my own blog.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/07/22/getting-my-highlights-and-notes-from-koreader">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Vibe coding and Robocop</title>
      <guid isPermaLink="false">vibe-coding-and-robocop</guid>
      <link>https://remysharp.com/2025/07/18/vibe-coding-and-robocop</link>
      <pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[I've been immersing myself in the AI news for the last few months, trying to get understanding of the landscape, what and why there's excitement, whether the ethics (both from a copyright and climate change perspective) are being considered, and what's the impact of my own usage.
In particular lately I've been looking at the idea of vibe coding. I've been playing with Claude Code which, as an experienced developer, is hard not to like (at time of writing!).
The short version of what I want to say is: vibe coding seems to live very squarely in the land of prototypes and toys. Promoting software that's been built entirely using this method would be akin to sending a hacked weekend prototype to production and expecting it to be stable.]]></description>
      <content:encoded><![CDATA[
<p>I've been immersing myself in the AI news for the last few months, trying to get understanding of the landscape, what and why there's excitement, whether the ethics (both from a copyright and climate change perspective) are being considered, and what's the impact of my own usage.</p>
<p>In particular lately I've been looking at the idea of vibe coding. I've been playing with <a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code</a> which, as an experienced developer, is hard <em>not</em> to like (at time of writing!).</p>
<p>The short version of what I want to say is: vibe coding seems to live very squarely in the land of prototypes and toys. Promoting software that's been built entirely using this method would be akin to sending a hacked weekend prototype to production and expecting it to be stable.</p>
<h2>Robocop</h2>
<p>Perhaps because I watched it when I was way too young to, but the original (1987) Robocop always pops into my mind when I think about unvetted coded going into a production level environment. Specifically Ed 209 being the prototype with the odd &quot;glitch&quot; (if you want an idea, here's a <a href="https://www.youtube.com/watch?v=TYsulVXpgYg">NSFW clip, heavy on the squibs</a>).</p>
<p>Robocop however was the version of the machine that had a human at the core to consider the protocols and determine the outcomes.</p>
<figure><img src="https://remysharp.com/images/robocop.jpg" alt="Ed 209 with the words &quot;Vibe Coding&quot; above it, and Robocop with &quot;Human Experience&quot; above it" decoding="async"></figure>
<p>Without Ed 209, there would be no Robocop, and in the world of Robocop, Robocop is (apparently) a good thing for society (which, going by Robocop 3, has rather gone to shit).</p>
<p>That's to say, in an analogy that I've rather stretched: prototypes entirely belong as part of the process.</p>
<h2>Vibe coding for toys</h2>
<p>If you're following AI it's likely you're following Simon Willison's work. He talks about using <a href="https://simonwillison.net/tags/vibe-coding/">vibe coding for creating tools and toys</a> (note this was linked July 2025).</p>
<p>I've used it myself to solve really bespoke problems where the user count is one. Most recently a tool I'd written years ago to manage our tax accounts and calculated all the VAT from Stripe stopped working (because I didn't maintain it and eventually all the libraries stopped working).</p>
<p>I turned to Claude Code and was able to create an extremely simple, but effective, single page app that would group the Stripe data (as we needed) and gave us the income and VAT that needed to be paid for the quarter. Then, again using Claude, I wrapped it up in <a href="https://tauri.app/">Tauri</a> so it could like on mine and Julie's computer (and <em>not</em> on the web) and it does the work. It took around 1-2 hour of gently giving direction to Claude Code and cost me around $5 of API tokens.</p>
<p>I can also tell you that this tool that was generated doesn't have tests. It's not accessible. It's not semantic, and it's certainly not written to the standard that I set for myself. However, that wasn't the aim, the aim was to get something working.</p>
<p>Would I put this out to production: absolutely not.</p>
<p>Though, perhaps I'm just a snowflake, because VCs are happy to throw money at anything that's popular in the moment. <a href="https://www.forbes.com/sites/iainmartin/2025/07/17/ai-vibe-coder-lovable-is-swedens-latest-unicorn/">Lovable, that has a $1.8B valuation</a> does exactly this: chat prompt =&gt; product for it's users. But then, VC culture has always been a weird thing to me.</p>
<h2>Consideration</h2>
<p>The problems I still don't have answers (even just for myself) to square away the rampant theft of content and the unprecedented energy consumption (along with data centers being built that take <a href="https://www.bbc.co.uk/news/articles/cy8gy7lv448o">priority over who gets the water</a>).</p>
<p>My concerns also sit with those people starting development careers. It's partly: if vibe, or AI assisted coding is all you learn, what happens when that generation of developers mature? This <em>feels</em> like something that might resolve itself (I'd love to see some research on this though), but moreover, money is required for vibe coding.</p>
<p>The costs of this kind of development cycle isn't really visible (yet). When you consider that junior developers or students won't have a lot of floating/free cash (if any), though I appreciate it's &quot;only&quot; cost me $5 to make the Stripe VAT tool, I've also spent quite a bit more on subscriptions and API costs before I got to that point.</p>
<p>I've got more to explore here, but I needed to get the Ed 209 vs. Robocop off my chest (albeit and excuse to actually use the analogy). Now, if you've not, go watch Robocop, it's peak late-80s over the top action (and probably violence, but in a way that makes you go - ew - see the &quot;I'm <em>melting</em>&quot; scene!).</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/07/18/vibe-coding-and-robocop">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Unhooking from Amazon ebooks</title>
      <guid isPermaLink="false">unhooking-from-amazon-ebooks</guid>
      <link>https://remysharp.com/2025/06/29/unhooking-from-amazon-ebooks</link>
      <pubDate>Sun, 29 Jun 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[Over the years we, as a family, have been moving our purchases away from Amazon, except in one single place: Kindle ebooks. For me it's that I'm incapable of reading physical books (but my kindle unlocked my reading), and with a Kindle, I was limited as to where I buy my books.
When I read that it was relatively easy to jailbreak all the Kindle models I used this as my opportunity to move to buying epub books and hopefully more of that money goes to the authors (in an ideal world…).
Here's how it's going so far. In short: not quite as well as I'd like.]]></description>
      <content:encoded><![CDATA[
<p>Over the years we, as a family, have been moving our purchases away from Amazon, except in one single place: Kindle ebooks. For me it's that I'm incapable of reading physical books (<a href="https://remysharp.com/2021/12/14/how-i-relearnt-to-read">but my kindle unlocked my reading</a>), and with a Kindle, I was limited as to where I buy my books.</p>
<p>When I read that it was relatively easy to <a href="https://kindlemodding.org/jailbreaking/">jailbreak all the Kindle models</a> I used this as my opportunity to move to buying epub books and hopefully more of that money goes to the authors (in an ideal world…).</p>
<p>Here's how it's going so far. In short: not <em>quite</em> as well as I'd like.</p>
<h2>Jailbreaking</h2>
<p>I won't cover the process here, but it wasn't too difficult. You have to properly identify your model (which is tricky enough), and then carefully follow the step by step process. It's lots of reading, clicking, double checking and waiting. The Kindle needs to be connected to the computer doing the work, it's tedious but gives you more features.</p>
<p>Importantly (for me) was that the full original system is intact. The jailbreak process adds a &quot;book&quot; (app) called KUAL which is the doorway to the custom e-reader - specifically I installed <a href="https://koreader.rocks/">KOReader</a> (though I've no idea if there's other readers available!).</p>
<h2>KOReader</h2>
<p>This is the app that will read the epub format (and most other e-book formats). A little frustrating that the Amazon purchased books are read using the original Kindle reader (they're the <a href="https://en.m.wikipedia.org/wiki/Kindle_File_Format">AZW3</a> ebook format - which has Amazon's DRM protection).</p>
<p>The UI is…not entirely intuitive. I spent probably the course of a week messing around with different settings, losing where the menu options were, being unsure why there's two types of config panels and completely missing the pagination on the settings. Eventually I found that there's a &quot;Menu Search&quot; (in the right hand side hamburger menu in &quot;Help&quot;).</p>
<p>There's some nice features for when the Kindle is turned off, besides having the book cover, I've got it set to show the cover, title, how long I've been reading it for and my current progress (search for &quot;wallpaper&quot;). I particularly like the &quot;book map&quot; feature (though, I can't say I visit often):</p>
<figure><img src="https://remysharp.com/images/koreader.jpg" alt="A complete map of the book, showing sections, chapters, progress and highlights I've made" decoding="async"></figure>
<h2>Getting ebooks / not giving money to Amazon</h2>
<p>This is really the draw for me. The short version is: this is not straightforward in the slightest, and Amazon have done a pretty good job of making the user experience pretty smooth (I believe, but don't have personal experience, that a Kobo reader has a similar workflow).</p>
<p>With Amazon, I can find the book I want using my phone, buy it, and sync my Kindle and it's ready to be read. I've managed to do this abroad even without wifi because I can hotspot my Kindle to my phone to pick up the new book.</p>
<p>There's two problems to overcome to get this kind of experience:</p>
<ol>
<li>Automatic syncing of newly purchased books</li>
<li>DRM stripping</li>
</ol>
<p>There's a few ways to handling syncing, none of them entirely straight forward.</p>
<p>If you're familiar with <a href="https://calibre-ebook.com/">Calibre</a>, From here there's a KOReader plugin that your Kindle can connect to and &quot;send to main&quot; will send the book to KOReader - though this is obviously not automated.</p>
<p>There is a &quot;cloud sync&quot; option in KOReader that can connect to Dropbox (and FTP if you like). This could get books across, but the biggest issue I came up against is the DRM removal.</p>
<p>DRM stripping can all be done in Calibre, but through my exploration, I couldn't find a way to automate this, even from the command line.</p>
<p>From my (little) experience, there's two parts to the DRM removal (so your bought book can be read on KOReader):</p>
<ol>
<li>The book comes in a .ascm format - a pointer file that is serviced by <a href="https://www.adobe.com/uk/solutions/ebook/digital-editions.html">Adobe Digital Editions</a> which only then gives you a DRM encoded epub (or PDF).</li>
<li>Once I've got the epub, I can use <a href="https://github.com/apprenticeharper/DeDRM_tools">Calibre's DeDRM tools</a> so that my book is finally portable.</li>
</ol>
<p>The problem with all this is that it requires that you drive the software to do the work.</p>
<p>I did find a <a href="https://github.com/Leseratte10/acsm-calibre-plugin">DeASCM plugin</a> which worked on the Adobe Digital Editions sample book, but didn't work on the <a href="https://www.goodreads.com/book/show/38739384-in-bloom">book I had bought</a> from Google's store (though it <em>had</em> successfully fulfilled the DRM fulfilment, but just couldn't get the epub out).</p>
<p>So the workflow I have is clunky, and definitely not compatible if I'm away from home reading a book (and it's usually holidays that I'll impulse buy a book).</p>
<h2>The other downsides</h2>
<p>There's a few other factors against me when trying to cut Amazon out of the chain of suppliers:</p>
<ol>
<li>Amazon have a near monopoly on ebooks. Okay, maybe not quite, but every book publisher I've visited since jailbreaking my Kindle has pointed me (often exclusively) to Amazon to buy their book.</li>
<li>Amazon have deals on books all the time - which, if I'm honest with myself, is appealing (because I save money) but almost certainly doesn't help the authors.</li>
<li>As much as <a href="https://www.goodreads.com/author/show/4048781.Remy_Sharp">Goodreads</a> sucks, I do use the &quot;mark as reading&quot; and have pulled <a href="https://remysharp.com/2025/05/01/showing-book-clippings-on-my-blog">my book highlights</a> from their service into my blog. I do have a work around for KOReader, but again it's even more manual.</li>
<li>Fiction (or the fiction I read) is almost always in this DRM-land (and I'm slow AF reading non-fiction, so I don't do it often)</li>
</ol>
<p>Hopefully I can revisit this process in the coming years and there will be some process to help me streamline the work.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/06/29/unhooking-from-amazon-ebooks">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>AI: did you check your work?</title>
      <guid isPermaLink="false">ai-did-you-check-your-work</guid>
      <link>https://remysharp.com/2025/05/31/ai-did-you-check-your-work</link>
      <pubDate>Sat, 31 May 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[There's no denying that the web industry, as with many others, has AI and LLMs as a ubiquitous presence. There's all kinds of different uses for LLMs, and comes with all the ethical concerns - ignored or perhaps at the heart of your use.
More recently &quot;vibe coding&quot; has me…wary.]]></description>
      <content:encoded><![CDATA[
<p>There's no denying that the web industry, as with many others, has AI and LLMs as a ubiquitous presence. There's all kinds of different uses for LLMs, and comes with all the ethical concerns - ignored or perhaps at the heart of your use.</p>
<p>More recently &quot;vibe coding&quot; has me…wary.</p>
<p>Very recently I had a fairly simple, but dull task where I had to extract the number of lessons from a log (which read &quot;23 lessons&quot; etc) and add them up.</p>
<p>This on the surface would seem like a perfect match for an LLM. Give it the log, describe the problem and the bot spits out a single number.</p>
<p>I threw this into chatgpt and it confidently (as always) gave me a number.</p>
<p>A number, that just didn't feel right at all. I already had a sense of where the result should be, and chatgpt gave me a number that was way too low.</p>
<p>Intrigued, I decided to repeat the exact same request to a number of other models, including: Gemini, Claude, multiple OpenAI models, and a couple of offline models.</p>
<p>All, except one got the wrong answer, each having wildly different values. They weren't just a bit off, they were simply wrong. Then the problem with <em>one</em> being right, unless you know the answer ahead of time, how do you tell?</p>
<figure><img src="https://remysharp.com/images/ai-counting.jpg" alt="Multiple LLMs showing different answers to the same question" decoding="async"></figure>
<p>The actual answer took me a few minutes to code up and I trust my skills for a particularly simple task. But I didn't start by coding it myself because it took less than 30 seconds to describe the problem in written English, and I <em>thought</em> these are the kinds of tasks perfectly suited to LLMs.</p>
<p>Perhaps if I had asked the LLM to write me a script, it would have had a better outcome, but they would require me then saving and running the script myself, which, sort of defeated the point of throwing the problem to the LLM.</p>
<h2>Why does this concern me?</h2>
<p>The simplicity of the task is one that, ideally, wouldn't require the kind rigour you might consider if the LLM were writing code for your code base.</p>
<p>Compound this with the simple fact that both LLMs are not deterministic (ie. if I host a local LLM it's not going to give me the same answer, or rather it won't use the same pathway), and if you're using a remote/online LLM the models update under your feet.</p>
<p>I'm sure this is because I'm not &quot;controlling&quot; the LLM in the best possible way. I recently read Simon Willison explaining that with 2 years of deep experience with LLMs the power user skill is about controlling the context the LLM has to guide it to the best outcome.</p>
<p>But most users aren't power users.</p>
<p>I don't doubt that this will change both for the positive and negative over time.</p>
<p>Is this just a hallucination? Do we seriously believe that developers will continuously check for hallucinations? I don't.</p>
<p>The web development industry moved from copying snippets of code to solve problems to installing npm modules for the answer. That's to say, due diligence has reduced over time in favour of DX.</p>
<p>Are developers going to validate everything that comes out of their LLM, particularly as vibe coding (however interpreted) becomes popular?</p>
<p>I worry we won't. I worry that asking for more rigour is too tall of an ask.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/05/31/ai-did-you-check-your-work">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Showing book clippings on my blog</title>
      <guid isPermaLink="false">showing-book-clippings-on-my-blog</guid>
      <link>https://remysharp.com/2025/05/01/showing-book-clippings-on-my-blog</link>
      <pubDate>Thu, 01 May 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[After jailbreaking my Kindle and seeing how simple it was and how all existing functionality was retained, I spotted that there was a My Clippings.txt file on the Kindle when mounted (I'm sure it's always there, I just hadn't mounted before).
This prompted me to get all my clippings (or as I think of them highlights) onto my blog since I already have all my books.]]></description>
      <content:encoded><![CDATA[
<p>After <a href="https://kindlemodding.org/mesquito/">jailbreaking my Kindle</a> and seeing how simple it was and how all existing functionality was retained, I spotted that there was a <code>My Clippings.txt</code> file on the Kindle when mounted (I'm sure it's always there, I just hadn't mounted before).</p>
<p>This prompted me to get all my clippings (or as I think of them <em>highlights</em>) onto my blog since I already have all <a href="https://remysharp.com/books">my books</a>.</p>
<h2>What failed</h2>
<p>Although the <code>My Clippings.txt</code> file was over 450Kb of content, it took me a long while to realise it contained <em>everything</em> that I highlighted, including when I was adjusting the selection on the Kindle.</p>
<p>What this means is that I have these <em>individual</em> clippings:</p>
<pre><code>- Added on Saturday, 19 May 2018 23:13:50
in sleep, had stopped dead and started again after
==========
- Added on Saturday, 19 May 2018 23:14:15
in sleep, had stopped dead and started again after a blank
==========
- Added on Sunday, 20 May 2018 15:47:35
You believe
==========
- Added on Sunday, 20 May 2018 15:47:39
You believe that reality is something objective, external,
existing in its own right.
==========
- Added on Sunday, 20 May 2018 15:48:17
You believe that reality is something objective, external,
existing in its own right. You also believe that the nature
of reality is self-evident. When you delude yourself into
thinking that you see something, you assume that everyone
else sees the same thing as you.
</code></pre>
<p>You can see it's captured every single interaction attempt at highlighting. Suddenly I can see how I managed nearly half a meg of clippings.</p>
<p>The clipping file does have additional metadata, including the book title, author and location - but given the content is (probably mostly) junk, I decided I had to get the clips from elsewhere.</p>
<h2>Getting data from Goodreads and Amazon</h2>
<p>The Goodreads API is notoriously rubbish. Their RSS feed of books you've read (i.e. books <em>I've</em> read) can sometimes, often, omit data that I know Goodreads has. So I wasn't looking forward to trying to use their site for the clippings.</p>
<p>Although I tried and failed to sniff the traffic from my Kindle when I rate a book or sync (I had hoped to capture a message to Amazon or Goodreads and intercept it), I realised I don't actually know where the &quot;I'm reading this now&quot; and the syncing actually goes.</p>
<p>It's possible it goes directly to Goodreads (I thought Amazon bought them). It's possible it goes to Amazon and then <em>across</em> to Goodreads (though, I somehow doubt that).</p>
<p>Either way, long story short, I found that there's an easily scrapable (not sure that's a word) web page on Amazon: <a href="https://read.amazon.com/notebook">https://read.amazon.com/notebook</a></p>
<p>It does also look like any changes on Goodreads (like when you make your highlights public) are pushed to this Amazon page.</p>
<p>So let's scrape the crap out of that page to get our data out.</p>
<h2>Scraping clippings</h2>
<p>The page <code>https://read.amazon.com/notebook?asin=${id}</code> contains HTML fragments that holds all the book data and highlights (and notes you made on your Kindle's janky keyboard).</p>
<p>So we'll scan the DOM for all the <code>asin</code> ids and loop through each one, extracting all the content:</p>
<pre><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">parseFromPage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// via https://read.amazon.com/notebook</span>
  <span class="token keyword">const</span> ids <span class="token operator">=</span> <span class="token function">$$</span><span class="token punctuation">(</span><span class="token string">'#kp-notebook-library > div'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">_</span><span class="token punctuation">)</span> <span class="token operator">=></span> _<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> res <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">credentials</span><span class="token operator">:</span> <span class="token string">'include'</span><span class="token punctuation">,</span>
    <span class="token literal-property property">mode</span><span class="token operator">:</span> <span class="token string">'cors'</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>

  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> id <span class="token keyword">of</span> ids<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token comment">// get the plain text from each fetch</span>
    <span class="token keyword">const</span> t <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>
      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://read.amazon.com/notebook?asin=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;contentLimitState=&amp;</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
      options<span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">_</span><span class="token punctuation">)</span> <span class="token operator">=></span> _<span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// then parse it with the extractKindleHighlights</span>
    res<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token function">extractKindleHighlights</span><span class="token punctuation">(</span>t<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token keyword">return</span> res<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Now we need to parse it. You could go down the regexp path, but since we're still in the browser, we can take advantage of the DOM for navigating and iterating.</p>
<p>We already have the response as text, so the process is to <code>createDocumentFragment()</code> and insert the text as HTML into the fragment (though actually I'll put a single DOM element in the root of the fragment, then append to this):</p>
<pre><code class="language-js"><span class="token keyword">const</span> frag <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createDocumentFragment</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> root <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'div'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
root<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> html<span class="token punctuation">;</span>
frag<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>root<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>Using a fragment keeps it out of the main document DOM (in case it causes it go awry) but still affords us some <code>querySelector</code> goodness.</p>
<pre><code class="language-js"><span class="token keyword">function</span> <span class="token function">extractKindleHighlights</span><span class="token punctuation">(</span><span class="token parameter">html</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">author</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span>
    <span class="token literal-property property">title</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span>
    <span class="token literal-property property">highlights</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>

  <span class="token keyword">const</span> frag <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createDocumentFragment</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> root <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'div'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  root<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> html<span class="token punctuation">;</span>
  frag<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>root<span class="token punctuation">)</span><span class="token punctuation">;</span>

  result<span class="token punctuation">.</span>title <span class="token operator">=</span> root<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'h3'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  result<span class="token punctuation">.</span>author <span class="token operator">=</span> root<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'h3'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>nextElementSibling<span class="token punctuation">.</span>textContent<span class="token punctuation">;</span>

  <span class="token keyword">const</span> highlightElements <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>
    root<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'#kp-notebook-annotations > .a-row.a-spacing-base'</span><span class="token punctuation">)</span>
  <span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> highlightElement <span class="token keyword">of</span> highlightElements<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> el <span class="token operator">=</span> highlightElement<span class="token punctuation">.</span>firstChild<span class="token punctuation">;</span>
    <span class="token keyword">const</span> page <span class="token operator">=</span> el
      <span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'.kp-notebook-metadata'</span><span class="token punctuation">)</span>
      <span class="token punctuation">.</span>textContent<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">':'</span><span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">pop</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
      <span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">const</span> text <span class="token operator">=</span> el<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'.kp-notebook-highlight'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token keyword">const</span> note <span class="token operator">=</span> el
      <span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'.kp-notebook-note span:last-child'</span><span class="token punctuation">)</span>
      <span class="token punctuation">.</span>textContent<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    result<span class="token punctuation">.</span>highlights<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
      text<span class="token punctuation">,</span>
      <span class="token literal-property property">note</span><span class="token operator">:</span> note <span class="token operator">||</span> <span class="token keyword">null</span><span class="token punctuation">,</span>
      <span class="token literal-property property">page</span><span class="token operator">:</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>page<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">,</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">return</span> result<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Now the final format is more like structure JSON (I do some extra work to match up the book title to my existing reviews in markdown):</p>
<pre><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"author"</span><span class="token operator">:</span> <span class="token string">"George Orwell and Thomas Pynchon"</span><span class="token punctuation">,</span>
  <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"Nineteen Eighty-Four (Penguin Modern Classics)"</span><span class="token punctuation">,</span>
  <span class="token property">"highlights"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
    <span class="token punctuation">{</span>
      <span class="token property">"text"</span><span class="token operator">:</span> <span class="token string">"The solid, contourless body, like a block of granite, and the rasping red skin, bore the same relation to the body of a girl as the rose-hip to the rose. Why should the fruit be held inferior to the flower?"</span><span class="token punctuation">,</span>
      <span class="token property">"note"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span>
      <span class="token property">"page"</span><span class="token operator">:</span> <span class="token number">219</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">{</span>
      <span class="token property">"text"</span><span class="token operator">:</span> <span class="token string">"You believe that reality is something objective, external, existing in its own right. You also believe that the nature of reality is self-evident. When you delude yourself into thinking that you see something, you assume that everyone else sees the same thing as you."</span><span class="token punctuation">,</span>
      <span class="token property">"note"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span>
      <span class="token property">"page"</span><span class="token operator">:</span> <span class="token number">249</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">]</span>
<span class="token punctuation">}</span>
</code></pre>
<p>So now I have my clippings on <a href="https://remysharp.com/books/2018/1984">my books like 1984</a>.</p>
<hr>
<p>The one (rather important) bit that I've not quite worked out, is how to fully automate this. I suspect I'll need something either like a headless browser to do the work, or something with my Amazon credentials which isn't ideal.</p>
<p>What <em>would</em> be perfect, is it I could find a way to capture the clippings directly from the Kindle, especially since it's jailbroken now - though I suspect this is an extremely tricky problem to crack.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/05/01/showing-book-clippings-on-my-blog">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>How I made an LED driver smart…</title>
      <guid isPermaLink="false">how-i-made-an-led-driver-smart</guid>
      <link>https://remysharp.com/2025/04/18/how-i-made-an-led-driver-smart</link>
      <pubDate>Fri, 18 Apr 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[…by just a little.
Our family bathroom has a cabinet with an IR sensor that turns on LEDs in the side of the cabinet. The IR sensor is &quot;wave your hand under the cabinet&quot; and the lights go on or off. The littlest uses this light (instead of the overhead which is connected to the extractor fan - i.e. loud) as a night light.
Since it's usually left on at night, I wanted to give the LEDs some smarts so when I go to bed, it automatically turns off with my blanket &quot;turn all the lights out&quot; home assistant command.]]></description>
      <content:encoded><![CDATA[
<p>…by just a little.</p>
<p>Our family bathroom has a cabinet with an IR sensor that turns on LEDs in the side of the cabinet. The IR sensor is &quot;wave your hand under the cabinet&quot; and the lights go on or off. The littlest uses this light (instead of the overhead which is connected to the extractor fan - i.e. loud) as a night light.</p>
<p>Since it's usually left on at night, I wanted to give the LEDs some smarts so when I go to bed, it automatically turns off with my blanket &quot;turn all the lights out&quot; home assistant command.</p>
<h2>How the cabinet worked, emphasis on past tense</h2>
<p>After I removed the doors and the enclosure housing all the electrics, I found the following inside:</p>
<ol>
<li>Mains supply coming in</li>
<li>Goes into a mains power transformer to reduce the current</li>
<li>Supply is then split between a &quot;shaver socket&quot; (which charges our toothbrushes) and a control box that takes input from the IR sensor and outputs to an LED driver</li>
<li>The control box is responsible for converting the signal from the IR sensor to a relay, if the relay is closed it then powers the LED driver</li>
<li>The LED driver then lights up the cabinet</li>
</ol>
<figure><img src="https://remysharp.com/images/cabinet_before.jpg" alt="The wiring before I got my hands on it" decoding="async"></figure>
<p>I'd found that the voltage going to the IR sensor was 5V which was perfect for an ESP32 and I had planned to incept the IR sensor and then send signals to the control box to &quot;pretend&quot; hand movement was detected, and thus turn the lights off.</p>
<p>That completely failed, and in the end I would connect to the output of the LED driver (which has stepped down to 12V) and add my own relay.</p>
<h2>First though, here's the mistakes I made</h2>
<p>There were a few (though hopefully you guessed the biggest one already - I'll save that for the end).</p>
<h3>1. Current</h3>
<p>The 5V that powered the IR sensor didn't provide enough current to boot up the ESP32. I'm not sure <em>how</em> much current it was providing, but when I connected the ESP to the control box it just wouldn't complete the boot process, which is (usually) indicative of not having enough current.</p>
<p>I think this makes sense, as the IR sensor wouldn't need a lot of current to detect a change.</p>
<h3>2. Shorting</h3>
<p>I decided perhaps I could pick up a decent supply elsewhere on the control box. I went probing around and noticed my multimeter wasn't really giving me the values I expected. Until, rather quickly, a loud BANG and flash made me jump and I realised I had blown the circuit.</p>
<p>The mistake here was that at some point, I had put my multimeter in current mode (which requires removing one of the probes and plugging it into a different socket). What that actually means is that when I probed the control box for voltage, instead of finding it, I created a short across the mains live and neutral. Hence the bang.</p>
<p>Thankfully the fuse box had just flipped the breaker.</p>
<p>After removing the control box and inspecting, I found that the trace I had been probing had been blown right off the PCB and eventually I spotted that my multimeter probe tips had melted!</p>
<h3>3. Assuming it would be okay after blowing</h3>
<p>After cleaning up the control box I decided that the only damage was my multimeter and the trace (which I had repaired with copious solder). I reinserted the control box, flipped the breaker only for it to immediately flip back down along with a bang (again) from the bathroom.</p>
<p>The control box had obviously been damaged (and frankly I should have known better), but now it was <em>really</em> dead. Lots of soot like black around components that I don't particularly recognise.</p>
<p>So now the cabinet lights are dead (well, the trigger mechanism) and it's coming to night time (since the electrics would be off for me to work on it, I'd be in the dark) - so the kids' night light was hosed for the next few nights.</p>
<figure><img src="https://remysharp.com/images/control_blown.jpg" alt="The control box completely blown" decoding="async"></figure>
<h3>4. Did I really make a better wheel?</h3>
<p>Now this is the big mistake: I'm fairly sure I made this smart for the sake of making something smart, <em>not</em> because it would enhance things.</p>
<p>I was stuck in this mess because I wanted to automate turning the light off at bedtime, but each night the process has been incredibly simple: right before I go to bed (and the kids are finally asleep), I lean into the bathroom, wave my hand, then carry on to bed.</p>
<p>I've written over 1,100 words, and spent a good few hours dismantling, fixing, breaking, testing, fixing and finally reassembling all to save myself from waving my hand in the evening. Even as I write, although the lights work again, the IR sensor doesn't - I <em>still</em> need to restore that functionality.</p>
<p>Lesson: weigh the pros and cons first, rather than diving straight in 🤦</p>
<h2>The smart LED driver</h2>
<p>Since I had blown the original control box beyond useful, I had to set my work on the output side of the LED driver.</p>
<p>I reused the relay from the control box and use a transistor to turn the relay on (because the ESP32-C3 GPIO isn't 5V safe, and the relay needed 5V).</p>
<figure><img src="https://remysharp.com/images/cab_esp_wiring.jpg" alt="The ESP32 wiring" decoding="async"></figure>
<p>This is what the schematic looks like (chatgpt suggested I include a flyback diode, which worked, but I really need to read up as to how and why - it's related to voltage spikes IIRC):</p>
<figure><img src="https://remysharp.com/images/smart-led-driver.png" alt="Smart LED driver schematic" class="bwimg" decoding="async"></figure>
<p>I also have a heat sink on the back of the 12V regulator. Finally, when this booted up, it was able to control the lights - or rather specifically: I was able to turn the lights <em>on</em> again.</p>
<figure><img src="https://remysharp.com/images/cabinet_lights_up.jpg" alt="The lights are finally on" decoding="async"></figure>
<p>The <a href="https://esphome.io">esphome</a> config revolves around this small snippet:</p>
<pre><code class="language-yaml"><span class="token key atrule">light</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> binary
    <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">'Cabinet Lights'</span>
    <span class="token key atrule">output</span><span class="token punctuation">:</span> cabinet_lights_output

<span class="token key atrule">output</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> gpio
    <span class="token key atrule">pin</span><span class="token punctuation">:</span> GPIO1
    <span class="token key atrule">id</span><span class="token punctuation">:</span> cabinet_lights_output
</code></pre>
<p>It did then appear on my home assistant panel, and although the IR sensor is still disconnected, at least I can turn on the light (plus the littlest can use their Google Home voice thingy to turn the cabinet lights on too).</p>
<figure><img src="https://remysharp.com/images/ha-cabinet.png" alt="Home assistant showing me control over the cabinet lights" decoding="async"></figure>
<p>What a palava.</p>
<h2>What remains</h2>
<p>I do need to restore the IR sensor because that's the primary way of interacting with the light.</p>
<p>I would have liked to include a fuse where the 12V comes in, but I forgot to originally so to put it in requires disassembling the whole thing (which I will probably do).</p>
<p>I <em>could</em> put in a MM wave sensor to be used to turn the lights on automatically, but referring back to mistake number 4, I think perhaps the project doesn't <em>really</em> need it…</p>
<p>An interesting learning experience, with important learning lessons for me, specifically: <strong>if it ain't broke, don't fix it</strong>.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/04/18/how-i-made-an-led-driver-smart">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Do politics belong at web events?</title>
      <guid isPermaLink="false">do-politics-belong-at-web-events</guid>
      <link>https://remysharp.com/2025/04/07/do-politics-belong-at-web-events</link>
      <pubDate>Mon, 07 Apr 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[This question has been asked before and discussed before and I've always looked on from the sidelines, even though, as a conference organiser, I do in fact have fairly strong opinions about this.]]></description>
      <content:encoded><![CDATA[
<p>This question has been asked before and discussed before and I've always looked on from the sidelines, even though, as a conference organiser, I do in fact have fairly strong opinions about this.</p>
<p>Firstly there's the simple idea that a web event would be possibly &quot;pure&quot; tech. Certainly FFConf hasn't been 100% technical for over a decade, but the person giving talks always infuses their personality into their talks. Jokes, fun, passion, excitement, confusion - all of these add to talks, or certainly the ones we host at FFConf.</p>
<p>But perhaps views on politics is somehow different?</p>
<p>The web that I like to promote and support, and the one that many of my peer events support, is one of inclusivity and welcomeness. This means creating accessible environments. This means creating teams that are open minded. This means holding the door open for those who need it.</p>
<p>Nothing is free of politics in this era as it does indeed impact nearly everything we do. Even moreso today. American politics have a direct impact on UK politics and culture - there's no ignoring that. Equally the world climate affects how we're all operating.</p>
<p>Although my own politics stem primarily from the human side of things, recent activity in US politics, has spurred on many europeans to move our technology <em>away</em> from the US.</p>
<p>The <a href="https://european-alternatives.eu/categories">European Alternatives web site</a> has been doing the rounds lately and shares many good alternatives for us to move to, disconnecting our (read: <em>my</em>) dependency on the US for technology.</p>
<p>The web, and particularly the kind of web that we advocate at our events, is from a progressive point of view (talks on accessibility is an easy example of this), and so you'll find these particular kinds of events, those put together with love and care for others, will usually have strong political points of view that support the ideal web.</p>
<p>Politics <em>is</em> relevant to working on the web. It's relevant to how we progress the way we work and the ways we want to work.</p>
<p>Everything is political and if you're siding with the racist, fascist, homophobes, transphobes and those engaged in genocide, then I'm afraid you're in for a rough ride because that creates a closed, private, elite and corroded version of the web.</p>
<p>Discourse is fine. I was asked how I'd feel if I was at an event that was web based, and the speaker was very pro Trump and Trump agendas - then the MC agreed. How would I react? I'd feel like they were wrong, and I'd likely not want to attend again. Which is fine.</p>
<p>What isn't okay, is if I then start to harass the speaker or become violent towards them. It's obvious, but the code of conduct would (and should) eject me immediately (at a bare minimum). Infusing a web talk with your political leaning and ideals does not equate hate speech.</p>
<p>In an excellent talk I saw last week, the first slide, which had nothing to do with the talk itself and not even mentioned, simply read &quot;protect trans kids&quot;. This is part of our web. The web where we hold the door open for others. Because <em>we</em> are the web.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/04/07/do-politics-belong-at-web-events">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>The day piracy changed</title>
      <guid isPermaLink="false">the-day-piracy-changed</guid>
      <link>https://remysharp.com/2025/03/22/the-day-piracy-changed</link>
      <pubDate>Sat, 22 Mar 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[It certainly wasn't today. It was some time ago, but I wanted to mark this in my blog as a reminder that once, long ago, piracy was, well, stealing.
That's all changed now.]]></description>
      <content:encoded><![CDATA[
<p>It certainly wasn't today. It was some time ago, but I wanted to mark this in my blog as a reminder that once, long ago, piracy was, well, stealing.</p>
<p>That's all changed now.</p>
<p>We, the web people, already knew that companies had, without permission, slurped up all our content to train their LLMs.</p>
<p>To some degree it was legitimate (though I'm not sure the licence on the site would be respected, such as creative commons) - our blog posts were on the open, and public web.</p>
<p>I'm certainly not justifying the carte blanche scraping and repurposing of our blog posts to help line the already rich pockets of tech bros.</p>
<p>However, this latest realisation has completely redefined piracy. I think I could happily argue that the concept of piracy is lost and gone.</p>
<p>I recently read Stephie Stimacs' post entitled &quot;<a href="https://blog.stephaniestimac.com/posts/2025/03/ethical-ai/">In which I discover my book has been scraped by Meta for its AI&quot;</a> - which you might be able to guess the conclusion just from the title.</p>
<p>As I was reading, I was reminded of a post I'd seen on BlueSky earlier that week:</p>
<blockquote>
<p>it’s possible to not care if people (humans, readers) pirate my book—or even feel pleased they do, if anything; and, at the same time, express fury when corporations grab and grind the text up like scrap metal for their word-garbage generators</p>
</blockquote>
<ul>
<li><a href="https://bsky.app/profile/jomc.bsky.social/post/3lkt6mwcsps2h">joanne mcneil
@jomc.bsky.social</a></li>
</ul>
<p>With all this in mind, I thought I'd check books I had written and contributed to in this LibGen database, and yep, my work was in there too. I'd go so far as bet that if you've written a book, it is too.</p>
<p>So what? So, <strong>Meta knowingly took pirated content and used it to train their models.</strong></p>
<p><a href="https://www.rollingstone.com/culture/culture-news/ai-meta-pirated-library-zuckerberg-1235235394/">The complicity goes all the way to the top.</a></p>
<p>There's piracy behind closed doors and private use (bad), and there's what Meta did (and yes, likely the other companies already rolling in cash they could have actually paid for this content). They've taken pirated content and they're selling it as their own.</p>
<p>What are the ramifications?</p>
<p>Actual ROFL. Are you fucking kidding me? Those with power have proven time and time again in this last decade that they're not accountable. The answer to no one. There's no ramifications. None.</p>
<p>So. I wanted to mark on my blog, for posterity's sake, that piracy is dead. Because without an reaction there is no action, ergo: piracy doesn't really exist in today's age of AI and the rich and powerful.</p>
<p>Though, I somehow suspect the hammer is being kept on the side just in case us little people step out of line and &quot;pirate&quot; something for our own use.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/03/22/the-day-piracy-changed">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Devs: draw your line</title>
      <guid isPermaLink="false">devs-draw-your-line</guid>
      <link>https://remysharp.com/2025/03/08/devs-draw-your-line</link>
      <pubDate>Sat, 08 Mar 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[This post is for my developers out there, web and otherwise. We have super powers. We can make something functional from practically nothing. And you know what they say about great power…
So this is short and sweet: know where you draw the line and stick to your god damn guns.]]></description>
      <content:encoded><![CDATA[
<p>This post is for my developers out there, web and otherwise. We have super powers. We can make something functional from practically nothing. And you know what they say about <a href="https://duckduckgo.com/?q=with+great+power">great power</a>…</p>
<p>So this is short and sweet: know where you draw the line and stick to your god damn guns.</p>
<h2>A case study</h2>
<p>For repeat prescriptions I use a site called <a href="https://www.pharmacy2u.co.uk/">Pharmacy2U</a> (who took over for Lloyds Pharmacy for fulfilment … I guess). The terrible name should be a give away to the quality of the site, but really for repeat prescriptions the web site interaction should be limited to: email alert, click, pay, close.</p>
<p>I've recently been browsing the web using the <a href="https://noscript.net/">NoScript</a> extension and deciding manually which scripts to enable (a point to note: nearly all checkout I've encountered requires JavaScript).</p>
<p>It was because of this script that I noticed that some developer, somewhere in the stack that put together the Pharmacy2U web site decided it was okay to share data with Facebook and TikTok.</p>
<p>I can understand (nearly) wanting to find where a business' traffic is coming from. Perhaps they're writing articles about health and wanting to track metrics (or something).</p>
<p>However, these 3rd party tracking scripts were on the prescription pages. The pages that list what medication I'm on.</p>
<p>Facebook and TikTok had been given access to read what medication I'm on by the development team responsible for the Pharmacy2U web site.</p>
<p>If that's you, sorry-not-sorry, but <strong>shame on you</strong>.</p>
<h2>Stop making our failing privacy worse</h2>
<p>As developers we're the ones that control whether these tracking scripts are included. We're the ones responsible for the security of our user's content. Some content is <em>obviously</em> more sensitive than other data. This <em>should</em> be common sense.</p>
<p>Regardless of whether developers specifically inserted the tracking scripts into the medication pages, or if they developed a CMS of sorts that let's someone insert the scripts into pages - it's still software that we developers made.</p>
<p>Analytics in and of itself isn't bad. Putting scripts that gives unfettered access to medical information is wrong. I suspect this also puts some individuals at risk too.</p>
<p>Take a stand, draw a line, and stick to it. Businesses and capitalism will want to move that line, give it some <em>wiggle room</em>, but fuck that. You're not a bad person, you're just doing your job, but as a developer, like I said, you have super powers. Use them. Stop enabling this gross misuse of software.</p>
<h2>Epilogue</h2>
<p>Even though Google very recently removed the excellent <a href="https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm?pli=1">uBlock Origin</a> - it's still very much alive and available to <a href="https://ublockorigin.com/">block trackers</a> where they (trackers) don't belong.</p>
<p>Equally, for me, using <a href="https://noscript.net/">NoScript</a> blocks these in my desktop browser, but I also use <a href="https://pi-hole.net/">Pi-Hole</a> in our home network, and I've been considering using <a href="https://nextdns.io/?from=4azjnggh">NextDNS</a> for my mobile outside of my home network.</p>
<p>So, thankfully, I'm protected, but that's only because I've got the super powers too. Mum and Dad aren't protected in the same way. I'm fairly sure my siblings aren't either.</p>
<p>It's a weird situation we've gotten ourselves in that we're having to actively fight to have a safe web.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/03/08/devs-draw-your-line">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Upgrading USB devices to USB-C</title>
      <guid isPermaLink="false">upgrading-usb-powered-devices-to-usbc</guid>
      <link>https://remysharp.com/2025/02/24/upgrading-usb-powered-devices-to-usbc</link>
      <pubDate>Mon, 24 Feb 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[For a small home project, my son suggested adding some under lights to a plant stand we recently added to our kitchen. I figured we could do this with a cheap LED strip and it could connect to the &quot;house&quot; using Zigbee and Home Assistant.
All was well until I realised the plug socket nearest the plants already had it's USB port in use and only the USB-C port was available. No worries, I'll just snip the USB plug and swap in a USB-C plug. Or so I thought.]]></description>
      <content:encoded><![CDATA[<p>For a small home project, my son suggested adding some under lights to a plant stand we recently added to our kitchen. I figured we could do this with a cheap LED strip and it could connect to the &quot;house&quot; using Zigbee and Home Assistant.</p>
<p>All was well until I realised the plug socket nearest the plants already had it's USB port in use and only the USB-C port was available. No worries, I'll <em>just</em> snip the USB plug and swap in a USB-C plug. Or so I thought.</p>
<h2>TL;DR</h2>
<p>The USB-C breakout boards with plugs attached tend to have a ~50K resistor tied to VCC. It needs to be removed, and a 5.1K resistor tied from ground to A5.</p>
<h2>USB-C breakout boards</h2>
<p>I was using these 4 pad USB-C breakouts since they're considerably easier to work with given the large solder pads for power and ground, <em>and</em> as I didn't need to use the data lines.</p>
<figure><img src="https://remysharp.com/images/usb-c-breakout.jpg" alt="USB-C breakout" decoding="async"></figure>
<p>I originally snipped off the end of the regular USB plug on the LED strip and then soldered on the new USB-C port. I did a short test injecting power via my bench supply and it appeared to work.</p>
<p>When I did a <em>real</em> test by plugging it into my wall USB-C socket, absolutely nothing happened. I was quite confused (for a brief moment worried I had killed my socket - I hadn't).</p>
<p>I also added a USB-C voltmeter between my new plug and the wall socket and it confirmed: zero volts being delivered.</p>
<p>This is because USB-C is smart, but to <em>be</em> smart, the &quot;host&quot; (in my case, the wall socket) needs to ask a question of the peripheral to be sure what kind of voltage to deliver.</p>
<p>I had assumed if the peripheral was dumb and didn't &quot;reply&quot;, the host was send out 5v (as it does with a traditional USB host), but apparently not (or certainly not for the case of my wall socket). Actually, if I've understood USB-C correctly, the host also needs to get confirmation that a peripheral has been inserted (and <em>not</em> another host).</p>
<p>This question and answer process is done through resistors, and possibly magic unicorns. Still, the <a href="https://www.usb.org/document-library/usb-type-cr-cable-and-connector-specification-release-24">specification is here</a> if you're interested.</p>
<p>The USB-C breakout comes with a 56K resistor tied from A5 (CC) to VCC, which means it's originally suited to making a USB-C to USB 2.0 cable. Not what I need at all.</p>
<p>To remedy this, the resistor is removed, and a 5.1K resistor is added tying A5 to ground. Thankfully the breakout board does have a preconfigured pads to make this relatively straight forward (both for SMD or THT soldering).</p>
<figure><img src="https://remysharp.com/images/usb-c-changed-resistor.jpg" alt="USB-C with new resistor" decoding="async"></figure>
<p>My soldering was a bit crunchy, but the resistor is now correct and when plugged in, the device now correctly pulls 5V from the wall socket.</p>
<p>The result, after sticking down the LED strip is now we've got some semi-fancy under plant lighting (which I promise you, looks better in real-life!):</p>
<figure><img src="https://remysharp.com/images/plant-led.jpg" alt="LED plant life" decoding="async"></figure>
<p><em>Originally published on <a href="https://remysharp.com/2025/02/24/upgrading-usb-powered-devices-to-usbc">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Blog Questions Challenge</title>
      <guid isPermaLink="false">blog-questions-challenge</guid>
      <link>https://remysharp.com/2025/02/06/blog-questions-challenge</link>
      <pubDate>Thu, 06 Feb 2025 10:00:00 +0000</pubDate>
      <description><![CDATA[I remember seeing something very similar doing the rounds on that site we used to use called &quot;Twitter&quot;, possibly back in 2007. Anyway, fast forward a full grown adult and it's making the rounds, except this time instead of looking dough eyed from afar, that lovely man Stuart tagged me (though arguably Jeremy also tagged everyone in his post too).
So here's my hat in the ring.]]></description>
      <content:encoded><![CDATA[
<p>I remember seeing something very similar doing the rounds on that site we used to use called &quot;Twitter&quot;, possibly back in 2007. Anyway, fast forward a full grown adult and it's making the rounds, except this time instead of looking dough eyed from afar, that lovely man <a href="https://www.kryogenix.org/days/2025/02/03/blog-questions-challenge/">Stuart</a> tagged me (though arguably Jeremy also tagged <a href="https://youtu.be/74BzSTQCl_c?si=hxm1WKV0_zIw6vT_&amp;t=9">everyone</a> in <a href="https://adactio.com/journal/21674">his post</a> too).</p>
<p>So here's my hat in the ring.</p>
<h2>Why did you start blogging in the first place?</h2>
<p>I'd had a couple of false starts with blogging. Once in 2004 during a 3 month sabbatical in Whistler, Canada to keep friends &amp; family up to date with my activities. That last 3 weeks (on Google's blogger platform - no idea if it still exists).</p>
<p>After getting back to the UK, whilst I was working full time in London (and travelling back home to Brighton), I decided that I would start my own business, and that like any good web based business, developer thingy person, I <strong>should</strong> have a blog. Literally as simple as that.</p>
<p>Since I had no one to write for, and nothing to say, I started just writing little movie reviews of films I'd seen at the cinema and little notes that I thought were interesting things happening on the web (technically this was <a href="https://remysharp.com/2006/09/01/flickr-geo-tags">my first blog post</a> on <a href="http://remysharp.com">remysharp.com</a> even though I back-posted one to before then).</p>
<p>Simultaneously at around the same time, I was blogging on <a href="http://leftlogic.com">leftlogic.com</a> which had a handful of high traffic posts (like my <a href="https://web.archive.org/web/20061018170535/http://leftlogic.com/info/articles/microformats_bookmarklet">Microformats bookmarklet</a>), that eventually I brought some of the posts across to this blog (I should really move the rest in one day).</p>
<h2>What platform are you using to manage your blog and why did you choose it? Have you blogged on other platforms before?</h2>
<p>Today my blog is a custom rolled static site generator (SSG) written in JavaScript. It's <a href="https://github.com/remy/remysharp.com">on github</a> if you want a peek. Because the site is entirely static (with some dynamic content either regenerated on build or if I run an offline command), it means I host my site on Netlify (thanks Netlify folks).</p>
<p>Before that I was running my blog on a SSG called <a href="https://github.com/sintaxi/harp">Harp</a>, and this was hosted on Heroku. It based a lot of the inspiration for my own code (along with <a href="https://www.11ty.dev/">11ty</a>, though I didn't use it for my personal blog).</p>
<p>Before that, and originally, it was WordPress. Mostly because I could get started on the blogging side rather than get too bogged down in how comments and <a href="https://indieweb.org/pingback">pingbacks</a> work. I've literally no idea where that was hosted, but I moved away from WordPress when I'd hit my limit of viagra adverts being injected into various vulnerabilities that WordPress so kindly hosted for me.</p>
<h2>How do you write your posts? For example, in a local editing tool, or in a panel/dashboard that's part of your blog?</h2>
<p>As markdown in whatever editor I happen to be using at the time. I'm writing this in VS Code, and if I'm at my desktop machine, it's likely the &quot;editor&quot; I'll use.</p>
<p>If I'm on my phone (and I've written <em>a lot</em> of posts on my phone, on a treadmill), it'll be iA Writer for Android.</p>
<p>I don't have any fancy formatting, and I've gotten the sense that for a lot of the bloggers who've written similar posts, they're in the same boat.</p>
<h2>When do you feel most inspired to write?</h2>
<p>There's two distinct times:</p>
<p>When there's an immediate trigger in front of me. This could be a technical problem that I thought would be useful to write up (so that I can search my own blog for the solution), or if there's some opinion on the web that I strongly want to leap on and have my say in (yes, though I won't admit it, I suspect I love the sound of my own words, I mean, look how long this post is already).</p>
<p>Those posts tend to be easier to write.</p>
<p>The other time is when I'm either in bed trying to sleep, or in bed trying not to get up, or driving and my brain is on idle. I find myself wanting to write about much more nuanced subjects, like for instance: how AI might affect the coders jobs and how that might impact children today. But inevitably they're subjects that I'm not confident to articulate and I pontificate instead (&quot;articulate&quot; and &quot;pontificate&quot; in a single blog post, 13 year old Rem would be proud, and slightly grossed out).</p>
<h2>Do you publish immediately after writing, or do you let it simmer a bit as a draft?</h2>
<p>Always immediately after. Otherwise they go to die a slow death in my <a href="https://remysharp.com/drafts/">drafts</a> - which I should really just go ahead and rename to <em>purgatory</em>.</p>
<p>The problem I now face having run my blog for 19 years (so far!), is that I don't tend to fart out posts as quickly. Ironically I've written about <a href="https://remysharp.com/2021/11/21/why-i-write-and-when-i-dont#why-i-wont-write">why I don't write</a> and what blocks me in particular. The thing that slows me down (a lot) is needing to make sure I'm being as technically accurate as possible. Checking for bugs or misunderstandings, which then leads to rabbit holes, which, quite a few times, leads to purgatory (sorry, &quot;permanent draft&quot;).</p>
<h2>What's your favourite post on your blog?</h2>
<p>I'm not entirely sure I have one. I'm very proud of <a href="https://remysharp.com/2015/09/14/jsbin-toxic-part-1">my series on JS Bin</a> (originally a single blog post at over well over 5,000 words, I was advised to break it up!). My series of posts when we <a href="https://remysharp.com/2019/02/12/cern-day-1">returned to CERN to build</a> the World Wide Web browser was fun too.</p>
<p>I'm also fairly keen on my post on <a href="https://remysharp.com/2014/03/07/youre-paying-to-speak">you're paying to speak</a> - because I <em>still</em> feel strongly about it and it's still relevant (and did kick off some good discussion).</p>
<h2>Any future plans for your blog? Maybe a redesign, a move to another platform, or adding a new feature?</h2>
<p>I'm regularly thinking on the backend of the blog, making slight build speed improvements, or having to fix or add some little thing. I briefly thought about adding a section on the games I'm playing and completing, but it turns out I'm pretty crap at games, so the list quickly ended at three games and hit the bin.</p>
<p>In short: no.</p>
<h2>Next?</h2>
<p>Although I love you, my dear reader, very much, I'm going to tag a few people who's blogs pop into my head. The first is <a href="https://ohhelloana.blog/posts/">Ana Rodrigues</a>. Next would be the lovely <a href="https://jakearchibald.com/">Jake Archibald</a> who, if he does blog on this one will be off brand from the mega technical posts! Finally, <a href="https://awfulwoman.com/">Charlie O'Hara</a> (aka Whale Coiner and all kinds of other names) - I'm sure Charlie's had multiple blogs, but not sure enough to bet on it, so it'll be nice to (hopefully) read her history on this too.</p>
<p>There's lots more people that I'd love to read their history, but I think it would be rude to snag them all for my own. Definitely looking forward to reading more of these posts from people.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/02/06/blog-questions-challenge">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Home Assistant: how get real-time UK electricity and gas</title>
      <guid isPermaLink="false">home-assistant-how-get-realtime-electricity-and-gas.md</guid>
      <link>https://remysharp.com/2025/02/03/home-assistant-how-get-realtime-electricity-and-gas.md</link>
      <pubDate>Mon, 03 Feb 2025 10:00:00 +0000</pubDate>
      <description><![CDATA[There's a number of ways to get real-time electricity readings into Home Assistant. I'm currently using a Puck.js to read the blinking light on my meter but our gas meter is one of the new ultrasonic ones (in particular there's no readings or lights without physically interacting with it), so there was no obvious means to getting this data.
Most UK based energy companies don't have public methods to get your data out. I know Octopus does have APIs (which isn't our supplier), and there's a specific smart meter that also makes the data available on MQTT - but it's been out of stock for (at least) the last 12 months.
However, this last weekend I discovered, so long as you have a smart meter, you can actually collect this data from the DCC.]]></description>
      <content:encoded><![CDATA[
<p>There's a number of ways to get real-time electricity readings into Home Assistant. I'm currently using a Puck.js to read the <a href="https://www.espruino.com/Smart+Meter">blinking light on my meter</a> but our gas meter is one of the new ultrasonic ones (in particular there's no readings or lights without physically interacting with it), so there was no obvious means to getting this data.</p>
<p>Most UK based energy companies don't have public methods to get your data out. I know Octopus does have APIs (which isn't <em>our</em> supplier), and there's a specific smart meter that also makes the data available on MQTT - but it's been out of stock for (at least) the last 12 months.</p>
<p>However, this last weekend I discovered, so long as you have a smart meter, you can actually collect this data from the DCC.</p>
<h2>TL;DR</h2>
<ol>
<li>Register with <a href="https://bright.glowmarkt.com">Bright</a></li>
<li>Install the <a href="https://github.com/HandyHat/ha-hildebrandglow-dcc">integration</a></li>
</ol>
<p>Job done.</p>
<h2>DCC and getting your data</h2>
<p>The Data Communications Company (DCC) enables standardised data exchange across the national smart meter network, allowing energy consumption data can be accessed securely.</p>
<p>Not particularly open to Joe Public, but if you have authorised access, such as the <em>&quot;Other User&quot;</em> role, you (they, the 3rd party) can get the data for us (not all of it, as the Other User from what I've read understandably has restricted access). The technical phrasing is:</p>
<blockquote>
<p>As a DCC Other User, we are also able to support SMETS data services without hardware.</p>
</blockquote>
<p>Creators of the Glow smart meter, Hildebrand, also have a mobile app called <a href="https://bright.glowmarkt.com">Bright</a> (native <em>and</em> web) that doesn't require any special hardware, that can collect your energy readings on a 30 minute interval (okay, not &quot;real-time&quot;, but real enough!). This is because Hildebrand have the Other User access.</p>
<p>Since the Bright is on the web, and there's an API that drives it (as seen in the network requests), there's now a way to get energy data into Home Assistant.</p>
<h2>The integration</h2>
<p>From here it's a matter of wiring up the software. The integration is available via HACS with the <a href="https://github.com/HandyHat/ha-hildebrandglow-dcc">install guide on github</a>, and a few clicks later I now have the daily running cost of both electricity <em>and</em> gas and the usage, which can help me make decisions about how we use the energy at home.</p>
<figure><img src="https://remysharp.com/images/hass-energy.png" alt="Visualisation of energy flow on Home Assistant" decoding="async"></figure>
<p>Plus, it looks cool 🤓</p>
<p>I know this is data for data's sake, but it's actually helped us to make a change at home, which should have a positive financial impact (I/we hope!). I have a glass infrared radiator in the office, which although is energy efficient, actually accounts for a fairly large whack of our daily electricity usage.</p>
<p>Switching back to the gas heating (whilst <em>not</em> heating the entire floor) is what we're testing now and should make an impact on our bills.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/02/03/home-assistant-how-get-realtime-electricity-and-gas.md">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>What is good web design, and bad web design?</title>
      <guid isPermaLink="false">what-is-good-web-design-and-bad-web-design</guid>
      <link>https://remysharp.com/2025/01/15/what-is-good-web-design-and-bad-web-design</link>
      <pubDate>Wed, 15 Jan 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[My son, aged 13, was given this question for his homework recently. As someone (me!) having worked on the web professionally since 1999, I felt a connection to this particular bit of homework. Obviously we clashed (homework sucks, both for kids, parents and teachers) - but I thought it would be a good exercise for me to try to answer.
I will add that I don't consider myself the authority on this answer, but I have been attending design, user experience and developer conferences for over 15 years - so hopefully I've learnt a thing or two.]]></description>
      <content:encoded><![CDATA[
<p>My son, aged 13, was given this question for his homework recently. As someone (me!) having worked on the web professionally since 1999, I felt a connection to this particular bit of homework. Obviously we clashed (homework sucks, both for kids, parents <em>and</em> teachers) - but I thought it would be a good exercise for me to try to answer.</p>
<p>I will add that I don't consider myself the authority on this answer, <a href="https://remysharp.com/2017/08/12/but">but</a> I have been attending design, user experience and developer conferences for over 15 years - so hopefully I've learnt a thing or two.</p>
<h2>There's no one measure</h2>
<p>When I consider what's good or bad about a web site, there's quite a few different tests I'm considering.</p>
<p>It's also worth adding that this list is non exhaustive and I think you'll agree quickly that there's <em>a lot</em> of factors that go into weighing whether a web site is good or bad in their design.</p>
<p><strong>Can I find what I'm looking to do?</strong> This means landmarks are clear, I'm not being pulled away by distracting content (like overlays) and I can find the navigation easily.</p>
<p>For example: if I'm on Brighton council refuse web pages, can I easily and quickly find when my next collection dates are? Or if I'm on a hotel web site, can I find the address (or map), contact details and booking dates?</p>
<p><strong>Do the pages work on mobile?</strong> Much to my frustration, my mobile phone is pretty much always on me, so when I go to look up some information, I'll usually use my phone first.</p>
<p>A bad design is going to have the page jumping all over the place when it's loading (i.e. I'm about to tap a link, then something loads and the link moves away from under my finger). A bad design is one that I have to scroll vertically because the page hasn't zoomed to the phone.</p>
<p>I think it's pretty straightforward to get the feel for a bad mobile design, because it's often immediately unusable, forget about the actual looks.</p>
<p><strong>Does the site load?</strong> Although whether a web site loads or not isn't immediately down to it's design, usually the cause of a blank page, or half broken page will be down to the design decisions and interactions loaded into the page.</p>
<p>For example: if I'm travelling by train, often the connection is spotty at best - i.e. loading a page with lots of assets (images and JavaScript) or loading a page with <em>large</em> assets can and <strong>will</strong> fail. If the result is a log in form that doesn't work, or an article that refers to images that haven't loaded - then that's a bad design.</p>
<p>(Yes, this is me calling out those using JavaScript to load images <em>and</em> gently suggesting that progressive enhancement is (still) the way.)</p>
<p><strong>Is the main content the <em>main</em> content item?</strong> Put another way, is the content hidden (or fighting) clutter on the page?</p>
<p>For example: a lot of indie bloggers put their blog content front and centre. Strong simple fonts, with sensible line heights leaving me with <em>very</em> legible content. On the flip side are pages that are sandwiched between navigation and other articles (that I'm likely not interested in) and the actual content is a thin column of content where the text just doesn't have room to breathe.</p>
<p>Or is the content blocked behind a wall of popups and overlays? Such as (when signed out of twitter) you visit (for example) <a href="https://twitter.com/lingscars">twitter/lingscars</a>. This is bad design (and a bad experience).</p>
<p><strong>Does this web site work for all people?</strong> Specifically I'm thinking about accessibility. I'm fortunate that I don't have any impairments that affect how I surf the web, but being the liberal equality chasing flower that I am, a &quot;good web design&quot; means it works just as well for me as it does for anyone else.</p>
<p>For example: are input fields properly labelled? Do images have appropriate alternative text? Is the focus of a screen reader considered (and not hijacked)? Is there confusing sentences (like &quot;if you don't want to receive our newsletter, then don't click on the checkbox&quot;).</p>
<p><strong>How are images used?</strong> Text for images is a super no-no. It might be something that suits the aesthetic of the web page, but not being able to select text (such as a phone number or a business name) is a huge failure on the web site's part.</p>
<p>Bad design, potentially invisible on initial viewing, would use images for text.</p>
<p><strong>Aesthetics</strong> Generally speaking, for a western user, we're going to have an idea of whether something is pleasing or … displeasing to the eye. Taste can be quite personal, and full on, excited, animated and in your face might suit one person (ala the days of My Space and can be seen on <a href="https://www.lingscars.com/">LINGsCARS</a>), and that person might not like the brutalist styles (like those of <a href="https://heydonworks.com/">Heydon Pickering</a>'s web site), or perhaps simple and straight forward (like the <a href="https://www.gov.uk/">GOV.UK sites</a>).</p>
<p>I'd argue that a good web site design puts this last in the priorities, and a bad web site design puts aesthetics above all else.</p>
<h2>Some things that I've not mentioned, but think of from a developer's perspective</h2>
<ul>
<li>I always look at a web site and catch inaccessible aspects, be it form fields, colour contrast, keyboard support. If any of these are lacking, it's a notch towards &quot;bad web site&quot;</li>
<li>Use of JavaScript. I'm often browsing the web with JavaScript either completely disabled, or with all 3rd party JavaScript disabled. In the later case, if the site doesn't function (Amazon product listings being one), it frustrates me that I would even need to load resources from many different origins. Even more so when a web page uses JavaScript to create elements such as images, or take over the scroll bar - these are failings in my opinion</li>
<li>Use of semantic mark up. I didn't mention it earlier, because every day users aren't going to think about how that impacts them. But good mark up goes towards good accessibility (such for a screen reader to navigate) or for search engines to pull out the pertinent content (or even to allow for a &quot;reader&quot; mode, something I use on Firefox both on desktop and mobile)</li>
</ul>
<p>There's more I'm sure. Maybe this is useful (or incomplete!?) to you, but I'm fairly certain it's useful for my son!</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/01/15/what-is-good-web-design-and-bad-web-design">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Server isn't sending content length</title>
      <guid isPermaLink="false">server-isnt-sending-content-length</guid>
      <link>https://remysharp.com/2025/01/03/server-isnt-sending-content-length</link>
      <pubDate>Fri, 03 Jan 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[As part of the ZX Spectrum community I wrote a tool that can download content over HTTP from a ZX Spectrum Next, but one prerequisite is that the content-length header is present (so it operate properly).
A friend from the community runs a server and built a (really nice) GUI tool on top the .http command where users can find and download software for their Spectrum.
However, for reasons unknown, it wasn't working and content-length just seemed to be missing.
This fix isn't Speccy specific, it will fix missing content length for Apache 2 servers.]]></description>
      <content:encoded><![CDATA[
<p>As part of the ZX Spectrum community I wrote <a href="https://github.com/remy/next-http">a tool that can download</a> content over HTTP from a ZX Spectrum Next, but one prerequisite is that the <code>content-length</code> header is present (so it operate properly).</p>
<p>A friend from the community runs a server and built a (really nice) GUI tool on top the <code>.http</code> command where users can find and download software for their Spectrum.</p>
<p>However, for reasons unknown, it wasn't working and <code>content-length</code> just seemed to be missing.</p>
<p><em>This fix isn't Speccy specific, it will fix missing content length for Apache 2 servers.</em></p>
<h2>Testing</h2>
<p>Even though the <code>content-length</code> was being set in the PHP files, by default, Apache was configured to chunk encoding (which is generally prefer, by the server, when sending dynamic based content, i.e. content generated by a PHP file).</p>
<p>So somewhere the <code>content-length</code> was being stripped.</p>
<p>This is all tested using a <code>curl</code> command with the <code>-i</code> flag to include headers:</p>
<pre><code>$ curl -i example.com
</code></pre>
<p>Though this does include the full body, so it's easier to debug using <code>-I</code> (which requests the headers via a <code>HEAD</code> request) but the <code>HEAD</code> request is not equal to <code>GET</code> so we'll explicitly call a <code>GET</code> using <code>-X</code>:</p>
<pre><code>$ curl -I -X GET example.com
</code></pre>
<p>Now we just have the headers and we can check if anything is missing.</p>
<hr>
<div class="update">
<strong>Note that the information about browsers is completely wrong. I <em>thought</em> the header was being synthesised by the browser, but if you look carefully the header is in the raw response. I'm sure I saw it was being added, but I now suspect I was just confused whilst debugging. I've left this here in case it's at all useful, but keep in mind it's wrong!</strong></div>
<p><strong>&lt;wrong-content&gt;</strong></p>
<p>A note about debugging with browser tools: browsers are going to augment the &quot;raw&quot; request. Firstly you have caching in play (so fire up the privacy window and disable caching from the network panel), but also they show you how <em>they</em> interpreted the headers.</p>
<p>This is Firefox for instance on <code>https://www.google.com</code>:</p>
<figure><img src="https://remysharp.com/images/firefox-headers.png" alt="Firefox GET request headers panel" decoding="async"></figure>
<p>Whereas if we tick that &quot;raw&quot; checkbox, we can see the <code>content-length</code> was never part of the headers:</p>
<figure><img src="https://remysharp.com/images/firefox-headers-raw.png" alt="Firefox raw headers" decoding="async"></figure>
<p>Chrome will also add the header (with no option for the original response) but show it as a content length of zero.</p>
<p><strong>&lt;/wrong-content&gt;</strong></p>
<hr>
<p>So, make sure you're testing as close to the metal as possible.</p>
<h2>Fixing</h2>
<p>At some point (in Apache's history), a change was made to prevent scripts from setting certain headers (in particular <code>content-length</code>) to mitigate potential <a href="https://httpd.apache.org/security/vulnerabilities_24.html">vulnerabilities</a>.</p>
<p>So to trust your scripts, an apache rule is required to apply this. The follow example solved our particular problem, including the change in an <code>.htaccess</code> file in the root of the directory being served:</p>
<pre><code>&lt;Files *.php&gt;
  SetEnv ap_trust_cgilike_cl 1
&lt;/Files&gt;
</code></pre>
<p>Now Apache will trust all the headers, <code>content-length</code> specifically, from the PHP files.</p>
<p><em>Originally published on <a href="https://remysharp.com/2025/01/03/server-isnt-sending-content-length">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>My 2024</title>
      <guid isPermaLink="false">my-2024</guid>
      <link>https://remysharp.com/2024/12/31/my-2024</link>
      <pubDate>Tue, 31 Dec 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[This year zipped past for me, mostly because I had my eye on an event in November for most of the year, but here we are: the close of '24 and on the doorstep of '25.
In this post I'll try to remind my future self of what I did, but unlike previous years, there's a lot less public work (side projects), a lot less blogging content…and generally, I think, a lot less.
So, with that bulge of joy, let's get on with it.]]></description>
      <content:encoded><![CDATA[
<p>This year zipped past for me, mostly because I had my eye on an event in November for most of the year, but here we are: the close of '24 and on the doorstep of '25.</p>
<p>In this post I'll try to remind my future self of what I did, but unlike <a href="https://remysharp.com/my-years">previous years</a>, there's a lot less public work (side projects), a lot less blogging content…and generally, I think, a lot less.</p>
<p>So, with that bulge of joy, let's get on with it.</p>
<h2>Some context of the now for future me</h2>
<p>This year, Labour came in to power after 14 years of Tory's shitfest. Generally it was a positive win because there were more seats for Lib Dems and four seats (up from one) for the Greens. The flip side is that the <s>Nazi</s> Reform party also placed seats in parliament and is growing in popularity towards the end of 2024.</p>
<p>Riding that wave of despair, Trump, the <a href="https://en.m.wikipedia.org/wiki/Prosecution_of_Donald_Trump_in_New_York">convicted</a> <a href="https://www.theguardian.com/us-news/article/2024/may/30/trump-guilty-hush-money-trial-biden-response">criminal</a> has also been re-elected to run The States for (<em>hopefully</em> no longer than) four years.</p>
<p>Then there's <a href="https://www.amnesty.org/en/latest/news/2024/12/amnesty-international-concludes-israel-is-committing-genocide-against-palestinians-in-gaza/">genocide</a> that Israel is committing against Palestinians in Gaza (which countries including the UK <em>and</em> the USA are complicit in), and the continued Russia fighting/war with Ukraine.</p>
<p>The world is upsidedown and a total trash fire. It's no wonder that mental health is a growing problem ... and I sort of feel a total disconnect with what I do and the real value on the world.</p>
<p>Jeez, what a fun start to this, eh?</p>
<h2>Work</h2>
<p>Most of this year I've been working with <a href="https://www.thenational.academy/about-us/who-we-are">Oak National</a> almost exclusively developing their public API. I love working for Oak partly for the social good, partly for the people there, but also because I know that the work has done, and continues to directly impact my own kids.</p>
<p>I've had to also finally, reluctantly (and begrudgingly) climb onto the TypeScript train (which I'm <a href="https://remysharp.com/2024/02/23/why-my-code-isnt-in-typescript">not converted at all</a>). I'm a stickler for ensuring that my code passes all linting rules, so I now have the extra pressure to ensure all the types are present and correct (we/Oak apply the rule that says <code>any</code> is banned).</p>
<p>Mostly TypeScript has been okay, but the times I struggle is the times that the actual error reported is a total trash fire of stack traces and &quot;possibilities&quot; of errors (see my <a href="https://remysharp.com/2024/02/23/why-my-code-isnt-in-typescript">earlier post</a>).</p>
<p>As a side effect, I've also discovered vitest (because of how terrible the Jest support was) and that was a welcome <a href="https://remysharp.com/2024/06/07/adding-tests-to-a-typescript-next-trpc-project-without-the-faff">simplicity to my tests</a>, even if I'm left writing stupid code like this:</p>
<pre><code class="language-ts"><span class="token keyword">const</span> subject <span class="token operator">=</span> res<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>subjects<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span>
  <span class="token punctuation">(</span>_<span class="token punctuation">)</span> <span class="token operator">=></span> _<span class="token punctuation">.</span>subjectSlug <span class="token operator">===</span> <span class="token string">'reading-writing-and-oracy'</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token function">expect</span><span class="token punctuation">(</span>subject<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBeTruthy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">expect</span><span class="token punctuation">(</span>subject<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toHaveProperty</span><span class="token punctuation">(</span><span class="token string">'units'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// this is more nonsense from typescript otherwise I get red snakes</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>subject <span class="token operator">||</span> <span class="token operator">!</span><span class="token punctuation">(</span><span class="token string">'units'</span> <span class="token keyword">in</span> subject<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'No subjects found'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">const</span> units <span class="token operator">=</span> subject<span class="token punctuation">.</span>units<span class="token punctuation">;</span>
<span class="token comment">// tests continue</span>
</code></pre>
<p>Still, Oak is falls under &quot;social good&quot; for me. Additionally, towards the end of the year I had to submit my very first tender (since my company's <a href="https://remysharp.com/2006/09/06/so-im-a-director">inception in 2006</a>), for Oak, which thankfully I won.</p>
<p>This also meant working with a new team which has (to my surprise) left me feeling reinvigorated about work - a rare feat!</p>
<h2>Projects</h2>
<p>I always expected as my kids got older (now 10 and 13) I would have more time for side projects, but the reality of that has been inverse. Possibly because I hadn't factored for my own self getting old, possibly because I'm tired of the computer. Who knows. Still, I do enjoy tinkering and learning, but the <em>new</em> full scale side projects have definitely dwindled (or I don't feel like have much to say about them).</p>
<p>However, as always: FFConf</p>
<h3>FFConf</h3>
<p>The event continues to be a success to all those who attend. The speakers, as usual, were absolutely superb with messages that resonate for long after the event has finished.</p>
<p>I do have non-technical people ask me what the event is about, and I'm fully aware that over the years (13 or 14) it's become increasingly vague. It's origins was in JavaScript, but quickly grew to &quot;the web&quot;. I tend to think of FFConf being a conference about &quot;how we can make the web a better place&quot;, and yes, I know that in itself is pretty vague too!</p>
<p>You can read my <a href="https://remysharp.com/2024/11/30/ffconf-2024">own write up</a> (if you like). The lead up to the event was one of the hardest I've had since we created FFConf back in 2009 though. It really boils down to ticket sales and how the events market has definitely changed post-pandemic. For me, it felt like ticket sales were a constant concern from the date we launched on April 22nd all the way until November 8th - which is a <em>long</em> time (for me).</p>
<p>We'll be running again in 2025, but we'll be more cognisant of how to sell and market tickets this time.</p>
<p>Still, a wonderful day of hoomon interaction.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024.jpg" alt="A view of the venue and attendees of FFConf" decoding="async"></figure>
<h3>Hardware and hacking</h3>
<p>I continue to enjoy breaking away from software and playing in the land of hardware. I do worry that I might be someone who quickly moves from &quot;hobby&quot; to hobby (I <em>still</em> have a pile of 10 or so Game Boys waiting to be fixed from over 18 months ago!).</p>
<p>This year I tinkered with customising <a href="https://remysharp.com/2024/05/25/casio-f91w-modding">Casio F-91w watches</a> and have also created a number of my own custom PCBs, one of which takes a Game Boy CPU and outputs on HDMI (using a Raspberry Pico) and takes input from a USB joystick, the other is a Pokemon Mini custom flash cart (which is very, <em>very</em> in progress).</p>
<figure><img src="https://remysharp.com/images/pcbs-2024.jpg" alt="A selection of custom created PCBs" decoding="async"></figure>
<h3>Home Assistant</h3>
<p>During the school summer holidays, I had told the littlest that we could, in theory, create an automation for her curtains. We wanted to be able to say &quot;Hey Google, open my curtains&quot;. I knew how all the parts would work, it was just the connection from the hardware to the Google intent that I didn't know.</p>
<p>Which is when I learnt Home Assistant, and boy was this a deep rabbit hole! The missing connection part I needed in my project turned out to be <a href="https://esphome.io/">esphome</a>. With the help of Home Assistant (which I had tried years ago but had quickly given up on, but in those years HA has massively improved), I was able to set up the whole curtain automation.</p>
<p>The hardware side was relatively straightforward and though ultimate it didn't work (because I couldn't get a good enough grip on the curtain pull string which has some elasticity in it), the project completely unlocked Home Assistant to me, and how I'm enjoying a whole range of fun little automations.</p>
<p>My favourite (and I'm promising myself I <em>will</em> blog about it), is that when I got to the gym, our shower towel rail starts to heat up (which already has my clean underwear hanging on it) and when I return home, after having a shower (which automatically turns <em>off</em> the towel rail) I have the pleasure of putting on warm undies, which let's face it: is the best feeling.</p>
<p>In this process, I've also customised our &quot;dumb&quot; <a href="https://bsky.app/profile/remysharp.com/post/3lawe74hmdk2i">cat flap</a> so that I can get a notification when it's low on battery, I <a href="https://remysharp.com/2024/08/06/making-a-dumb-doorbell-smart">upgraded our doorbell</a> and have forayed into the <a href="https://bsky.app/profile/remysharp.com/post/3latqwu7lig23">230 volt DIY'ing</a>!</p>
<h3>Donations</h3>
<p>During the pandemic years because costs were so low I found we had a good deal of profit we could donate to charities, and we've decided to bake this into our business. The rule (that I/we made for ourselves) is that 10% (or more) of our profits are donated.</p>
<p>This year, most of the donations (money) has gone towards families affected by genocide or wars.</p>
<h2>Personal</h2>
<p>One of the biggies for me was the addition of <a href="https://remysharp.com/2024/07/05/say-what-on-tinnitus-and-hearing">hearing aids</a> to my life. Initially I was extremely ashamed of just the idea of having to wear hearing aids, but from the day I had them at the start of the year, I've worn them every day and they've absolutely improved my quality of life/hearing.</p>
<p>It's like they open up the sounds around me. I think a visual equivalent is perhaps if you can see mostly fine, but if some things you need to move away or move closer to read properly (i.e. you'd need glasses), the hearing aids have done for my hearing. I can understand most conversations and I'm not having to pattern match what I <em>think</em> someone has said to me (nearly as much).</p>
<p>Secondly we (as a family) have been undergoing a pretty significant home renovation. When we moved in to our home in 2019, we knew right away that the whole place needed improvements, but it was liveable from the get go.</p>
<p>The original plan was always to move the kitchen to a completely different room and make it the centre of the social life of our home and family. But then came along The Pandemic which immediately stopped any plans in their tracks.</p>
<p>Now in 2024, at the start of May we finally started the work (after getting an architect to go through realistic options). The plan was to extend the house with a side return/extension and thus creating a kitchen in what was our lounge (and would become a larger space).</p>
<p>The wall demolition, creation, beam installation and foundation digging was a pretty serious chunk of work (which, no, I didn't do myself, I <em>wish</em> I were that practical!). Thankfully we were in the position where we could seal the door off to the old lounge, we still had a full kitchen and we converted our dining room into a lounge dining room (folding away tables and chairs to make it our TV room). That kept the vast majority of dust out.</p>
<p>It's taken 7-8 months (there's still some bits to be finished like the decking that was part of the project), but we're living in our new kitchen and it's as if it was always here in our home.</p>
<p>We definitely feel like proper grown ups now!</p>
<h3>Next bits</h3>
<p>In 2025 I have a few small goals. Nothing spectacular, but I want to keep reading, maybe even increase my reading (or rather stop going to bed so late so that I can actually read).</p>
<p>I definitely want to find the motivation to blog some more, but I feel like <em>what</em> I want to write about has changed significantly compared to a decade ago - but, it's my blog so I can really just dump any old thought farts here (see <em>this</em> post for examples!).</p>
<p>I also started (gently) journalling, partly inspired after seeing both Amy Hupe and Imran Afzal's talked each twice. I can't keep my fears, anxiety and anger either bottled up, or constantly being dumped on Julie (my wife) - so hopefully some journalling will help put that somewhere.</p>
<p>I also need to keep an eye on my health. The pandemic did a number on my gym going, but lock down finished quite a while ago and it doesn't hold up that great as an excuse. I've been attending the gym, but it's the diet part that I've been rather slack about. I'm also hoping that this year I'll manage to get through the year without more of me breaking (or getting a diagnosis that tells me something else is crap), though I've got blood tests coming this coming week and I <em>know</em> there's some news on the end of that…</p>
<p>All that aside, happy new year, and I hope you find some joy in 2025.</p>
<p>We out 👋</p>
<figure><img src="https://remysharp.com/images/2024-sharps.jpg" alt="Sharp family in 2024" decoding="async"></figure>
<p><em>Originally published on <a href="https://remysharp.com/2024/12/31/my-2024">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>FFConf 2024</title>
      <guid isPermaLink="false">ffconf-2024</guid>
      <link>https://remysharp.com/2024/11/30/ffconf-2024</link>
      <pubDate>Sat, 30 Nov 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[Our 16th year, 17th outing (it's complicated with doubles for 3 years and skipping the pandemic years…) completed two weeks ago, and it is high time I post my thoughts.
There are two parts this post breaks into: the day itself and the amazing talks, and the lead up and its future.]]></description>
      <content:encoded><![CDATA[
<p>Our 16th year, 17th outing (it's complicated with doubles for 3 years and skipping the pandemic years…) completed two weeks ago, and it is high time I post my thoughts.</p>
<p>There are two parts this post breaks into: the day itself and the amazing talks, and the lead up and its future.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/ppl.jpg" alt="People queuing to attend FFConf" decoding="async"></figure>
<h2>The day</h2>
<p>Due to my role up front and emceeing between the talks, quite often the actual day races by leaving me exhausted whilst I feel like all I did was sit and watch talks.</p>
<p>I have to admit, with the (extremely) recent news of US elections plus the genocide being delivered to the Palestinians in Gaza (and our role in funding it), the thought of sitting and thinking about how to make the web better seemed awfully trivial.</p>
<p>I can't remember who said it to me, but actually perhaps this is what we needed, a day of brief reprieve. A day to remember the good, to see how we might make a positive impact on the world. A day of being kind to ourselves.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/olu.jpg" alt="" decoding="async"></figure>
<p><a href="https://olu.online/">Olu</a> started the day in a swiftly delivered talk on creating a better webs (plural). If I'm honest, I need to watch it back because it was rammed with content and the day was very full of inspiration. However I <em>do</em> remember that Olu gave me the overall impression that there <em>was</em> hope for the web, and for us, specifically it's amazing that we can, and have leart from shared efforts of people.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/leonie.jpg" alt="" decoding="async"></figure>
<p><a href="https://tink.uk/">Léonie</a> was next with accessibility, AI and the bollocks. Léonie prefix her talk with an incredibly important reminder, and context when it comes to AI:</p>
<ol>
<li>The content that powers these LLM has been stolen, and taken without permission from us. There was no payment or retribution.</li>
<li>The energy required both to train and run these LLMs is unpresidented, and not in a good way.</li>
</ol>
<p>Léonie then went on to share the state of the art with respect to AI in accessibility. Using an Apple advert for an iPhone, with AI offering a rich description of what the video showed is entirely new to someone with visual impairment.</p>
<p>Léonie also compared the response to &quot;what is this a picture of?&quot; when asked of a human, will likely be to the point and it can be awkward for the person asking to increasingly ask for more information about the picture. However, AI is often overly verbose but this can offer a much richer understanding of what the image represents.</p>
<p>The video will be released soon enough, and (with all the talks) I'd highly recommend watching it.</p>
<p>Léonie importantly (and you'd probably guess from her title) moved on to so-called Accessibility Overlays and AI. The short version is that they don't work. The slightly longer version is that they don't work and a site using these could well be sued.</p>
<p>I had this talk in mind for FFConf back in 2023, and really I could only think of Léonie for it, and as I had hoped and expected, she did an amazing job.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/andy.jpg" alt="" decoding="async"></figure>
<p><a href="https://piccalil.li/">Andy Bell</a> then spoke around the topic of communication. It's far from rocket science but Andy does a brilliant job (whilst being entertaining) of comparing and contrasting terrible and lazy exchange with subtly tweaked and much more effective exchange.</p>
<p>I'm sure we're all guilty of the examples that Andy pulled up as shit, and there's some really useful take aways for thinking about how to get what you (or I) really want out of the exchange.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/kimberley.jpg" alt="" decoding="async"></figure>
<p>To close the first half of the day, <a href="https://builtby.kim/">Kimberley Cook</a>, director and trustee of Codebar, talked about the experience of building a community and the mistakes and learnings she experienced along to way to Codebar becoming 27,000 strong community.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/amy.jpg" alt="" decoding="async"></figure>
<p><a href="https://amyhupe.co.uk/">Amy</a> started our second half. I'd seen Amy's talk at State of the Browser in 2023 entitled &quot;It all means nothing, in the end&quot; and I remember thinking at the time that I wanted to share this same message with my audience at FFConf. More importantly and personally the talk itself resonated so much with me.</p>
<p>So when Amy reprised her talk for us at FFConf not only did it absolutely hit all the notes I had wanted, but she also updated it to include some of the bizarre fallout she was subjected to by random US religious nuts. Someone in the evening commented how there were moments that reminded them of Adam Buxton reading when <a href="https://www.youtube.com/watch?v=gx-WBaSNrTQ">reading out YouTube comments</a>!</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/mike.jpg" alt="" decoding="async"></figure>
<p><a href="https://www.skeptic.org.uk/author/mike-hall/">Mike Hall</a> was asked to help me understand how to think critically. Mike, with with his two co-hosts runs <a href="https://www.merseysideskeptics.org.uk/podcasts/skeptics-with-a-k">Skeptics with a K podcast</a> which meant he was perfectly placed for this talk.</p>
<p>Mike started with a non-exhaustive list of biases and showed practical examples of how they work and how we can reframe our thinking whilst then applying it to a practical code example. I would have loved to dive into his brain to explore <em>all</em> the biases.</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/michael.jpg" alt="" decoding="async"></figure>
<p>Then into the last leg <a href="https://www.uxmichael.co/">Michael Kibedi</a> introduced the idea of death and technology. It's a really interesting topic that's not really considered very much in technology.</p>
<p>The content is a little hard to put into words (particularly after two weeks), but even today, I read a product I was looking at offered &quot;lifetime cloud storage&quot; when the reality isn't that at all. More likely is that the product dies before I do, then what happens to that &quot;cloud&quot; storage?</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/imran.jpg" alt="" decoding="async"></figure>
<p>Finally, to close out our day, another talk that I had seen earlier on this year at the excellent <a href="https://heypresents.com/conferences">All Day Hey</a>: <a href="https://www.imranafzal.com">Imran Afzal</a> on <em>the art of reflection</em>.</p>
<p>To me, one of the most valuable take aways from Imran's talk was taking the time to be still to reflect (though, it's possibly obvious from the title). I never really stop, not in work and not in my personal projects. Quite often it's the exact opposite, I keep piling more and more on perhaps to validate myself in some way.</p>
<p>Although I curate the event around the idea of us (the attendees, myself included) as learning how to improve the world (and web) around us for others, Imran closed our day on a much more personal and important individual to ask to spend time with: ourselves. It's not about, for me, finding purpose, but finding space to take a breath.</p>
<hr>
<p>I'm always absolutely blown away by the speakers and their talks each year. These eight individuals put a lot of time and effort into making the day what it came to be, and I'm always humbled and grateful.</p>
<p>This wraps up <em>the day</em> part of my post. There is a second part to this post, but it's taken me a week to write this so far. It'll talk about the lead up and how the event space this year has been particularly hard on organisers. But, like I said, that's for another day.</p>
<p>If you attended this year, thank you so much for your support!</p>
<figure><img src="https://remysharp.com/images/ffconf-2024/fam.jpg" alt="Remy and his family closing out FFConf" decoding="async"></figure>
<p><em>Originally published on <a href="https://remysharp.com/2024/11/30/ffconf-2024">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>The moment before: 30th Aug</title>
      <guid isPermaLink="false">the-moment-before-30th-aug</guid>
      <link>https://remysharp.com/2024/08/30/the-moment-before-30th-aug</link>
      <pubDate>Fri, 30 Aug 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[I don't like waking up on this particular day. I almost want to just skip the day and not face it.
I don't like it because I know there's a version of me that's waking up to this particular morning in 2010 still innocent and full of hope and excitement that our first child is on their way.
That version of me doesn't know that this would be the last day. That in the hours that lead into the night, that Tia, our first, would die during those long hours of labour.
This day, the 30th, is the moment before.
Her tiny heart would stop and that version of me, of us, Julie and I, our entire lives will be derailed forever.
It's not a version of me, but actually me. When our planet was pretty much in the same place as it is today, just 14 small years ago.
Instead of wrapping presents for a teenage girl, Julie and I steal a single day to be on our own and think about that single day, on the 31st that we got to spend with Tia.
It's not fair. It's never fair when a child goes before its parents. It's not the order of things and no amount of good karma can re-balance it or make good things happen or change the past.
So instead Julie and I schedule our grief for this day, the 30th, and tomorrow will spend the day with our living and loving children, and together we'll visit Tia's resting place, set flowers, read cards and wrap ourselves in love. And even though we have a grave for Tia, we've always told our kids, and ourselves, that she isn't there, at that grave, but with us, she's with us when we're home. She's around us.
I just want this day to be over, but I have to face it, and it'll forever be the last day her heart beat.]]></description>
      <content:encoded><![CDATA[
<p>I don't like waking up on this particular day. I almost want to just skip the day and not face it.</p>
<p>I don't like it because I know there's a version of me that's waking up to this particular morning in 2010 still innocent and full of hope and excitement that our first child is on their way.</p>
<p>That version of me doesn't know that this would be the last day. That in the hours that lead into the night, that Tia, our first, would die during those long hours of labour.</p>
<p>This day, the 30th, is the moment <em>before</em>.</p>
<p>Her tiny heart would stop and that version of me, of us, Julie and I, our entire lives will be derailed forever.</p>
<p>It's not a version of me, but <em>actually</em> me. When our planet was pretty much in the same place as it is today, just 14 small years ago.</p>
<p>Instead of wrapping presents for a teenage girl, Julie and I steal a single day to be on our own and think about that single day, on the 31st that we got to spend with Tia.</p>
<p>It's not fair. It's never fair when a child goes before its parents. It's not the order of things and no amount of good karma can re-balance it or make good things happen or change the past.</p>
<p>So instead Julie and I schedule our grief for this day, the 30th, and tomorrow will spend the day with our living and loving children, and together we'll visit Tia's resting place, set flowers, read cards and wrap ourselves in love. And even though we have a grave for Tia, we've always told our kids, and ourselves, that she isn't there, at that grave, but with us, she's with us when we're home. She's around us.</p>
<p>I just want this day to be over, but I have to face it, and it'll forever be the last day her heart beat.</p>
<figure><img src="https://remysharp.com/images/tia-rose.jpg" alt="A single rose blooms with a small plaque with Tia's name" decoding="async"></figure>
<p><em>Originally published on <a href="https://remysharp.com/2024/08/30/the-moment-before-30th-aug">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Making a dumb doorbell smart</title>
      <guid isPermaLink="false">making-a-dumb-doorbell-smart</guid>
      <link>https://remysharp.com/2024/08/06/making-a-dumb-doorbell-smart</link>
      <pubDate>Tue, 06 Aug 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[And no, I didn't just pop a bow tie on my doorbell! I recently installed Home Assistant on a spare Raspberry Pi I had laying around, and though our house isn't very smart-enabled, I thought it wouldn't be too hard to improve our cheap doorbell to notify through our Google home speakers (or even phone).]]></description>
      <content:encoded><![CDATA[
<p>And no, I didn't just pop a bow tie on my doorbell! I recently installed Home Assistant on a spare Raspberry Pi I had laying around, and though our house isn't very smart-enabled, I thought it wouldn't be too hard to improve our cheap doorbell to notify through our Google home speakers (or even phone).</p>
<p>TL;DR: although I went down the path of installing a module inside the doorbell receiver, this wasn't the sensible solution (due to battery drain) and I settled with a 433Mhz receiver to do the job.</p>
<p>There's quite a bit of detail in this post, because I think the methods are reusable beyond a doorbell, but in &quot;upgrading&quot; any dumb (also known as &quot;regular&quot;) hardware.</p>
<h2>Smart control over a doorbell</h2>
<p>What I have:</p>
<ul>
<li>A doorbell run on a coin cell (literally velcro'ed to the front door)</li>
<li>The doorbell receiver that has a speaker, an LED and 2 AA batteries</li>
</ul>
<p>My plan:</p>
<ul>
<li>When the doorbell is being pressed, the LED on the receiver lights up</li>
<li>Attach to the LED to piggyback the signal going HIGH</li>
<li>Using an ESP01 (tiny and cheap) running esphome then sends data to Home Assistant</li>
<li>Bonus extra: track battery level in receiver so I know when to change the batteries</li>
<li>Bonus extra: ability to control the speaker (so it doesn't &quot;ring&quot; before 8am)</li>
</ul>
<p>Components used:</p>
<ul>
<li><a href="https://remysharp.com/2021/04/28/fun-with-esp-modules">ESP01</a>, a wifi enabled controller - just because I had a few and there's enough pins on it, though any ESP8266 would work</li>
<li><a href="https://learn.adafruit.com/adafruit-ina219-current-sensor-breakout/overview">INA219</a>, a component to track voltage and current over <abbr title="I Squared C">I2C</abbr></li>
</ul>
<p>The software used to manage the interfacing from the ESP's pins was <a href="https://esphome.io/">ESPHome</a> a fairly powerful and configuration driven system.</p>
<p>Note the best schematic, but this is the wiring I've used:</p>
<figure><img src="https://remysharp.com/images/doorbell_schematic.png" alt="Schematic showing the wiring of the ESP and the INA module" decoding="async"></figure>
<p>I use an <a href="https://duckduckgo.com/?q=esp+programmer+dev+board&amp;t=ffab&amp;iar=shopping&amp;iax=shopping&amp;ia=shopping">ESP development board programmer</a> to flash the ESPHome firmware.</p>
<p>The configuration of the firmware configuration boils down to this important snippet:</p>
<pre><code class="language-yaml"><span class="token key atrule">binary_sensor</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> gpio
    <span class="token key atrule">pin</span><span class="token punctuation">:</span>
      <span class="token key atrule">number</span><span class="token punctuation">:</span> RX
      <span class="token key atrule">inverted</span><span class="token punctuation">:</span> <span class="token boolean important">True</span>
      <span class="token key atrule">mode</span><span class="token punctuation">:</span>
        <span class="token key atrule">input</span><span class="token punctuation">:</span> <span class="token boolean important">True</span>
    <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Doorbell Button"</span>
    <span class="token key atrule">filters</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> <span class="token key atrule">delayed_on</span><span class="token punctuation">:</span> 10ms
      <span class="token punctuation">-</span> <span class="token key atrule">delayed_off</span><span class="token punctuation">:</span> 10ms
    <span class="token key atrule">on_press</span><span class="token punctuation">:</span>
      <span class="token key atrule">then</span><span class="token punctuation">:</span>
        <span class="token punctuation">-</span> <span class="token key atrule">mqtt.publish</span><span class="token punctuation">:</span>
            <span class="token key atrule">topic</span><span class="token punctuation">:</span> doorbell/button
            <span class="token key atrule">payload</span><span class="token punctuation">:</span> <span class="token string">'pressed'</span>
</code></pre>
<p>I found that the LED in the doorbell receiver was controlled by the expoxy coated IC pulling one of the pins to ground, so I needed to connect to the correct side of the resistor (being used to protect the LED) and then when it went <code>LOW</code> I knew the LED was being lit up and thusly the doorbell had been pressed.</p>
<details>
<summary>See full <code>esphome-doorbell.yaml</code> listing</summary>
<pre><code class="language-yaml"><span class="token key atrule">substitutions</span><span class="token punctuation">:</span>
  <span class="token key atrule">wifi_ssid</span><span class="token punctuation">:</span>
  <span class="token key atrule">wifi_password</span><span class="token punctuation">:</span>
  <span class="token key atrule">broker_ip</span><span class="token punctuation">:</span>
  <span class="token key atrule">broker_username</span><span class="token punctuation">:</span>
  <span class="token key atrule">broker_password</span><span class="token punctuation">:</span>
  <span class="token key atrule">name</span><span class="token punctuation">:</span> esphome<span class="token punctuation">-</span>doorbell
  <span class="token key atrule">friendly_name</span><span class="token punctuation">:</span> doorbell
<span class="token comment"># remember to fill those in</span>


<span class="token key atrule">esphome</span><span class="token punctuation">:</span>
  <span class="token key atrule">name</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>name<span class="token punctuation">}</span>
  <span class="token key atrule">friendly_name</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>friendly_name<span class="token punctuation">}</span>
  <span class="token key atrule">min_version</span><span class="token punctuation">:</span> 2024.6.0
  <span class="token key atrule">name_add_mac_suffix</span><span class="token punctuation">:</span> <span class="token boolean important">false</span>
  <span class="token key atrule">project</span><span class="token punctuation">:</span>
    <span class="token key atrule">name</span><span class="token punctuation">:</span> esphome.web
    <span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">'1.0'</span>

<span class="token key atrule">esp8266</span><span class="token punctuation">:</span>
  <span class="token key atrule">board</span><span class="token punctuation">:</span> esp01_1m

<span class="token comment"># Enable logging</span>
<span class="token key atrule">logger</span><span class="token punctuation">:</span>

<span class="token comment"># Enable Home Assistant API</span>
<span class="token key atrule">api</span><span class="token punctuation">:</span>

<span class="token comment"># Allow Over-The-Air updates</span>
<span class="token key atrule">ota</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> esphome

<span class="token comment"># Allow provisioning Wi-Fi via serial</span>
<span class="token key atrule">improv_serial</span><span class="token punctuation">:</span>

<span class="token key atrule">wifi</span><span class="token punctuation">:</span>
  <span class="token key atrule">ssid</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>wifi_ssid<span class="token punctuation">}</span>
  <span class="token key atrule">password</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>wifi_password<span class="token punctuation">}</span>

  <span class="token comment"># Enable fallback hotspot (captive portal) in case wifi connection fails</span>
  <span class="token key atrule">ap</span><span class="token punctuation">:</span>
    <span class="token key atrule">ssid</span><span class="token punctuation">:</span> <span class="token string">'Doorbell Fallback Hotspot'</span>
    <span class="token key atrule">password</span><span class="token punctuation">:</span> <span class="token string">'doorbell'</span>


<span class="token comment"># In combination with the `ap` this allows the user</span>
<span class="token comment"># to provision wifi credentials to the device via WiFi AP.</span>
<span class="token key atrule">captive_portal</span><span class="token punctuation">:</span>

<span class="token key atrule">dashboard_import</span><span class="token punctuation">:</span>
  <span class="token key atrule">package_import_url</span><span class="token punctuation">:</span> github<span class="token punctuation">:</span>//esphome/example<span class="token punctuation">-</span>configs/esphome<span class="token punctuation">-</span>web/esp8266.yaml@main
  <span class="token key atrule">import_full_config</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>

<span class="token comment"># To have a "next url" for improv serial</span>
<span class="token key atrule">web_server</span><span class="token punctuation">:</span>

<span class="token key atrule">mqtt</span><span class="token punctuation">:</span>
  <span class="token key atrule">broker</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>broker_ip<span class="token punctuation">}</span>
  <span class="token key atrule">username</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>broker_username<span class="token punctuation">}</span>
  <span class="token key atrule">password</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>broker_password<span class="token punctuation">}</span>
  <span class="token key atrule">discovery</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
  <span class="token key atrule">discovery_prefix</span><span class="token punctuation">:</span> homeassistant

<span class="token key atrule">binary_sensor</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> gpio
    <span class="token key atrule">pin</span><span class="token punctuation">:</span>
      <span class="token key atrule">number</span><span class="token punctuation">:</span> RX
      <span class="token key atrule">inverted</span><span class="token punctuation">:</span> <span class="token boolean important">True</span>
      <span class="token key atrule">mode</span><span class="token punctuation">:</span>
        <span class="token key atrule">input</span><span class="token punctuation">:</span> <span class="token boolean important">True</span>
    <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Doorbell Button"</span>
    <span class="token key atrule">filters</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> <span class="token key atrule">delayed_on</span><span class="token punctuation">:</span> 10ms
      <span class="token punctuation">-</span> <span class="token key atrule">delayed_off</span><span class="token punctuation">:</span> 10ms
    <span class="token key atrule">on_press</span><span class="token punctuation">:</span>
      <span class="token key atrule">then</span><span class="token punctuation">:</span>
        <span class="token punctuation">-</span> <span class="token key atrule">mqtt.publish</span><span class="token punctuation">:</span>
            <span class="token key atrule">topic</span><span class="token punctuation">:</span> doorbell/button
            <span class="token key atrule">payload</span><span class="token punctuation">:</span> <span class="token string">'pressed'</span>

<span class="token key atrule">switch</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> gpio
    <span class="token key atrule">pin</span><span class="token punctuation">:</span>
      <span class="token key atrule">number</span><span class="token punctuation">:</span> TX
      <span class="token key atrule">mode</span><span class="token punctuation">:</span>
        <span class="token key atrule">output</span><span class="token punctuation">:</span> <span class="token boolean important">True</span>
    <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Speaker active"</span>

<span class="token key atrule">i2c</span><span class="token punctuation">:</span>
  <span class="token key atrule">sda</span><span class="token punctuation">:</span> GPIO2
  <span class="token key atrule">scl</span><span class="token punctuation">:</span> GPIO0
  <span class="token key atrule">scan</span><span class="token punctuation">:</span> <span class="token boolean important">True</span>

<span class="token key atrule">sensor</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> ina219
    <span class="token key atrule">address</span><span class="token punctuation">:</span> <span class="token number">0x40</span>
    <span class="token key atrule">shunt_resistance</span><span class="token punctuation">:</span> 0.1 ohm
    <span class="token key atrule">current</span><span class="token punctuation">:</span>
      <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Current"</span>
    <span class="token key atrule">power</span><span class="token punctuation">:</span>
      <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Power"</span>
    <span class="token key atrule">bus_voltage</span><span class="token punctuation">:</span>
      <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Bus Voltage"</span>
    <span class="token key atrule">shunt_voltage</span><span class="token punctuation">:</span>
      <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Shunt Voltage"</span>
</code></pre>
</details>
<p>After some soldering (and accidentally wiring the INA219 incorrectly causing a short circuit 🙈) I ended up with a working &quot;smart&quot;(ish) doorbell:</p>
<figure><img src="https://remysharp.com/images/doorbell_inside.jpg" alt="The wiring inside the doorbell receiver after I had upgraded it" decoding="async"></figure>
<p>With the web UI showing off the glorious power information and states:</p>
<figure><img src="https://remysharp.com/images/doorbell_web.jpg" alt="The closed receiver with the ESPHome dashboard in the background" decoding="async"></figure>
<h2>However…</h2>
<p>I was already missing one extra part for my project: a voltage step-up/boost for the ESP module.</p>
<p>Apparently ESPs can be picking about having a stable 3v3 DC power supply - which two AA batteries can't provide, so I had intended to add in a a <a href="https://www.adafruit.com/product/4654">TMS61023</a> - which provides 5v but can be adjusted to 3v3 by swapping out the resistors.</p>
<p>This would give me the correct voltage to the ESP to run reliably. That combined with the clever INA219 component to track and notify of low battery and it would be &quot;perfect&quot;.</p>
<p>Except… I needed to a do a little maths first.</p>
<p>The new components inside the doorbell receiver, the ESP in particular in idle state, was drawing around 70mAh. In the best scenario of having fresh batteries, that could hold (lets say on average) 2500mAh, that would mean the batteries would last… around 70 hours (2500 / 70 * 2). <strong>Less than 3 days 🤦.</strong></p>
<h2>Flip the script, use 433Mhz</h2>
<p>Instead of going inside the receive (which, I still maintain is clever, but really not sensible on batteries), apparently it's not uncommon to use a <abbr title="radio frequency">RF</abbr> receiver at 433Mhz to sniff for the doorbell message.</p>
<p>I happened to have a few laying around from an unknown project, and wiring up was a matter of VCC, GND and a data pin.</p>
<p>The first problem I ran into was that the <abbr title="radio frequency">RF</abbr> receiver requires 5v which the ESP doesn't use, but I also have a lot of Wemos D1 Minis which do have a 5v line so I swapped out the microcontroller (which still works with ESPHome) and had this:</p>
<figure><img src="https://remysharp.com/images/doorbell_433.jpg" alt="A Wemos microcontroller next to a simple RF receiver" decoding="async"></figure>
<p>The first phase was to detect the doorbell signal. Using this configuration flashed to the ESP, I was able to log any <abbr title="radio frequency">RF</abbr> signals, and with the doorbell button pressed down I immediately saw a report.</p>
<pre><code class="language-yaml"><span class="token key atrule">remote_receiver</span><span class="token punctuation">:</span>
  <span class="token key atrule">pin</span><span class="token punctuation">:</span> GPIO5
  <span class="token key atrule">dump</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> rc_switch
  <span class="token key atrule">tolerance</span><span class="token punctuation">:</span> 50%
  <span class="token key atrule">filter</span><span class="token punctuation">:</span> 250us
  <span class="token key atrule">idle</span><span class="token punctuation">:</span> 4ms
</code></pre>
<p>The log looked like this when I pressed the doorbell:</p>
<pre><code>[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='1111001000'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
[23:43:05][I][remote.rc_switch:261]: Received RCSwitch Raw: protocol=6 data='111100100011'
</code></pre>
<p>Notice how the data isn't 100% intact (around half way the packet is missing 2 bits). It's okay, because it's repeated around 80 times over about 2 seconds.</p>
<p>Now that I know the protocol and the data payload (<code>111100100011</code>), I can configure the ESPHome to this time <em>listen</em> for the payload and then trigger the doorbell event to my Home Assistant:</p>
<pre><code class="language-yaml"><span class="token key atrule">binary_sensor</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token key atrule">platform</span><span class="token punctuation">:</span> remote_receiver
    <span class="token key atrule">id</span><span class="token punctuation">:</span> doorbell
    <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">'Doorbell'</span>
    <span class="token key atrule">rc_switch_raw</span><span class="token punctuation">:</span>
      <span class="token key atrule">code</span><span class="token punctuation">:</span> <span class="token string">'111100100011'</span>
      <span class="token key atrule">protocol</span><span class="token punctuation">:</span> <span class="token number">6</span>
    <span class="token key atrule">filters</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> <span class="token key atrule">delayed_off</span><span class="token punctuation">:</span> 500ms
    <span class="token key atrule">on_press</span><span class="token punctuation">:</span>
      <span class="token key atrule">then</span><span class="token punctuation">:</span>
        <span class="token punctuation">-</span> <span class="token key atrule">homeassistant.service</span><span class="token punctuation">:</span>
            <span class="token key atrule">service</span><span class="token punctuation">:</span> esphome.rf_receiver_set_doorbell_on
</code></pre>
<p>Since this picks up on radio frequencies, it can now sit in my office with the other little sensors I've added using mains based power (rather than relying on changing batteries every 3 days!).</p>
<p>Now I've got an event when the doorbell rings (though I can't control the speaker any more, but I can tuck my head under a pillow if it's too early), and with that, the event can trigger pretty much anything from Home Assistant.</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/08/06/making-a-dumb-doorbell-smart">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Screen reading eff eff conf</title>
      <guid isPermaLink="false">screen-reading-eff-eff-conf</guid>
      <link>https://remysharp.com/2024/07/23/screen-reading-eff-eff-conf</link>
      <pubDate>Tue, 23 Jul 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[In a recent (personal) audit of accessibility and continued struggle to get my head around Voice Over (for macos), I remembered that the spoken sound of &quot;ffconf&quot; (as I'll usually display it) is &quot;ef-conf&quot; all sounded out in two syllables (it's hard to explain). So I wanted to revisit if it was possible to have a screen reader pronounce it the way I wanted and keep the visual style (as it's a brand as lowercase).]]></description>
      <content:encoded><![CDATA[
<p>In a recent (personal) audit of accessibility and continued struggle to get my head around Voice Over (for macos), I remembered that the spoken sound of &quot;ffconf&quot; (as I'll usually display it) is &quot;ef-conf&quot; all sounded out in two syllables (it's hard to explain). So I wanted to revisit if it was possible to have a screen reader pronounce it the way <em>I wanted</em> and keep the visual style (as it's a brand as lowercase).</p>
<h2>Testing</h2>
<p>Thanks to kind people on Mastodon I was pointed in the right direction of testing. Pairing Safari with Voice Over (when I was originally trying to use Firefox), and then JAWS and Firefox on Windows I was able to test a number of different iterations.</p>
<p>The original &quot;ffconf&quot; sounds like this:</p>
<p><audio preload controls src="/images/bad-ffconf.mp3"></audio></p>
<p>An extremely soft &quot;f&quot; at the start and then it peaks in to &quot;Conf&quot;. Which isn't ideal.</p>
<p>The pronunciation I was after was &quot;F F Conf&quot; (which also looks terrible written down). This is what it should sound like:</p>
<p><audio preload controls src="/images/good-ffconf.mp3"></audio></p>
<p>I also tried a number of variations on the text (along with some gracious help from <a href="https://adrianroselli.com/">Adrian Roselli</a>):</p>
<ul>
<li>Mixed case: <code>FFConf</code> - this works perfectly (when read out), but visually doesn't match what I needed.</li>
<li>Using aria-label: <code>&lt;span aria-label=&quot;FF Conf&quot;&gt;ffconf&lt;/span&gt;</code> - made no difference, but actually broke the reading up of the sentence so the screen reader would stop before the span, then read the span, then continue.</li>
<li>Using ABBR: <code>&lt;abbr title=&quot;FF Conf&quot;&gt;ffconf&lt;/abbr&gt;</code> - the abbreviation element isn't used by the screen reader in this case.</li>
<li>Using space and CSS: close</li>
<li>Using <code>aria-labelledby</code>: <code>&lt;span aria-labelledby=&quot;Ref Rest&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;ffconf&lt;/span&gt;&lt;span id=&quot;Rest&quot;&gt;&lt;/span&gt;&lt;span id=&quot;Ref&quot;&gt;f f conf&lt;/span&gt;</code> - works well but still breaks up the reading</li>
</ul>
<p>I also tried a number of joiners - none of which made a difference, it resulted in the same original &quot;fConf&quot; sound.</p>
<ul>
<li>Zero non joiner: <code>f&amp;zwnj;f&amp;zwnj;conf</code></li>
<li>Zero joiner: <code>f&amp;zwj;f&amp;zwj;conf</code></li>
<li>Zero width: <code>f&amp;#65279;f&amp;#65279;conf</code></li>
<li>Zero width no break space: <code>f&amp;#8203;f&amp;#8203;conf</code></li>
</ul>
<p>Here's a video with the audio of Voice Over and Safari:</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/xtCsS1ITd-g" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"  referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<p>If that doesn't work, am I just doing it wrong?</p>
<h2>Am I doing it wrong?</h2>
<p>Adrian has an excellent post entitled <a href="https://adrianroselli.com/2023/04/dont-override-screen-reader-pronunciation.html">Don't Override Screen Reader Pronunciation</a>, and this made me think a lot about someone who is using a screen reader is going to (I suspect, or hope) hear the &quot;f-conf&quot; and either assume it's the screen reader falling a little short, or that the event is actually called that (which… does it even matter)?</p>
<p>There's also how other people write FFConf and how that might appear on blog posts or social media - areas that I can't control.</p>
<p>Should I just keep it as &quot;ffconf&quot; and accept it <em>sounds</em> weird, or normalise on FFConf which pronounces correctly, but (to my eye) looks weird (like it's FFC and some other text).</p>
<p>…or is there one last trick in the bag?</p>
<h2>Probably overkill, but I'm happy with it</h2>
<p>What if I used font ligatures? Sort of like the text is akin to HTML and ligatures are akin to CSS; the structure remains the same, but visual design has flourishes.</p>
<p>I could create a ligature for the sequence &quot;FFC&quot; and (simply) show the &quot;ffc&quot; glyphs.</p>
<p>The web tool <a href="https://www.glyphrstudio.com/app/">Glyphr Studio</a> was perfect for this. I could upload my font, create a new ligature in the tool, carefully lay it out (and importantly ensuring the ligature's width is correct).</p>
<figure><img src="https://remysharp.com/images/glyphstudio.png" alt="Screenshot of Glyphr Studio and editing a single ligature" decoding="async"></figure>
<p>As someone who had never really messed around with fonts, I found this tool particularly easy to use to get the result I needed. Once I'd downloaded the <code>otf</code> file, I then converted it to the <code>woff</code> and <code>woff2</code> I needed and it's now up on the web site.</p>
<p>The end result, I think, is perfect:</p>
<figure><img src="https://remysharp.com/images/ffconf-spoken-good.png" alt="A screenshot of the FFConf web site and the DOM inspector open showing the text verses the visual representation of the text" decoding="async"></figure>
<p>It correctly reads as &quot;FFConf&quot; and visually it's &quot;ffconf&quot;.</p>
<p>Though I did later spot that when I had written &quot;FFCONF&quot; (usually linking to other people's posts), the text then appeared as &quot;ffcONF&quot; which was double weird! So I also have an &quot;FFCONF&quot; ligature that maps to &quot;ffconf&quot; to solve this too (a bit whack-a-mole, but hopefully that's the only mole).</p>
<h2>What about other places?</h2>
<p>Such as the <code>title</code> tag, or even the event sites like this <a href="https://2024.ffconf.org/">year's ffconf</a>?</p>
<p>Well, I've just had to accept that it doesn't need to be… whatever &quot;pixel perfect&quot; is in the audible world! In fact, some places we use CSS to transform to uppercase, which Voice Over will sometimes read as &quot;f-conf&quot; and sometimes read as &quot;F F C O N F&quot;.</p>
<p>It's the little battles that are fun.</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/07/23/screen-reading-eff-eff-conf">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Is AI part and parcel of web dev?</title>
      <guid isPermaLink="false">is-ai-part-and-parcel-of-web-dev</guid>
      <link>https://remysharp.com/2024/07/12/is-ai-part-and-parcel-of-web-dev</link>
      <pubDate>Fri, 12 Jul 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[Over the last year+ I've seen AI quickly increase its dominance in the social feeds I follow.
I know I'm in an echo chamber, but it feels like working on the web, you/we must understand how AI works and have some kind of integration in our products.
It feels like it's being bulldozed by private companies clambering over each other to gain control of user intimacy, whilst we live in a world that still hasn't properly integrated or understood the social and mental effects of social media (TL;DR: it bad).
This is compounded by the figures of costing vs. actual profit (cost being disproportionately higher than profit - needs reference though!) and the physical energy required to produce working AI models. It almost feels irresponsible to use this tech. The so-called net positive effect just isn't justified...is it?
AI can certainly be used for good, but do we already live in a world where if you're a software developer, particularly on the web, you have to be fully on board the AI train to be of actual value?
I'm really not sure, just sounding out. Maybe I see too many stupid use cases (like the Tour De France generated videos, or garbage code suggestion from paid tools like copilot) skewing my view.
I'd love to hear a balanced discussion on this topic, from the tooling, financial, energy/climate impact: &quot;is AI a prerequisite to develop for the web?&quot; at ffconf. I'd love it if you might &quot;tag&quot; anyone (ideally UK or Europe based) that you think would be a good fit.
(I originally posted this on LinkedIn, but wanted to capture it &quot;forever&quot; on my blog).]]></description>
      <content:encoded><![CDATA[
<p>Over the last year+ I've seen AI quickly increase its dominance in the social feeds I follow.</p>
<p>I know I'm in an echo chamber, but it feels like working on the web, you/we must understand how AI works <em>and</em> have some kind of integration in our products.</p>
<p>It feels like it's being bulldozed by private companies clambering over each other to gain control of user intimacy, whilst we live in a world that still hasn't properly integrated or understood the social and mental effects of social media (TL;DR: it bad).</p>
<p>This is compounded by the figures of costing vs. actual profit (cost being disproportionately higher than profit - needs reference though!) and the physical energy required to produce working AI models. It almost feels irresponsible to use this tech. The so-called net positive effect just isn't justified...is it?</p>
<p>AI can certainly be used for good, but do we already live in a world where if you're a software developer, particularly on the web, you have to be fully on board the AI train to be of actual value?</p>
<p>I'm really not sure, just sounding out. Maybe I see too many stupid use cases (like the <a href="https://www.instagram.com/reel/C8pMkOWOUiL/">Tour De France generated videos</a>, or garbage code suggestion from paid tools <a href="https://remysharp.com/links/2024-02-09-395b2ad3">like copilot</a>) skewing my view.</p>
<p>I'd love to hear a balanced discussion on this topic, from the tooling, financial, energy/climate impact: &quot;is AI a prerequisite to develop for the web?&quot; at ffconf. I'd love it if you might &quot;tag&quot; anyone (ideally UK or Europe based) that you think would be a good fit.</p>
<p><em>(I originally posted this <a href="https://www.linkedin.com/feed/update/urn:li:activity:7216056343168778240/">on LinkedIn</a>, but wanted to capture it &quot;forever&quot; on my blog).</em></p>
<p><em>Originally published on <a href="https://remysharp.com/2024/07/12/is-ai-part-and-parcel-of-web-dev">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Say what? On tinnitus and hearing</title>
      <guid isPermaLink="false">say-what-on-tinnitus-and-hearing</guid>
      <link>https://remysharp.com/2024/07/05/say-what-on-tinnitus-and-hearing</link>
      <pubDate>Fri, 05 Jul 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[In January this year I was upgraded from guy who has tinnitus and bad hearing, to upgraded ears (via hearing aids) and, for a brief time, no tinnitus.]]></description>
      <content:encoded><![CDATA[
<p>In January this year I was upgraded from guy who has tinnitus and bad hearing, to upgraded ears (via hearing aids) and, for a brief time, no tinnitus.</p>
<h2>NHS audiology department</h2>
<p>As part of the excellent care I get from the <a href="https://remysharp.com/2022/04/26/ms-update">MS</a> nurse/specialist here in Brighton, the specialist ask if there's anything else going on that she can help with. <a href="https://remysharp.com/search?q=tinnitus">Tinnitus</a> has been a long standing part of my life (very high levels from around the age of 23), and so she referred me to the audiology department.</p>
<p>I wasn't in any particular rush and (I think) about 9 months later I had an appointment booked to see a hearing specialist.</p>
<p>Before digging into the tinnitus, their process is to check the hearing and also run an MRI to check for an rare condition called <a href="https://www.nhs.uk/conditions/menieres-disease/">Ménière's disease</a> - which the result came back quickly confirm that I don't have this.</p>
<p>However the hearing specialist did check over my hearing test and as I'd seen before (around when I was doing the Linear therapy) I've got loss of hearing in the higher frequencies.</p>
<figure><img src="https://remysharp.com/images/hearing-2023.png" alt="Two charts showing a range of hearing ability with a strong drop off at the right side on both charts, where &quot;fs&quot; and &quot;th&quot; sounds are heard" decoding="async"></figure>
<p>In both ears the report was that I have &quot;moderate high-frequency hearing loss&quot;.</p>
<p>What I didn't know about the hearing test was that it's based around sounds of speech. Which is why there's letters on the chart. What this shows is that I was often missing the &quot;fs&quot; and &quot;th&quot; sounds. For example, if someone said &quot;Do you want fish&quot;, I'd likely lose the word &quot;fish&quot; and in my head fill in with most obvious word within the context of what was happening (ie. we're looking at dinner options).</p>
<p>The audiologist immediately offered me hearing aids to trial. I could turn them down, and internally that's exactly what I wanted to do, but I knew logically it would be the right thing to do.</p>
<p>I will admit that after I left the appointment I was completely distraught. Much, <em>much</em> more so than when I was diagnosed with MS. My own mum had hearing aids from her mid-40s (the same as me now) and I definitely picked up the shame she felt wearing them and spending decades trying to hide them.</p>
<p>There is a weird stigma (from my generation certainly) that if you wear glasses, you're considered smart, clever, even sexy. If you wear hearing aids, you're old, breaking, a bit of a failure. It's completely stupid - though I can see how glasses that aid poor eyesight can be used as a fashion accessory.</p>
<p>Anyway I would be fitted with the hearing aids in January this year (the original appointment being in November before). I did reading around hearing aids, I talked <em>a lot</em> to Julie and my mum about her hearing aids, and I actually managed to make peace with it by the time the appointment came around to INSTALL MY NEW ROBOTIC EARS! 🤖🦻</p>
<figure><img src="https://remysharp.com/images/hearing-aids.jpg" alt="A hearing aid: clear tube to insert in the ear and dark brown unit" decoding="async"></figure>
<hr>
<p>One slightly annoying/failing in the tech, is that when the battery is low it'll ring out (in my ear) which is fine, but it'll then keep ringing out every 60 seconds or so until you replace the battery. I had hoped it would be more of a warning like &quot;10% left, only a few hours of life left&quot;, but no, it's &quot;CHANGE ME NOW!&quot;. Because of this, I made myself a <a href="https://www.printables.com/model/862775-hearing-aid-holder-size-13">tiny carry case (for size 13 batteries)</a> which I always have a pair of batteries in my trousers or shorts watch pocket.</p>
<h2>A brief moment of no ringing</h2>
<p>The original reason for visiting the ENT department was for tinnitus. I was sent this (excellent) <a href="https://www.youtube.com/watch?v=6uHPHKZ1Yrw&amp;t=1868s">NHS video on tinnitus</a> - a lot of which I know, but the section I linked to is about how the NHS propose that hearing aids can help.</p>
<p>The fundamental problem with tinnitus is that it's not fully understood, and thusly there's no ensured method to dampen it. Still, the working theory in this video is that the brain is <em>trying</em> to hear a sound that doesn't exist, so there's this sound that's the result of adjustment. The hearing aids are customised to my hearing and compensates those sounds that I can't hear. In theory, then my brain says &quot;oh hey, the ear can actually hear those sounds, so I can back off&quot;.</p>
<p>For the first couple of weeks there were actual days that I couldn't hear my tinnitus. I could <em>feel</em> it, somewhere in my head, but I couldn't hear it. That (was) a pretty huge deal.</p>
<p>However, since then (and rather quickly for me), my tinnitus is a strong presence in my head. I know from personal experience that my tinnitus bothers me more when I'm working on it (because it's moved into my focus). Most mornings (probably <em>every</em> morning) I wake up to the screaming in my head. I can't really hear the kids or Julie (my wife) properly in the morning (part hearing, part tinnitus, part fatigue).</p>
<p>The hearing aids <em>do</em> relieve some of that pressure, and I can actually feel my hearing opening up when I put my hearing aids in and turn them on, but the tinnitus, for me today, and for the last 6 months, is still there.</p>
<h2>Tinnitus therapy</h2>
<p>I was also offered hearing therapy, specifically around tinnitus from the NHS which I took up. I have a monthly phone call with the therapist who is helping me to develop methods to help my brain cope with the sounds.</p>
<p>Really this is something I should have done years, or even decades ago. There's a few methods I'm using, though once in particular that I still find challenging:</p>
<ol>
<li>Using pillow speakers to <a href="https://www.brain.fm/">listen to sounds</a> in the night (I usually wake up at 4am and the tinnitus is loud, so trying to tune into another sound is the aim).</li>
<li>Thought identification. This means being aware of my thoughts about tinnitus, but then voicing them out loud it <em>&quot;I feel that I wish it were quieter in my head&quot;</em>, and then again with <em>&quot;I notice that I feel that I wish it were quieter in my head&quot;</em>. I understand this kind of therapy, and I see it as &quot;putting a light on scary thoughts&quot;, it so of diminishes the concerns… a little. I usually do this late at night in bed, out loud, but quietly. The sound of my voice does also calm the tinnitus.</li>
<li>Practise switching my attention. I find this particularly hard, but the idea is: hone into an external sound (perhaps trying to visualise it too) for around 30 seconds. Then move the attention to the sound of the tinnitus for 30 seconds. Then to another sound, then to the tinnitus and finally one more repeat. This is supposed to help train my brain to understand that the tinnitus will still be there and doesn't need my immediate attention. I'm struggling a lot with this but I think it's because I've had over 20 years of hearing the tinnitus and <em>that</em> being the highest priority in my brain that it's baked pretty deeply into my system.</li>
</ol>
<hr>
<p>So that's my journey so far. I really do love how the hearing aids have opened up my hearing and, if I say so myself, I'm proud of myself for getting over the shame I initially felt.</p>
<p>I hope this can help some of you that also suffer from tinnitus. Thanks for reading.</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/07/05/say-what-on-tinnitus-and-hearing">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Adding tests to a TypeScript, Next, tRPC project without the faff</title>
      <guid isPermaLink="false">adding-tests-to-a-typescript-next-trpc-project-without-the-faff</guid>
      <link>https://remysharp.com/2024/06/07/adding-tests-to-a-typescript-next-trpc-project-without-the-faff</link>
      <pubDate>Fri, 07 Jun 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[Aside from the cringe inducing stack I've got layered, with client projects you (actually, me) work with the hand you've been dealt.
I needed to add tests to this particular project (which will eventually be open source) and the typical route would have been Jest, but that failed hard. So here's what I did to get tests working.]]></description>
      <content:encoded><![CDATA[
<p>Aside from the cringe inducing stack I've got layered, with client projects you (actually, me) work with the hand you've been dealt.</p>
<p>I needed to add tests to this particular project (which will eventually be open source) and the typical route would have been Jest, but that failed hard. So here's what I did to get tests working.</p>
<h2>Tooling</h2>
<p>Great tools are great. However, I'm a stickler for config hell and I quickly lose my patience when the exact coordination of tools aren't stacked stacked in the exact right way, often leaving my screen full of obscure errors and even more obscure obscenities coming out of my face hole.</p>
<p>There's many test frameworks, and I've used a lot of them. Jest is one that I tend to reach for because it bundles a lot of the test mechanism I use (expects, mocks and so on).</p>
<p>Where Jest gets…tricky, is when it's used with modules, which tends to mean &quot;imports aren't going to work&quot;. So this is when more tools are added.</p>
<p>When the project (this one in particular) is using Next.js which has &quot;zero config&quot; TypeScript support, but dropping that into Jest isn't straight forward.</p>
<p>Problems included <code>SyntaxError: Cannot use import statement outside a module</code> and I <a href="https://nextjs.org/docs/app/building-your-application/testing/jest">tried</a> quite a <a href="https://jestjs.io/blog/2019/01/25/jest-24-refreshing-polished-typescript-friendly#typescript-support">few</a> <a href="https://jestjs.io/docs/getting-started#using-typescript">solutions</a> - I felt like I was close with <a href="https://github.com/kulshekhar/ts-jest">ts-jest</a>, but never <em>quite</em> there.</p>
<p>I can't remember exactly how or why I pivoted (probably a github issue on Jest) but I tried Vitest and instantly was unblocked.</p>
<h2>Vitest</h2>
<p>I can appreciate a simple to understand <a href="https://vitest.dev/">homepage</a> and Vitest does that well:</p>
<blockquote>
<ul>
<li>
<p>Out-of-box ESM, TypeScript and JSX support powered by esbuild.</p>
</li>
<li>
<p>Jest Compatible: Expect, snapshot, coverage, and more - migrating from Jest is straightforward.</p>
</li>
</ul>
</blockquote>
<p>Moreover, it seemed almost config/faff free.</p>
<p>Adding <code>vite.config.mts</code> (not sure what <code>.mts</code> is…) to the root of the project with the following had me running immediately:</p>
<pre><code class="language-js"><span class="token comment">/// &lt;reference types="vitest" /></span>

<span class="token keyword">import</span> <span class="token punctuation">{</span> defineConfig <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vite'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> tsconfigPaths <span class="token keyword">from</span> <span class="token string">'vite-tsconfig-paths'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineConfig</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
  <span class="token literal-property property">plugins</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token function">tsconfigPaths</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token comment">// make my aliases work</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>From there I'm unblocked.</p>
<h2>Testing tRPC</h2>
<p>I know the API for testing has changed already (from the point of tutorials I'd found and what was the current code), but once I'd figured this out I created a helper which then exposes all tRPC API to my tests:</p>
<pre><code class="language-js"><span class="token comment">// helper.ts</span>
<span class="token keyword">import</span> type <span class="token punctuation">{</span> NextApiRequest<span class="token punctuation">,</span> NextApiResponse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next'</span><span class="token punctuation">;</span>

<span class="token comment">// the normal tRPC route that's used in the server</span>
<span class="token keyword">import</span> router <span class="token keyword">from</span> <span class="token string">'~/lib/router'</span><span class="token punctuation">;</span>

<span class="token comment">// the result of initTRPC.create()… then createCallerFactory is exposed</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> createCallerFactory <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'~/lib/trpc'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">makeCaller</span><span class="token punctuation">(</span><span class="token parameter">opts <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> createCaller <span class="token operator">=</span> <span class="token function">createCallerFactory</span><span class="token punctuation">(</span>router<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> callerOptions <span class="token operator">=</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">req</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token keyword">as</span> NextApiRequest<span class="token punctuation">,</span>
    <span class="token literal-property property">res</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token keyword">as</span> NextApiResponse<span class="token punctuation">,</span>
    <span class="token literal-property property">rateLimit</span><span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">,</span> <span class="token comment">// rateLimit and user is bespoke to my code</span>
    <span class="token literal-property property">user</span><span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">,</span>
    <span class="token operator">...</span>opts<span class="token punctuation">,</span> <span class="token comment">// allows me to overload as required in my tests</span>
  <span class="token punctuation">}</span><span class="token punctuation">;</span>

  <span class="token keyword">return</span> <span class="token function">createCaller</span><span class="token punctuation">(</span>callerOptions<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>I also prefer test code that actually imports the function it's using (rather than having to guess what's been magically injected into the global scope). Here's a simple/pointless litmus test:</p>
<pre><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> expect<span class="token punctuation">,</span> test <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vitest'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> makeCaller <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./helper'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> getLatestVersion <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'~/lib/handlers/changelog'</span><span class="token punctuation">;</span>

<span class="token function">test</span><span class="token punctuation">(</span><span class="token string">'change log'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> caller <span class="token operator">=</span> <span class="token function">makeCaller</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

  <span class="token keyword">const</span> res <span class="token operator">=</span> <span class="token keyword">await</span> caller<span class="token punctuation">.</span>changelog<span class="token punctuation">.</span><span class="token function">latest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token function">expect</span><span class="token punctuation">(</span>res<span class="token punctuation">.</span>version<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toEqual</span><span class="token punctuation">(</span><span class="token function">getLatestVersion</span><span class="token punctuation">(</span><span class="token string">'1'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<h2>Tools with less faff win</h2>
<p>It seems like the days of &quot;killer config&quot; originating back in the height of WebPack (though probably slightly earlier with Grunt and Gulp et al), this mindset that developers should have to stack up layers and layers of config seems to have become de facto - but it really doesn't need to.</p>
<p>I'll always gravitate towards config being hidden away from me when it comes to tooling.</p>
<p><s>Complicated</s> Faff is not a badge of honour after all.</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/06/07/adding-tests-to-a-typescript-next-trpc-project-without-the-faff">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Casio f-91w Modding</title>
      <guid isPermaLink="false">casio-f91w-modding</guid>
      <link>https://remysharp.com/2024/05/25/casio-f91w-modding</link>
      <pubDate>Sat, 25 May 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[I do love my Pebble mostly because it still uses tactile buttons and it's a slim watch. Though recently I saw that Casio offered a design in a bright orange colour (and then found a multitude of colours available) and really wanted to brighten up my style.
Obviously, quickly it got out of hand!]]></description>
      <content:encoded><![CDATA[
<p>I do love my <a href="https://remysharp.com/search?q=pebble">Pebble</a> mostly because it still uses tactile buttons and it's a slim watch. Though recently I saw that Casio offered a design in a bright orange colour (and then found a multitude of colours available) and really wanted to brighten up my style.</p>
<p>Obviously, quickly it got out of hand!</p>
<figure><img src="https://remysharp.com/images/casio/array.jpg" alt="Casio watch colours" decoding="async"></figure>
<p>However, the one thing that I absolutely adore about Pebble is the app that I wrote that I rely heavily when I (managed to) go to the gym: <a href="https://apps.rebble.io/en_US/application/53ff41ed8cdf37902b000050?query=rest&amp;section=watchapps">My Rest App</a>.</p>
<p>But you can't write apps for a Casio… lol, of course you can!</p>
<h2>Sensor Watch</h2>
<p>There's a few different PCB replacement projects for the Casio f-91, but I quickly found the <a href="https://www.sensorwatch.net/">Sensor Watch</a> project, and though I don't need the sensors (and actually I bought the &quot;lite&quot; version that doesn't let you add more sensors beyond the temperature), you <em>can</em> write your own watch faces for it.</p>
<p>The documentation is a little spread out, but <a href="https://github.com/joeycastillo/Sensor-Watch">github repo</a> has everything I needed to create my own customisations and to create my own watch face.</p>
<p><a href="https://www.crowdsupply.com/oddly-specific-objects/sensor-watch#products">Ordering and delivery</a> was pretty quick (I assumed it was delivered from the US to the UK) and installation was very straight forward. You need to make sure you have a small enough screw driver to remove the back panel, otherwise everything slots out.</p>
<p>You will also need a solder iron if you want the buzzer support (it's a wafer thin piezo buzzer glued to the metal backing).</p>
<p>With a few hours of programming in C, I was able to port the functionality of my Rest app to the Sensor Watch. If you're interested, the <a href="https://github.com/remy/Sensor-Watch/blob/main/movement/watch_faces/complication/rest_face.c">source code is on github</a>. One of the key design aspects was that the &quot;start&quot; button (the alarm right hand side button) would restart the timer whether it's overrun or still running - making it a timer I can use with minimal fuss.</p>
<p>It's not much to look at, but here's my Rest app side by side:</p>
<figure><img src="https://remysharp.com/images/casio/rest_app.jpg" alt="Casio Rest App" decoding="async"></figure>
<p>The light button (top left) cycles the timers (30 seconds, 60 seconds and 120 seconds), the alarm button (bottom right) starts the timer (and restarts if it's overrun), the mode button (bottom left) stops and resets the timer <em>if</em> it's running, and if it's not running, cycles to the next watchface.</p>
<p>However, there is one pretty important feature that I miss from the Pebble: the vibration motor. My Pebble would give me a tactile nudge when the timer was up, but the Casio only has an LED (which is much brighter than the default LED that comes with the Casio watches) and buzzer, and most of the time in the gym, I have headphones on (plus, being British I would <em>hate</em> to make a sound that might disturb anyone!).</p>
<h2>Vibrate support for the f-91</h2>
<p>Other people have already faced this problem, and smart cookie has already documented and created themselves a PCB that adds a vibration motor to the Casio f-91 and documented it in <a href="https://www.instructables.com/MAKE-IT-VIBRATE-Vibrator-Module-for-Casio-F-91W/">an Instructable</a>.</p>
<p>It does add extra thickness, but it's so cramped inside the watch that there's really no option.</p>
<p>I've messaged the author to ask (as they have written) for the PCB CAD files but I've not heard back. However, I plan to recreate this PCB but with a cut out for the motor to try to sit more shallow inside the watch. I'm also going to test with a coin and a column based motor to see how they sit.</p>
<p>Once I've got vibrate support, I think my orange watch will be perfect.</p>
<h2>Backlight</h2>
<p>If you've ever owned a Casio watch, you'll know that the &quot;light&quot; is extremely weak and probably only going to give you the hour (since the rest of the watch is in darkness).</p>
<p>The sensor watch does replace the LED, which is actually bright enough to use as a weak torch in the dark. But when if you <em>just</em> want a better backlight?</p>
<p>There's a few options.</p>
<p>The first is to &quot;simply&quot; swap the LED with a new surface mount LED. If you're happy with surface mount soldering and tiny components, <em>and</em> you have an SMD LED knocking around, a green LED is a good substitute.</p>
<figure><img src="https://remysharp.com/images/casio/led.jpg" alt="LED swap" decoding="async"></figure>
<p>In the photo above, the LED is a 0603 (I think) which is too small so I had to bridge the tabs with solder. It's a little messy, but it does the job. Here's the comparison - though appreciate that the photo is trying to make the light much better than appears in reality. The left shows the original LED, the right shows the newly installed LED:</p>
<figure><img src="https://remysharp.com/images/casio/led_compare.jpg" alt="LED swap" decoding="async"></figure>
<h3>Light spreader</h3>
<p>This is a no solder modification that much more evenly spreads the light on the watch, even with the original LED.</p>
<p>I found a <a href="https://www.etsy.com/uk/listing/1448973768/back-light-spreader-for-casio-f-91w-a">supplier on Etsy</a> and their delivery arrived in good time.</p>
<p>The installation was straightforward (though they also include a video to follow too), but it's a matter of disassembly, removing the white backing to the LCD, and inserting both a new backing (which can be paper, though I chose &quot;ESR&quot;) and then inserting the spreader. The spreader is a thin layer of transparent plastic with a gradient of layered white dots which helps spread the light much more evenly.</p>
<p>The reviews of the product are extremely positive and when it came to installing the plastic on my own watch (this time the pink Casio variant) I was a little doubtful of how well it worked. But once it was installed, it really did improve the backlight many, many times over.</p>
<figure><img src="https://remysharp.com/images/casio/light_spreader_in_dark.jpg" alt="Light spreader in action" decoding="async"></figure>
<p>The backlight is really even and incredibly easy to see in the dark.</p>
<h2>LCD filters</h2>
<p>The filters (can) look really cool as a simple mod. It's an extremely thin, partially transparent layer that sits on the lens.</p>
<p>I bought a pack of <a href="https://www.etsy.com/uk/listing/1374696404/casio-f-91w-monochrome-gradient-filter">colour filters from Etsy</a> (via Germany to the UK, again pretty quick).</p>
<p>I'm on the fence on these though. The problem I faced was that there was no guidance on how to remove the filter (in case of mistake), and I hadn't realised how delicate the Casio's own lens printed image is (the bit around the LCD). It's <em>incredibly</em> thin paint.</p>
<p>I installed the filter on a blue watch I had, but decided that I didn't like the colour and wanted to try a different colour (since the pack included the full range), but when I was trying to remove the filter, I nicked the paint on the lens. It's extremely small, but <em>I know</em> it's there.</p>
<p>If you install these, I would strongly advise that you expose nearly all of the self adhesive, but not all. I kept a corner still covered (after learning from my mistake).</p>
<p>I also felt like I could never quite get it 100% perfectly flat against the glass lens. There were no air bubbles, but when the light catches you can see there's something in the way. I was using the light blue Casio I had, and started with the blue filter (which I thought was <em>too</em> blue), then the green, but I thought I had air under the lens, then the red (which you can see below), which actually makes the LCD quite hard to read.</p>
<figure><img src="https://remysharp.com/images/casio/lens_mod.jpg" alt="Lens filter" decoding="async"></figure>
<p>You can see from the angle I've tilted the watch that it's not quite 100% flat to the glass, and I did eventually notice the same effect on the Etsy page too. It's certainly not noticeable from arm's length, but if you know it's there, then the obsessive perfectionist sees it.</p>
<p>In the end however, I removed these entirely - but I do think they're a nice idea.</p>
<h2>Making it water proof, to nearly 1000m</h2>
<p>Although the f-91 is a measly &quot;water resistant&quot; (though this has been tested to be mostly okay to swim with), you can modify the watch further to make it fully waterproof.</p>
<p>The process involves disassembling the watch, submerging it in olive oil (Extra Virgin … apparently) and whilst in the oil, fully reassemble the watch. The oil creates a seal that prevents the water from penetrating and corroding the PCB - so now it's waterproof (and from photos apparently easier to see underwater).</p>
<p>Here's the story of how one individual tested their <a href="https://forum.tz-uk.com/showthread.php?330958-The-Little-Watch-That-Could">modded watch dropping it to the sea bed</a>!</p>
<h2>Misc other modding ideas</h2>
<p>There's a couple more mods that might be interesting to play with. Once is a lens replacement (though the only supplier I can find has closed their store as of a year ago, and I can't seem to find anyone else). I've got a couple of Casio watches with broken glass lens (which I was going to harvest the straps), but a new lens would be fun.</p>
<p>Finally, you can invert the colours of the LCD by removing the original polarizer and <a href="https://www.watchuseek.com/threads/casio-f91w-mod-negative-display-diy.757778/">replacing and rotating accordingly</a>. It's a neat hack (and might look good with the red filter from earlier) and given my experience with Game Boy polarizers, should be fairly accessible to me.</p>
<p>I'll try to update this post if/when I get around to adding a vibration motor to the Casio watch too.</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/05/25/casio-f91w-modding">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>CSS text fragment selector (please)</title>
      <guid isPermaLink="false">css-text-fragment-selector</guid>
      <link>https://remysharp.com/2024/04/17/css-text-fragment-selector</link>
      <pubDate>Wed, 17 Apr 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[A little while ago (read: 14 years ago) I wished for a parent selector, and we finally have :has 🎉.
Now, how about the ever useful jQuery selector :contains but reusing the text fragment syntax?]]></description>
      <content:encoded><![CDATA[
<p>A little while ago (read: 14 years ago) <a href="https://remysharp.com/2010/10/11/css-parent-selector">I wished for a parent selector</a>, and we finally have <code>:has</code> 🎉.</p>
<p>Now, how about the ever useful jQuery selector <code>:contains</code> but reusing the <a href="https://developer.mozilla.org/en-US/docs/Web/Text_fragments">text fragment</a> syntax?</p>
<h2>Text fragment selector</h2>
<p>Although <a href="https://caniuse.com/mdn-html_elements_a_text_fragments">Firefox's support</a> is (<a href="https://mozilla.github.io/standards-positions/#scroll-to-text-fragment">currently</a>) missing, the text fragment support to jump to and highlight a block (or more) of text is good.</p>
<p>This means I can share and link to specific content regardless of whether there's a <code>name</code> or <code>id</code> attribute on the element (because that's how we do &quot;classic&quot; text linking!).</p>
<p>So the parser exists (though I'm not sure if it's a standard…maybe someone can clarify), so the next logical step is to bake this into a CSS selector, and finally we can have what jQuery was <a href="https://blog.jquery.com/2007/08/24/jquery-114-faster-more-tests-ready-for-12/">rocking in 2007</a>.</p>
<p>The syntax, I would expect, would look like this:</p>
<pre><code class="language-css"><span class="token selector">[:~:text="fragment selector"]</span> <span class="token punctuation">{</span>
  <span class="token property">background</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">/* or */</span>
<span class="token selector">[:~:text="Although,text is good"]</span> <span class="token punctuation">{</span>
  <span class="token property">background</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<h2>Implementation questions</h2>
<p>Beyond the complexities of the actual implementation, there is a question of <em>what</em> exactly the expression should match. Should it match the <code>#text</code> node, or should it match the direct parent element? Although if it did target the <code>#text</code> node, we could use <code>:has</code> to select the parent (though…this isn't as good as an ascender selector).</p>
<p>What if the selector crosses multiple DOM nodes? Would I expect to target them all or into the highest node?</p>
<p>Finally, I've noticed that the text fragment works but only on initial load. This is seen if you have <code>::target-text</code> to highlight the text (though my copy of Chrome has it's own default style), it highlights the block, but on refresh, the highlight is lost. I'm not sure what the technical reason for this is, but wonder if that might add complications to a CSS based implementation.</p>
<h2>Who this really benefits</h2>
<p>As a selector for developers and authors, I can't see this being a core selector for developers, but there's a decent use case where the author might want to clean up third party injected content.</p>
<p>Moreover, and to me, even more excitedly, this would put a significant amount of power in the user's hands via user style sheets. This would allow users to control what content was hidden from them using relatively simple text expressions.</p>
<p>Don't want to see Facebook's follow suggestions, irrespective of the trash fire classnames in use, you'll be able to target the element by text, cycle up to the container, and whoosh, gone.</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/04/17/css-text-fragment-selector">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>groupBy and other ways that I'm an old dog</title>
      <guid isPermaLink="false">groupby-and-other-ways-that-im-an-old-dog</guid>
      <link>https://remysharp.com/2024/04/12/groupby-and-other-ways-that-im-an-old-dog</link>
      <pubDate>Fri, 12 Apr 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[More of a TIL but my blog is rather bare lately, so this will do.
I was browsing the excellent MDN to check something and came across both Map.groupBy and Object.groupBy. A handy static method for keying an array by some property (I'll show working example in a moment).
Pretty useful, but I quickly realised that I'm very much stuck in the camp of the old dog not quite learning the new tricks.]]></description>
      <content:encoded><![CDATA[
<p>More of a <a href="https://remysharp.com/til">TIL</a> but my blog is rather bare lately, so this will do.</p>
<p>I was browsing the excellent MDN to check something and came across both <code>Map.groupBy</code> and <code>Object.groupBy</code>. A handy static method for keying an array by some property (I'll show working example in a moment).</p>
<p>Pretty useful, but I quickly realised that I'm very much stuck in the camp of the old dog not quite learning the new tricks.</p>
<h2>groupBy</h2>
<p>Before I chastise myself, here's an example.</p>
<p>I've got an array of pets and their names, but I want to break it up into dogs, cats and others (because I treat a tortoise and a hamster equally interesting…).</p>
<p>My data:</p>
<pre><code class="language-json"><span class="token punctuation">[</span>
  <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"Coco"</span><span class="token punctuation">,</span> type<span class="token operator">:</span> <span class="token string">"dog"</span><span class="token punctuation">,</span> age<span class="token operator">:</span> <span class="token string">"9m"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"Taco"</span><span class="token punctuation">,</span> type<span class="token operator">:</span> <span class="token string">"cat"</span><span class="token punctuation">,</span> age<span class="token operator">:</span> <span class="token string">"1y8m"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"Kipo"</span><span class="token punctuation">,</span> type<span class="token operator">:</span> <span class="token string">"cat"</span><span class="token punctuation">,</span> age<span class="token operator">:</span> <span class="token string">"2y8m"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"Disco"</span><span class="token punctuation">,</span> type<span class="token operator">:</span> <span class="token string">"cat"</span><span class="token punctuation">,</span> age<span class="token operator">:</span> <span class="token string">"2y8m"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"Mrs Garry Crackers"</span><span class="token punctuation">,</span> type<span class="token operator">:</span> <span class="token string">"hamster"</span><span class="token punctuation">,</span> age<span class="token operator">:</span> <span class="token string">"unknown"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"Sir Biscuit"</span><span class="token punctuation">,</span> type<span class="token operator">:</span> <span class="token string">"hamster"</span><span class="token punctuation">,</span> age<span class="token operator">:</span> <span class="token string">"unknown"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">"Delilah"</span><span class="token punctuation">,</span> type<span class="token operator">:</span> <span class="token string">"tortoise"</span><span class="token punctuation">,</span> age<span class="token operator">:</span> <span class="token string">"mega unknown"</span> <span class="token punctuation">}</span>
<span class="token punctuation">]</span>
</code></pre>
<p>Ordinarily, I would use a <code>Array.reduce</code> map and hope that muscle memory serves me well on whether the accumulator is the first or second argument.</p>
<p>This is how it's achieved using <code>Object.groupBy</code>:</p>
<pre><code class="language-js"><span class="token keyword">const</span> groupedPets <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">groupBy</span><span class="token punctuation">(</span>pets<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> type <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">===</span> <span class="token string">'dog'</span> <span class="token operator">||</span> type <span class="token operator">===</span> <span class="token string">'cat'</span><span class="token punctuation">)</span> <span class="token keyword">return</span> type<span class="token punctuation">;</span>
  <span class="token keyword">return</span> <span class="token string">'other'</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Object { dog: (1) […], cat: (3) […], other: (3) […] }</span>
</code></pre>
<p>Quite a bit simpler. If I needed to key by a non-string property, then the <code>Map.groupBy</code> is the way. i.e. if I wanted to look up using <code>{ type: &quot;dog&quot; }</code> from the result.</p>
<h2>Support and moving to new stuff</h2>
<p>In this particular case, the <a href="https://caniuse.com/?search=groupby">support is pretty good</a> - partly due to browsers auto-updating and that this seemed to land at the same time in Firefox and Chrome (because, yes, there's only two rendering engines…).</p>
<p>Equally, this is one of those methods that is a prime candidate for a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy#see_also">polyfill</a> (I like to think that writing polyfills is great practise for coding as the obvious answer isn't always the most complete).</p>
<p>Of course, if you were using lodash then you possibly always used this in your toolkit.</p>
<p>I've noticed for myself though, that I'm still stuck very much in coding like it's 2020 (perhaps without the lockdowns and scary pandemic). Just this week I realised I never write <code>const fs = require('node:fs')</code>. Equally I've <em>never</em> used <code>yield</code> or a generator function in the wild.</p>
<p>Am I long in the tooth? Probably… Should I care? I think so…whilst at the same time: not really (there's something deeper here, so I'll try to stay on topic).</p>
<p>Back in the early 2000s, I would use the browser developer tools (Firebug at the time and the Venkman debugger) and I would poke the DOM and objects and cruise through their properties looking for things I didn't recognise and see if I could find out more.</p>
<p>I don't really have a process like that these days, mostly I think because my code is sitting on top of code that belongs to Facebook/React, so even peeking under the hood is kind of pointless because it'll completely change quickly enough.</p>
<p>Perhaps there's a useful summary of updates landing browsers that exists (I'm 99% sure there is) - feel free to enlighten me (comments, DMs, email - what have you).</p>
<p>I should really keep up, even if I'm behind by 12 months - I'm too far away from retirement!</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/04/12/groupby-and-other-ways-that-im-an-old-dog">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Apple going to kneecap the web: PWAs</title>
      <guid isPermaLink="false">apple-trying-to-kneecap-the-web-pwas</guid>
      <link>https://remysharp.com/2024/02/28/apple-trying-to-kneecap-the-web-pwas</link>
      <pubDate>Wed, 28 Feb 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[Remember the days that Apple used to pitch itself as David in the David and Goliath stories? Well, the tables have turned. Apple, in a short number of days, are going to intentionally kill off PWA support.
2-March 2024: Apple has backed down (for now), thanks to the efforts of amazing people, but the fight continues. Read the details

This affects developers, businesses and users.
When any documentation eventually emerged, Apple not only intentionally confused readers as to the reasoning, but also lied, and not just a little lie, just outright lie.
Alex Russell has an excellent long form piece that explains the reality of what's going on. Take 20 minutes of your day and read it.
Do two things
There's two things you can do to help to attempt to fight back against Goliath Apple:

Sign the open letter to Tim Cook
Blog (feel free to copy this post if you want), toot, bookface, linkitin, tweet (or ecks…?) - it doesn't have to be long, but it has to be now.

Safari has long held users back. If Apple succeed here, they'll have delivered a heavy blow to the open web (and frankly, some decent choices for your phones).
Don't just read this: fight back]]></description>
      <content:encoded><![CDATA[
<p>Remember the days that Apple used to pitch itself as <em>David</em> in the David and Goliath stories? Well, the tables have turned. Apple, in a short number of days, are going to intentionally kill off PWA support.</p>
<div class="update"><strong>2-March 2024:</strong> Apple has backed down (for now), thanks to the efforts of amazing people, but the fight continues. <a href="https://open-web-advocacy.org/blog/apple-backs-off-killing-web-apps/">Read the details</a>
</div>
<p>This affects developers, businesses <em>and</em> users.</p>
<p>When any documentation eventually emerged, Apple not only intentionally confused readers as to the reasoning, but also lied, and not just a little lie, <a href="https://infrequently.org/2024/02/home-screen-advantage/#lies%2C-damned-lies%2C-and-%22still%2C-we-regret...%22">just outright lie</a>.</p>
<p>Alex Russell has an <a href="https://infrequently.org/2024/02/home-screen-advantage/">excellent long form piece that explains the reality</a> of what's going on. Take 20 minutes of your day and read it.</p>
<h2>Do two things</h2>
<p>There's two things you can do to help to <em>attempt</em> to fight back against Goliath Apple:</p>
<ol>
<li><a href="https://letter.open-web-advocacy.org/">Sign the open letter to Tim Cook</a></li>
<li>Blog (feel free to <a href="https://github.com/remy/remysharp.com/blob/main/public/blog/apple-trying-to-kneecap-the-web-pwas.md">copy this post</a> if you want), toot, bookface, linkitin, tweet (or ecks…?) - it doesn't have to be long, but it has to be now.</li>
</ol>
<p>Safari has long held users back. If Apple succeed here, they'll have delivered a heavy blow to the open web (and frankly, some decent choices for your phones).</p>
<p>Don't just read this: <a href="https://letter.open-web-advocacy.org/"><strong>fight back</strong></a></p>
<p><em>Originally published on <a href="https://remysharp.com/2024/02/28/apple-trying-to-kneecap-the-web-pwas">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Why my code isn't in TypeScript</title>
      <guid isPermaLink="false">why-my-code-isnt-in-typescript</guid>
      <link>https://remysharp.com/2024/02/23/why-my-code-isnt-in-typescript</link>
      <pubDate>Fri, 23 Feb 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[I came across a reasonably interesting question on Reddit today, ask &quot;If you don't use TypeScript, tell me why (5 year follow up)&quot;.
Frustratingly it was mostly replies as to why the dev was using TypeScript, or replies to those who didn't saying that they're probably using TypeScript wrong. I guess that's the state of the developer bro/you're wrong culture we've managed to create.
Anyway, it made me want to consider why my code doesn't use TypeScript and what I personally (still) struggle with when it's a requirement of the client work.]]></description>
      <content:encoded><![CDATA[
<p>I came across a reasonably interesting question on Reddit today, ask &quot;If you don't use TypeScript, tell me why (5 year follow up)&quot;.</p>
<p>Frustratingly it was mostly replies as to why the dev <em>was</em> using TypeScript, or replies to those who didn't saying that they're probably using TypeScript wrong. I guess that's the state of the developer bro/you're wrong culture we've managed to create.</p>
<p>Anyway, it made me want to consider why my code doesn't use TypeScript and what I personally (still) struggle with when it's a requirement of the client work.</p>
<h2>Parsing</h2>
<p>A &quot;well crafted&quot; definition, type or interface (still no idea when I should use each), is often a huge cognitive load on me.</p>
<p>Being presented with lots of double colons, <code>&lt;T&gt;</code> when I'm not sure what <code>T</code> refers to, a wall of interfaces and more is an upfront cost on me, the reader.</p>
<p>Often the types will be tucked away in other files (probably good) but working out the argument required to a function call often leaves me distracted in the task of understanding what's required rather than making my function call.</p>
<p>I'm sure if I spent a few years loving TypeScript then maybe parsing the types will become muscle memory. But for something that could be easily solved with documentation (and scarcely much would be needed), I find it hard to be motivated.</p>
<p>Speaking of documentation...</p>
<h2>Docs and examples</h2>
<p>I've lost count of the number of libraries that point to their type definitions as an excuse for actually providing docs, or even a surface level API.</p>
<p>Pointing to types is no better than the Java-like generated docs. There's inevitably an argument called <code>options</code> that's defined as an <code>object</code> pointing to MDN to help the reader understand what an object is, rather than documenting the properties.</p>
<p>Again, remember reader (and you trolls, I know you can read too), this is my own experience, not yours.</p>
<p>Then we come to examples. This is where in the work I'm doing lately requires I take some code samples from projects like Next, which are written as TypeScript, and as soon as it's pasted into our project (and duly adjusted), the code is shouting RED SNAKES... TypeScript is angry about something that's been introduced by the sample code that was indeed itself TypeScript.</p>
<p>I'm yet to understand whether it's not validated on the example site or if our project has some extra stuff in TypeScript that means it's not compatible.</p>
<h2>Appeasing TypeScript</h2>
<p>This is probably the hill I'll die on. There are three main blockers I run into on a regular basis:</p>
<ul>
<li>I spend more time trying to resolve TypeScript complaints than I do adding code</li>
<li>Our team has lost hours on TypeScript exceptions in staging and production builds (but oddly not offline/local) where some external type was missing or incompatible, another one being that the local environment passes linting but CI doesn't, it shouldn't be so hard</li>
<li>Having to rewrite correctly and infallible JavaScript so that it was friendly enough for TypeScript to understand</li>
</ul>
<p>I've used TypeScript in a single file app that was being run with Deno. I think I could count the types defined on one hand and it didn't help anyone (but it was a Netlify Edge function, which has to use Deno, so TypeScript was fine to use). In this case, the code is so simplistic that it both doesn't benefit from TypeScript but is a breeze to code: no red snakes.</p>
<p>But when it comes to team code, a larger, more complicated codebase (one &quot;at scale&quot; some buzzword bingo players might say), it's hazardous. I'm regularly presented with a wall of errors from TypeScript in VSCode telling me through some form of recursion (I'm guessing) that either the argument in, or the result out is incompatible.</p>
<figure><img src="https://remysharp.com/images/ts-trace.png" alt="Trace" decoding="async"></figure>
<p>Here's a real example. I even pay for CoPilot, almost exclusively, to explain TypeScript errors to me (goodness knows what the TypeScript library can't do a decent job of explaining errors, we devs are suckers for shit tools). But half the time CoPilot suggests some nonsense change (or even exactly the same code that's already in place). At best, it gives me a hint as to what I should start editing.</p>
<p>But…sometimes it doesn't help:</p>
<figure><img src="https://remysharp.com/images/ts-rewrite-fail.png" alt="Copilot attempting to fix TypeScript" decoding="async"></figure>
<p>Finally, having written JavaScript for such a long time (going back to '99), I've spent a lot of time trying to fully understand the language and capability. So every now and then (more often than I would have thought), I run into a problem where TypeScript insists that there's potential for a error, yet it's impossible in JavaScript (known as the classic &quot;Remy Knows Best&quot;… (sure, I'm not infallible at all!)).</p>
<p>Still, this was the latest example:</p>
<figure><img src="https://remysharp.com/images/ts-impossible.png" alt="TypeScript's wrong" decoding="async"></figure>
<p>In this case, TypeScript specifically wanted the <code>upperCase</code> call to protect against being <code>undefined</code>, this code avoided the error: <code>_[0]?.toUpperCase()</code>.</p>
<p>The problem is that <code>_[0]</code> can never be undefined, it's just not possible.</p>
<p><em>Edit: I've been writing this post over a week, and it's now been a month before I finally got to the bottom of this…</em></p>
<p>It turned out, buried deep inside the project code was <code>noUncheckedIndexedAccess: true</code>, which had only lead to the code being littered with the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining">optional chaining</a> operator <code>?</code> (which, I'd argue adds extra cognition to parse the code as a developer).</p>
<p>It's in these processes that I have wasted so much time.</p>
<hr>
<p>I am entirely certain that TypeScript helps other developers, and my problems are <em>my problems</em>. But, prompted by the Reddit post, I found it useful to articulate what's been troubling me. I wonder if I'll return to this post in another 5 years with a different experience...</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/02/23/why-my-code-isnt-in-typescript">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Removing episodes, audiobooks and more from Spotify</title>
      <guid isPermaLink="false">removing-episodes-audiobooks-and-more-from-spotify</guid>
      <link>https://remysharp.com/2024/02/02/removing-episodes-audiobooks-and-more-from-spotify</link>
      <pubDate>Fri, 02 Feb 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[I've removed shows from Spotify in the past, but the change was to find the specific request and modify what was being sent. Eventually it would, and did fail.
So I've gone one step higher and now I'm intercepting every request Spotify makes and I can modify the content as I wish.]]></description>
      <content:encoded><![CDATA[
<p>I've removed <a href="https://remysharp.com/2021/08/17/removing-shows-from-spotify">shows from Spotify</a> in the past, but the change was to find the specific request and modify what was being sent. Eventually it would, and did fail.</p>
<p>So I've gone one step higher and now I'm intercepting every request Spotify makes and I can modify the content as I wish.</p>
<h2>TL;DR</h2>
<p>If you just want to remove episodes and so on from the homepage, you to:</p>
<ol>
<li>Extract xpui.js from the xpui.spa (this is a zip file with .spa extension)</li>
<li>Prepend the code found here</li>
<li>Add the xpui.js back into the zip xpui.spa</li>
</ol>
<p>If you're on a Mac or *nix-like OS, then <a href="https://remysharp.com/downloads/spotify-fix">this script</a> will do the trick fairly easily:</p>
<pre><code class="language-sh"><span class="token builtin class-name">cd</span> /Applications/Spotify.app/Contents/Resources/Apps <span class="token comment"># where spotify lives</span>
<span class="token function">cp</span> xpui.spa xpui.spa.bak <span class="token comment"># backup the source</span>
<span class="token function">unzip</span> xpui.spa xpui.js <span class="token comment"># extract the guts</span>
<span class="token function">curl</span> https://gist.githubusercontent.com/remy/f7a1941a90ea3404a9a6a7384d420fd1/raw/spotify-rewrite.js <span class="token parameter variable">-o</span> spotify-rewrite.js
<span class="token function">cat</span> spotify-rewrite.js xpui.js <span class="token operator">></span> xpui.tmp.js <span class="token comment"># add the patch to the top</span>
<span class="token function">mv</span> xpui.tmp.js xpui.js <span class="token comment"># rename</span>
<span class="token function">zip</span> xpui.spa xpui.js <span class="token comment"># put back in spotify</span>
</code></pre>
<p><strong>You can download the script from here: <a href="https://remysharp.com/downloads/spotify-fix">spotify-fix</a></strong></p>
<p>You'll need to re-run every time Spotify updates (make sure to <code>chmod u+x spotify-fix</code> and move to a directory in your <code>$PATH</code>).</p>
<h2>Under the hood</h2>
<p>What's happening is that I'm including a complete patch of the <code>fetch</code> method so that we can intercept requests and modify the response.</p>
<p>I've written a (mostly) fully fledge library for an &quot;in the middle patch&quot; for <code>fetch</code> that allows intercepting requests by characteristics but also sending the request to different endpoints (thought this isn't required by the Spotify fix at all). This library is available on <a href="https://github.com/remy/fetch-rewrite/">github/fetch-rewrite</a> and <a href="https://www.npmjs.com/package/fetch-rewrite">npm/fetch-rewrite</a>.</p>
<p>The intention is that it makes it easier for you to modify what you filter. Perhaps you do want podcasts in Spotify, the code that does the patching looks like this:</p>
<pre><code class="language-js"><span class="token function">rewriteFetch</span><span class="token punctuation">(</span><span class="token punctuation">[</span>
  <span class="token punctuation">{</span>
    <span class="token literal-property property">url</span><span class="token operator">:</span> <span class="token string">"/pathfinder/v1/query"</span><span class="token punctuation">,</span>
    <span class="token literal-property property">query</span><span class="token operator">:</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">operationName=home&amp;</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span>
    <span class="token literal-property property">modify</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token function">json</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">const</span> block <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"episodes"</span><span class="token punctuation">,</span> <span class="token string">"audiobooks"</span><span class="token punctuation">,</span> <span class="token string">"podcasts"</span><span class="token punctuation">,</span> <span class="token string">"shows"</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
        data<span class="token punctuation">.</span>data<span class="token punctuation">.</span>home<span class="token punctuation">.</span>sectionContainer<span class="token punctuation">.</span>sections<span class="token punctuation">.</span>items <span class="token operator">=</span>
          data<span class="token punctuation">.</span>data<span class="token punctuation">.</span>home<span class="token punctuation">.</span>sectionContainer<span class="token punctuation">.</span>sections<span class="token punctuation">.</span>items<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>res<span class="token punctuation">.</span>data<span class="token punctuation">.</span>title<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>res<span class="token punctuation">.</span>data<span class="token punctuation">.</span>title<span class="token punctuation">.</span>text<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
            <span class="token keyword">const</span> needle <span class="token operator">=</span> res<span class="token punctuation">.</span>data<span class="token punctuation">.</span>title<span class="token punctuation">.</span>text<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token operator">!</span>block<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> needle<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
          <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> data<span class="token punctuation">;</span>
      <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>It might look unwieldy, but hopefully you can see that to <em>include</em> podcasts, you'd remove the word &quot;podcast&quot; from the block list.</p>
<h2>Going forward</h2>
<p>For Spotify in particular, this makes updating the requests much easier. If Spotify changes their format and the filter breaks, then the fetch rewrite silently fails, and returns the original data (so you'll see shows, etc reappear).</p>
<p>In the longer term, I actually want to run a proxy on my local machine <em>and</em> my mobile phone. Then I can intercept Spotify requests in the same way and rewrite the result - but I can also shut off their rubbish on my phone. This would also be impervious to software updates (as this patch needs to be run on each update).</p>
<p>I'd approach this with the <a href="https://mitmproxy.org/">mitmproxy</a> project and have to write the logic in Python (I was thinking to use <a href="https://jqlang.github.io/jq/">jq</a> to do the rewrite logic - which I'm a big fan of but I think would also be easier to read). This should also port to mobile relatively simply (though I've yet to jump into builds for mobile).</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/02/02/removing-episodes-audiobooks-and-more-from-spotify">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>The Doggie Loggie</title>
      <guid isPermaLink="false">the-doggie-loggie</guid>
      <link>https://remysharp.com/2024/01/11/the-doggie-loggie</link>
      <pubDate>Thu, 11 Jan 2024 00:00:00 +0000</pubDate>
      <description><![CDATA[As we (try) to train Coco, one of the big tasks is being able to leave the dog alone for a decent amount of time (made trickier by the fact that both Julie and I work from home so are usually always around).
We also learnt from our Battersea training that if the dog has a negative association with being left alone (because, for example, there's no build up) it can be distressing for the dog.
So I built a small bit of hardware that monitors for sound, logging to an SD card and then visualises it in the browser. Here's how I did it.]]></description>
      <content:encoded><![CDATA[
<p>As we (try) to train <a href="https://remysharp.com/2023/10/04/coco">Coco</a>, one of the big tasks is being able to leave the dog alone for a decent amount of time (made trickier by the fact that both Julie and I work from home so are usually always around).</p>
<p>We also learnt from our Battersea training that if the dog has a negative association with being left alone (because, for example, there's no build up) it can be distressing for the dog.</p>
<p>So I built a small bit of hardware that monitors for sound, logging to an SD card and then visualises it in the browser. Here's how I did it.</p>
<h2>Hardware</h2>
<p>I'm a big fan of &quot;as cheap as possible&quot;, so here's the shopping list with approximate prices I paid:</p>
<ul>
<li>RP2040 Zero - a mini Raspberry Pico that does the logic work - £1.48</li>
<li>INMP441 Microphone module - probably overkill, but I picked it up - £1.26</li>
<li>OLED 128x32 0.91&quot; - just to show it was working (not really needed) - £1.37</li>
<li>MicroSD card breakout board+ - completely overkill, but I had it laying around - £7.20</li>
</ul>
<p>Powered from USBc. Once it's booted up, it'll show a &quot;Waiting to start&quot; on the display, then I use the &quot;boot&quot; button on the RP2040 to signal the start of recording.</p>
<p>Below is the wiring (sorry, I'm not great at Fritzing). Because I'm taking the super cheapo approach, the final build result is very scrappy.</p>
<figure><img src="https://remysharp.com/images/doggie-loggie-fritzing.png" alt="" decoding="async"></figure>
<p>Once wired up, there's two software components. The first is the firmware for the RP2040, written in micropython, and the rendering of data that's stored on the SD card (which is done in the browser).</p>
<p>Once I had this wired up, instead of 3D printing a fancy case for it, I just squished all the parts together to make a weird ball of wires and bits!</p>
<figure><img src="https://remysharp.com/images/doggie-loggie.jpg" alt="" decoding="async"></figure>
<p>Yes, I know, it's a hot mess.</p>
<h2>Software on the hardware</h2>
<p>The bulk of the code in micropython is mostly noddy code that creates instances of hardware (such as the OLED display).</p>
<p>The full source is <a href="https://download.remysharp.com/doggie-loggie.py">available here (doggie-loggie.py)</a> (or as a <a href="https://gist.github.com/remy/506f44454eff34848fe9601baf55664d">gist</a> if you prefer). It's not anything special.</p>
<p>The micropython also requires you add a package called &quot;SSD1306_I2C&quot; - in <a href="https://thonny.org/">Thonny</a> once the RP2040 is connected via USB, Tools -&gt; Manage Packages and search for the package and add.</p>
<p>The code also uses a patch on the boot button to treat it as a user button (which I quite like - and can also be done on the Raspberry Pico which I've done before). This is achieved with the following snippet of code:</p>
<pre><code class="language-python"><span class="token keyword">import</span> rp2

<span class="token keyword">if</span> rp2<span class="token punctuation">.</span>bootsel_button<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
  <span class="token comment"># do a thing</span>
</code></pre>
<p>The code to calculate the amount of sound was courtesy of Copilot 🤭 - though the code was wrong on a few occasions, eventually I ended up with something that gave me a value of 50-60 for &quot;quiet&quot; and 100 for loud. Whether this is actually decibels isn't really important - it's enough to <em>know</em> there's sound.</p>
<p>This data is visualised on the OLED display (which is kind of pointless, but I used it to debug) but also written to the SD card.</p>
<p>Each time the <code>toggleRun</code> function starts (a new &quot;loggie&quot;), a single <code>0x255</code> byte is written to the <code>/sdcard/doggie.log</code> file. This is my simplistic header (or just a marker that I can split the data on), then the noise capture callback runs every one second.</p>
<p>The sound value is mapped so that it can be charted, but also compressed to a scale from 0 to 120. This also means that I can store this converted value on the SD card as a byte since it'll never be our marker value of <code>0x255</code>. So now I have a series of data that's captured every second as a single byte.</p>
<h2>Software-software</h2>
<p>Once the logging process is over, I'll pull the power from the device (which is fine for the SD card since each write opens and closes the file - overkill, but did the tricks for me).</p>
<p>Then the file is dragged into a <a href="https://doggie-loggie.netlify.app/">browser window</a>. The data is split by <code>0x255</code>, and then the value is rebased back to the rough decibel values (approximately <code>n * 2 + 60</code>) and then rendered into the chart.</p>
<figure><img src="https://remysharp.com/images/doggie-loggie-chart.png" alt="" decoding="async"></figure>
<h2>In practise</h2>
<p>In reality, there's not a huge demand for using this. We've used it when we left Coco for an hour and quickly realised we'd ramped up her alone time too quick (we could see she was whining and crying nearly all the time), so we reset back to 15 minutes and are doing that daily, then increasing the time (we're on 20-25 minutes so far).</p>
<p>I guess you could repurpose this project to hide it under a Christmas tree to see if Santa makes any noise whilst dropping off presents, but I suspect his magic dust interferes with audio waves… I'm sure there's <em>some</em> other useful use. If not, I certainly learnt a couple of new tricks.</p>
<p><em>Originally published on <a href="https://remysharp.com/2024/01/11/the-doggie-loggie">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>My 2023</title>
      <guid isPermaLink="false">my-2023</guid>
      <link>https://remysharp.com/2023/12/31/my-2023</link>
      <pubDate>Sun, 31 Dec 2023 00:00:00 +0000</pubDate>
      <description><![CDATA[That's another year done and time for another &quot;my years&quot; post, where I attempt to log what I got up to so that in 2033 I can look back and chuckle from my full body haptic AR web browsing kit I bought from Amaoogle for £3,999.99.
So, please sit back, engage your eyeballs whilst I prattle on dear future Remy and all those present-now humung beans.]]></description>
      <content:encoded><![CDATA[
<p>That's another year done and time for another <a href="https://remysharp.com/my-years">&quot;my years&quot;</a> post, where I attempt to log what I got up to so that in 2033 I can look back and chuckle from my full body haptic AR web browsing kit I bought from Amaoogle for £3,999.99.</p>
<p>So, please sit back, engage your eyeballs whilst I prattle on dear future Remy and all those present-now humung beans.</p>
<h2>Work</h2>
<p>Lol. This should be short! I finished up a contract working with <a href="https://stef.io/">Stef</a> whom I've had the pleasure of working with many times over the years (and Stef, if you're reading, I hope to again).</p>
<p>In reality, at the end of 2022 I had hit a bit of a impenetrable brick wall. By the time the '22 Christmas holidays were over I was in a discombobulated state over the prospect of software development. It wasn't the contract, but the previous pandemic years finally catching up.</p>
<p>I'd been going full throttle since the pandemic had landed in the fear that work would dry up (Julie's industry, events, collapsed weeks before the pandemic landed in the UK fully).</p>
<p>I was stuck very much in a loop of: what's the point?</p>
<p>The upshot is that since day 1 of the business, I've always been risk adverse and kept a long financial runway in case I'm discovered as a fraud and can't find work for a year or so. Instead, it was my brain that had left the building so with the agreement of my family, I took April to the end of the year off (though excluding ffconf).</p>
<p>So, work was, thankfully, quiet. I'm a little nervous about returning to it in 2024, but I'll keep that for next year's post.</p>
<h2>Off work for 9 months</h2>
<p>On paper this looks like a lark, but the practical side really wasn't just sitting on the sofa watching TV.</p>
<p>I really wanted to get into side projects (specifically <a href="https://jsbin.com/">JS Bin</a>) but in reality, I didn't find the motivation to start coding again until July and then September I got back into enjoying it.</p>
<p>The reality was mental health was the priority. I've struggled over many years with my own demons and in February I wrote about <a href="https://remysharp.com/2023/02/28/the-flavour-of-my-funk">The flavour of my funk</a>. It also included my <em>happy list</em>, which at the time was working more as a flotation device with a half-life of a few hours each.</p>
<p>Looking at my blog, my self-agreement to write twice a month fell to the wayside, reading books slowed right down and in many ways I was much less engaged in my computer (which I've used for so many aspects of my life for decades).</p>
<p>In March I finally had the <a href="https://remysharp.com/2023/03/11/i-had-a-good-week">start of an uptick</a>, and most importantly to my year was that I started therapy (again) and would continue to until mid-October (with the agreed plan to return in Feb '24). It would take a good few months, but things would incrementally get more quiet in my head.</p>
<h2><abbr title="Multiple Sclerosis">MS</abbr></h2>
<p>With my diagnosis of <a href="https://www.nhs.uk/conditions/multiple-sclerosis/"><abbr title="Multiple Sclerosis">MS</abbr></a> at the start of 2020 (though I'm sure it was end of 2019), things have been a bit up and down. I started <a href="https://www.mssociety.org.uk/about-ms/treatments-and-therapies/disease-modifying-therapies"><abbr title="Disease Modifying Therapies">DMT</abbr></a> with some of the strongest stuff available and the good news is the MRIs say there's no change to the four legions having a party inside my body.</p>
<p>I've had a mix of weirdness around my feet and legs (which has been recurring for me) and interestingly I've found that <em>more</em> walking, although initially uncomfortable, in the long run actually helps to reduce the discomfort in my legs.</p>
<p>In addition to this, the <abbr title="Multiple Sclerosis">MS</abbr> team in East Sussex are really amazing (the <a href="https://remysharp.com/2018/07/31/my-nhs-story">NHS</a> really is incredible), and I was asked if I wanted to attend an &quot;<abbr title="Multiple Sclerosis">MS</abbr> fatigue workshop&quot;.</p>
<p>The workshop was superb, run entirely on the efforts of the two <abbr title="Multiple Sclerosis">MS</abbr> specialist nurses (where they needed to get our feedback at the end of 6 weeks to help convince the higher ups that this was worth their time - <em>it absolutely was</em>). Once a week for 6 weeks, I joined a small group of other people who have <abbr title="Multiple Sclerosis">MS</abbr> and suffer from fatigue.</p>
<p>It's hard to explain, because it's easy for someone to chime in with &quot;yeah, I'm really tired too&quot;. Though I think COVID in some ways has helped understand what the fatigue is. The long COVID fatigue is apparently similar. The fog that comes over your brain. The inability to concentrate. The exhaustion from doing literally nothing.</p>
<p>One week we were asked to log over a period of 3-5 days, on every hour, what the level of fatigue was and what were were doing. From this simple process, I discovered two easily identifiable triggers for me: bright sunshine outdoors - it immediately trips me into mega sluggish mode. Simple(ish) solution, sunglasses and shade. Just reduce the light coming in. The other was semi-busy social situations. It saps me of energy.</p>
<p>These discoveries don't mean I need to avoid the situations but instead I know how to work with them, allowing myself rest - and equally that Julie <em>knows</em> that I'll be exhausted afterwards (and it isn't just laziness).</p>
<h3>MRIs and my brain on drugs</h3>
<p>Part of my <abbr title="Multiple Sclerosis">MS</abbr> also means I have yearly MRIs, but this year my consultant asked if I would be willing to partake in a study in to fatigue and <abbr title="Multiple Sclerosis">MS</abbr>, in exchange for £50 and imaging data of my brain. Oh - photos of my brain - yes please!</p>
<p>Short version, here's my brain:</p>
<figure><img src="https://remysharp.com/images/brain.avif" alt="MRI scan of Remy's brain, his straight nose rather prominent!" decoding="async"></figure>
<p>Funnily enough, I had another MRI today… MRIs are <a href="https://www.youtube.com/watch?v=nFkBhUYynUw">pretty amazing</a>.</p>
<h2>Therapy</h2>
<p>I'm not sure how much I've got to say about the process but I've always believed that talky-therapy is good for everyone, no (known) baggage required.</p>
<p>For me, in this round, it's helped me get some handle on the body shame that I've carried around with me for many decades. There's a lot of noise in my head. Sometimes it's &quot;Bob&quot; sometimes it's just awful shit that my brain says to me.</p>
<p>Julie bought me a decal that's now in our bathroom mirror that reads &quot;I am worthy&quot;. On the surface (and this is probably Bob doing his thing) I find these kind of affirmations stupid. But the reality is that in the mornings, when I see it, my brain says &quot;You're worthless&quot; and my other brain says &quot;Hang on, it says right there that I <em>am</em> worthy, so you're wrong. Stupid brain&quot;.</p>
<p>My therapy came to a natural end around the end of September and we closed up in October, but my counsellor/therapist (I'm not sure which is what) said that we could continue a drop in either monthly, or bi-monthly, or whatever I like. I love this approach, and I want to keep her (my therapist) in my life as long as I can.</p>
<h2>Personal development</h2>
<p>My family started Duolingo (for better or worse) this year and I randomly decided to jump on board too, learning Japanese (though in reality my time would be better spent (re)learning French or even German). Still, I thought it could help me play Japanese RPGs (it can't, I'm about 5 years off of that being a sniff of a reality!).</p>
<p>I do have a 200+ day streak to my name and I even managed #1 in their diamond league - which I then immediately turned my account to private so I wouldn't be obsessively gamed over my league position!</p>
<p>My Japanese is at the point where I can recognise sounds and the break up of words, but I'm currently at the stage (in Duolingo) where I'm able to link sounds to words but I probably can't tell you accurately (without prompts) what the words mean (this is bad)!</p>
<p>I've also started to learn <abbr title="British Sign Language">BSL</abbr>. I'm using an app called <a href="https://www.lingvano.com/bsl/">Lingvano</a> which I'm really enjoying. The whole family is learning. It's something that Julie and I sometimes use to communicate across distances (like: &quot;do you know where E is?&quot;, &quot;do you want a coffee?&quot;). It's also because my hearing is not good (you'll probably get a post about this in Jan) and we want a backup plan in case it gets as bad as both of our mother's own hearing. Plus, it's pretty rewarding.</p>
<p>Finally, not <em>quite</em> personal development, but something of a coup for me: I've been completing games. Recently I worked my way through all of the Metroid games (including the 3DS which I bought a faulty 3DS and <a href="https://remysharp.com/tif/2023-03-16-3ds">fix it up</a> specifically). I've never been able to complete games, but apparently all it took was some sticking power!</p>
<h2>Side projects</h2>
<p>As I mentioned, software didn't really come to me until much later on in the year, though I did managed to restore <a href="https://webmention.app/">webmention.app</a> (after <a href="https://remysharp.com/2023/01/30/on-vercel-if-some-of-my-sites-are-down">Vercel blocked my account</a> at the start of the year).</p>
<p>I also released a service (after blogging about the idea) that uses the Internet Archive to &quot;unrot&quot; old links: <a href="https://unrot.link/">unrot.link</a>.</p>
<p>Beyond software, I've continued to play in the hardware land, restoring vintage consoles. Mostly Game Boys but more recently enjoying the WonderSwan. I made a little site where you can buy my repaired warez if you're interested: <a href="https://retrobyrem.uk">retrobyrem.uk</a></p>
<h2>ffconf</h2>
<p>After returning in 2022 the ticketing and sponsor experience of ffconf 2023 was very different. Tickets sold out but took much longer to get there and sponsors are <a href="https://remysharp.com/2023/09/14/why-sponsor-ffconf">exceptionally thin</a>.</p>
<p>However, in spite of this, the event was still a resounding success. All the videos are available to <a href="https://ffconf.org/talks/?filter=&amp;years=2023">watch online</a>, and I've already built up a list of topics I want for the 2024 edition (though I'll try to avoid planning anything for another few weeks!).</p>
<p>It's also amazing to watch the family grow with the event over the years:</p>
<figure><img src="https://remysharp.com/images/ffconf-before-after.jpg" alt="" decoding="async"></figure>
<h2>Finally: family</h2>
<p>Last year Taco came to join our family (becoming besties with Kipo - our tabby, whilst Disco continues to use our home as a cat-hotel). This year we (for the first time) bring a dog into our lives.</p>
<p>Coco.</p>
<p>She's a cross Chihuahua and Yorkipoo… a Chorkiepoo (or just &quot;a mix&quot; if you're like me and think these names are stupid).</p>
<p>Coco also loves Taco - so the family of animals get on rather well 💞 (as do the humungs…most of the time!)</p>
<figure><img src="https://remysharp.com/images/coco-taco.jpg" alt="" decoding="async"></figure>
<p><em>Originally published on <a href="https://remysharp.com/2023/12/31/my-2023">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Unrot (that) link</title>
      <guid isPermaLink="false">unrot-that-link</guid>
      <link>https://remysharp.com/2023/12/04/unrot-that-link</link>
      <pubDate>Mon, 04 Dec 2023 00:00:00 +0000</pubDate>
      <description><![CDATA[I present to you, a gift, a gift of a service that will &quot;unrot&quot; your links, cleverly named… unrot.link.
The service promises to prevent and undo link rot on long lived web sites, via a service based implementation of my No More 404 method, free for all.]]></description>
      <content:encoded><![CDATA[
<p>I present to you, a gift, a gift of a service that will &quot;unrot&quot; your links, cleverly named… <a href="https://unrot.link">unrot.link</a>.</p>
<p>The service promises to prevent and undo link rot on long lived web sites, via a service based implementation of my <a href="https://remysharp.com/2023/09/26/no-more-404">No More 404</a> method, free for all.</p>
<p>I've published as much documentation that I could think would be useful on the unrot.link web site, including an <a href="https://unrot.link/docs/how/">interactive walkthrough</a> of how the server side process works (for the nerds and those who want to port to their own language).</p>
<p>When I originally wrote the post on No More 404, one concern was the 2 second timeout - both for myself but also raised by <a href="https://chriscoyier.net/2023/10/11/remys-dead-link-solution/">Chris Coyier</a>. This new version tries its best to do away with using multiple methods (a DNS resolution check and <code>HEAD</code> requests to attempt to reducing the latency). This mostly does work, and on <a href="https://unrot.link/try/">the test page</a> I rarely see 2 seconds, and in some cases where there's a redirect, it can happen in under a second.</p>
<p>Also, importantly, the client script (that does the progressive enhancement) has built in redundancy to <a href="https://unrot.link/docs/down/">protect against</a> the unrot service going down: a &quot;ping&quot; is used and <em>only</em> if it succeeds does the script progressively enhance.</p>
<p>Finally, the service has an <a href="https://unrot.link/access/">allow list</a>. This means I have some way of contacting users of the service if there's any need. The process is relatively straightforward via a request on GitHub (as documented). If the access isn't in place, the unrot service will ignore the redirect check and forward you to the original requested URL (effectively a no-op).</p>
<p>Other than that, do check it out, and give me any feedback/PRs if you see room for improvement: <strong><a href="https://unrot.link">https://unrot.link</a></strong></p>
<p><em>Originally published on <a href="https://remysharp.com/2023/12/04/unrot-that-link">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
    <item>
      <title>Scraping Goodreads</title>
      <guid isPermaLink="false">scraping-goodreads</guid>
      <link>https://remysharp.com/2023/11/21/scraping-goodreads</link>
      <pubDate>Tue, 21 Nov 2023 00:00:00 +0000</pubDate>
      <description><![CDATA[I only use Goodreads because it's directly integrated into my Kindle (which I love) so I can easily track when I start and finish a book.
However, Goodreads itself is terrible for data, either losing the data, corrupting it or just not having it.
So I've fixed that.]]></description>
      <content:encoded><![CDATA[
<p>I only use Goodreads because it's directly integrated into my Kindle (<a href="https://remysharp.com/2018/05/18/my-extinguished-kindle">which I love</a>) so I can easily track when I start and finish a book.</p>
<p>However, Goodreads itself is terrible for data, either losing the data, corrupting it or just not having it.</p>
<p>So I've fixed that.</p>
<h2>Scraping</h2>
<p>Good old reliable web scraping. It worked 20 years ago and it still works today. In some cases I'm forced to use Puppeteer, but I try to avoid it if I can (to reduce the overhead) and in this case, it's not required.</p>
<p>I've written a dual platform script that will work either from the command line using Node, or it can be pasted directly into the browser's console. Both versions will spit out a full JSON object of the books I've read.</p>
<p>I strongly recommend running the script on a signed in page, and have a look at the <a href="https://github.com/remy/goodreads-scrape/">README for the project</a> for prerequisites for the Node version.</p>
<p>This is the typedef of the <code>Goodreads</code> object (which the <code>parseBooks</code> returns an array of):</p>
<pre><code class="language-js"><span class="token comment">/**
 * @typedef {Object} Goodreads
 * @property {string} title
 * @property {string} seriesEntry
 * @property {string|null} [seriesNumber]
 * @property {string|null} [series]
 * @property {string} author
 * @property {number} pages
 * @property {number|null} rating 1-5
 * @property {string[]} read timestamp of times the book was finished, or if missing "?"
 * @property {string[]} start timestamp of times the book was started, or if missing "?"
 * @property {string} published year the book was published
 * @property {string} goodreads
 * @property {number} goodreads_id
 * @property {string} cover url to cover scaled to 315px wide
 * @property {string|null} [review]
 * @property {boolean} spoiler whether the review contains spoilers
 * @property {string} slug the slugified title
 */</span>
</code></pre>
<p>There's a few things to note that might not be immediately obvious about the data:</p>
<ol>
<li>The <code>seriesNumber</code> is the number of the book in the series, e.g. <code>1</code> for the first book in the series. If the book is not part of a series, this is <code>null</code>, and <code>series</code> is an empty string. In addition, if the book is not part of a series, the <code>title</code> and <code>seriesEntry</code> are the same.</li>
<li>The <code>start</code> and <code>read</code> (the date the book was completed) are arrays. This allows for repeat readings. If there's no date, then the value is a <code>?</code> string.</li>
</ol>
<p>You can look and poke around my <a href="https://jqterm.com/d01be74a1faab95070836d8c306f0927?query=map%28.title%29">own book reviews on jqTerm</a>.</p>
<h2>Download</h2>
<p>The script is available on <strong><a href="https://github.com/remy/goodreads-scrape">github to download</a></strong>. Feel free to use, adjust and fit to your own needs.</p>
<h2>Some code notes</h2>
<p>I don't really know why I decided to make it work both in the browser <em>and</em> in Node - probably for the simplicity of use and experimentation.</p>
<p>To pull this off, I created a simplistic implementation of <a href="https://www.npmjs.com/package/cheerio">Cheerio</a> (which is used in the node version) to support the <code>map</code>, <code>find</code>, <code>text</code>, and a few other methods.</p>
<p>What was fun to find out was that Goodreads is using (I think) <a href="http://prototypejs.org/">Prototype</a> and the <code>Array.map</code> method (and probably others) had been overwritten. So I had to create a hidden iframe to reclaim original <code>Array.map</code> method.</p>
<p>Speaking of <code>map</code> - since Cheerio implements jQuery syntax, it also implements the (slightly) backward map argument signature of <code>(index, value)</code> (instead of <code>(value, index)</code>). So I had to port that over too (:shivers:).</p>
<p>There was also some fun finding out that Firefox and Chrome implement <em>slightly</em> different <code>Date</code> parsers (I'd assume because that particular API is so old there's legacy bits of cruft laying around).</p>
<p>Specifically Chrome can parse <code>new Date('18 Nov, 2023, Z')</code> but Firefox can't. And Firefox can parse <code>new Date('18 Nov, 2023, +0')</code> but Chrome can't. So if you <a href="https://github.com/remy/goodreads-scrape/blob/4e5f0ba1bd41a7a58ddc3bf7c927eabc89a7f1f1/goodreads-scrape.js#L125-L130">look at the code</a>, you'll see I try both!</p>
<p><em>Originally published on <a href="https://remysharp.com/2023/11/21/scraping-goodreads">Remy Sharp's b:log</a></em></p>]]></content:encoded>
    </item>
  </channel>
</rss>