Congratulations!

[Valid RSS] This is a valid RSS feed.

Recommendations

This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.

Source: http://akrabat.com/feed/

  1. <?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
  2. xmlns:content="http://purl.org/rss/1.0/modules/content/"
  3. xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  4. xmlns:dc="http://purl.org/dc/elements/1.1/"
  5. xmlns:atom="http://www.w3.org/2005/Atom"
  6. xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
  7. xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
  8. >
  9.  
  10. <channel>
  11. <title>Rob Allen</title>
  12. <atom:link href="https://akrabat.com/feed/" rel="self" type="application/rss+xml" />
  13. <link>https://akrabat.com</link>
  14. <description>Pragmatism in the real world</description>
  15. <lastBuildDate>Tue, 29 Apr 2025 10:34:09 +0000</lastBuildDate>
  16. <language>en-US</language>
  17. <sy:updatePeriod>
  18. hourly </sy:updatePeriod>
  19. <sy:updateFrequency>
  20. 1 </sy:updateFrequency>
  21. <generator>https://wordpress.org/?v=6.8</generator>
  22. <item>
  23. <title>Some thoughts on LLM usage in my work</title>
  24. <link>https://akrabat.com/some-thoughts-on-llm-usage-in-my-work/</link>
  25. <comments>https://akrabat.com/some-thoughts-on-llm-usage-in-my-work/#respond</comments>
  26. <dc:creator><![CDATA[Rob]]></dc:creator>
  27. <pubDate>Tue, 29 Apr 2025 10:00:00 +0000</pubDate>
  28. <category><![CDATA[AI]]></category>
  29. <guid isPermaLink="false">https://akrabat.com/?p=7339</guid>
  30.  
  31. <description><![CDATA[While it would be nice to put the genie back in the bottle, that hasn't happened often in human history, so for the foreseeable future, AI in the form of LLMs are here to stay. I imagine that what we use them for will change over time as we collectively internalise their limitations. Personally, I'm now using them for my work as much as I reasonably can. This is involving many different areas, such as… <a href="https://akrabat.com/some-thoughts-on-llm-usage-in-my-work/">continue reading</a>.]]></description>
  32. <content:encoded><![CDATA[<p>While it would be nice to put the genie back in the bottle, that hasn't happened often in human history, so for the foreseeable future, AI in the form of LLMs are here to stay. I imagine that what we use them for will change over time as we collectively internalise their limitations. </p>
  33. <p>Personally, I'm now using them for my work as much as I reasonably can. This is involving many different areas, such as asking questions that I would previously have just googled. While the LLM does make things up, the first 40 answers on Google can be decidedly less than helpful nowadays too. The LLM will read many more webpage results than I can be bothered to, provide a summary and cite its sources so that I can click a few to get a feel for how much I trust it.</p>
  34. <p>When developing, I'm using the LLM for multiple tasks. It can help me find bugs when I paste in snippets of code and say "this code is getting X wrong. Tell me why and propose a fix". I generally find that this level of focussed question works quite well, though sometimes, it will go off on a tangent and never come back.</p>
  35. <p>In an IDE, I've found LLM-based smart-autocomplete from something like Copilot works quite well. Again, it's a focussed small task and so tends to work reasonably well for me. I've also found that writing a comment as I would normally do helps the LLM get it right.</p>
  36. <p>New to me is Claude Code. This command line tool reads the files in the current directory and can operate on them. I've found that it is able to do bigger chunks of work as a result and I use <tt>git diff</tt> to assess its work and tweak as necessary before I personally commit. </p>
  37. <p>One thing that is obvious in hindsight, but if I don't have a clear idea in my mind of what I need, then the tools will do worse. This can be more clearly seen with the larger blocks of work, so now, if I'm exploring how and what I want to do, I will talk with the LLM more conversationally more like a brainstorming session to get to the point where I know what I want to do. Then I can instruct it to do the work. This works no differently from trying to delegate a job to a junior and sounds so obvious when written down. However, in the excitement of seeing the magic of the LLM doing the right thing, it's easy to forget and then be surprised when it goes so so wrong.</p>
  38. <p>Not every tool is useful for everyone or useful in every situation. There are also ethical and environmental considerations that affect how people view any given technology and I do not want to suggest that LLM tools must be used; they don't. </p>
  39. <p>I realise that I'm not using these tools are much as others, however, I'm finding them useful.</p>
  40. ]]></content:encoded>
  41. <wfw:commentRss>https://akrabat.com/some-thoughts-on-llm-usage-in-my-work/feed/</wfw:commentRss>
  42. <slash:comments>0</slash:comments>
  43. </item>
  44. <item>
  45. <title>Previewing OpenAPI specs using redocly&#039;s Docker container</title>
  46. <link>https://akrabat.com/previewing-openapi-specs-using-redoclys-docker-container/</link>
  47. <comments>https://akrabat.com/previewing-openapi-specs-using-redoclys-docker-container/#respond</comments>
  48. <dc:creator><![CDATA[Rob]]></dc:creator>
  49. <pubDate>Tue, 22 Apr 2025 10:00:00 +0000</pubDate>
  50. <category><![CDATA[API]]></category>
  51. <category><![CDATA[Docker]]></category>
  52. <category><![CDATA[OpenAPI]]></category>
  53. <guid isPermaLink="false">https://akrabat.com/?p=7325</guid>
  54.  
  55. <description><![CDATA[To provide consistency between the environments of our developers, I'm a strong proponent of using containers so that every developer is using the same versions of our tools. This is really important for command line tooling that depends on a separately installed language such as NodeJS or PHP as a simple npm i -g can install wildly different versions if a dev is running an older (or newer!) version of Node. For OpenAPI specs, listing… <a href="https://akrabat.com/previewing-openapi-specs-using-redoclys-docker-container/">continue reading</a>.]]></description>
  56. <content:encoded><![CDATA[<p>To provide consistency between the environments of our developers, I'm a strong proponent of using containers so that every developer is using the same versions of our tools. This is really important for command line tooling that depends on a separately installed language such as NodeJS or PHP as a simple <tt>npm i -g</tt> can install wildly different versions if a dev is running an older (or newer!) version of Node.</p>
  57. <p>For OpenAPI specs, listing is a must and I currently use <a href="https://stoplight.io/open-source/spectral">Spectral</a> for that, but it can be handy to have rendered documentation visible when writing specs. I don't know about you, but I find it easier to proofread text when in a different context to the editor that I'm currently writing it in. </p>
  58. <p>To render OpenAPI docs in real-time as I write, I picked <a href="https://redocly.com/docs/cli">Redocly</a> which has a <tt>preview-docs</tt> option.</p>
  59. <p>The local command is: <tt>redocly/cli preview-docs openapi.yaml</tt> which will render the OpenAPI spec in <tt>openapi.yaml</tt> as an HTML page at http://localhost:8080. You can then edit the spec file and it will hot reload the browser which is super-convenient.</p>
  60. <h2>Using preview-docs in the Docker container</h2>
  61. <p>To preview the docs using the <a href="https://hub.docker.com/r/redocly/cli">redocly/cli Docker image</a>, you need this command:</p>
  62. <pre>
  63. docker run --init --rm \
  64. -p 8080:8080 -p 32201:32201 \
  65. -v $(PWD):/spec \
  66. redocly/cli preview-docs --use-community-edition openapi.yaml --host 0.0.0.0
  67. </pre>
  68. <p>There's quite a lot going on here, so let's look at the key options:</p>
  69. <ul>
  70. <li><tt>--init</tt> is required so that ctrl+c will exit cleanly.</li>
  71. <li><tt>--rm</tt> deletes the container on exit, so we tidy up after ourselves.</li>
  72. <li><tt>-p 8080:8080 -p 32201:32201</tt> maps ports 8080 and 32201 from the container to our local computer. We need 32201 so that hot reloading works.</li>
  73. <li><tt>-v $(PWD):/spec</tt> maps our current directory to the container's <tt>/spec</tt> directory which is its working directory.</li>
  74. <li><tt>redocly/cli preview-docs openapi.yaml</tt> runs Redocly's <tt>preview-docs</tt> command against our <tt>openapi.yaml</tt> OpenAPI spec file.</li>
  75. <li><tt>--host 0.0.0.0</tt> to make the app accessible from all network interfaces in the container so that we can view the preview from our local computer.</li>
  76. </ul>
  77. <p>We can now open <a href="http://localhost:8080/">http://localhost:8080/</a> and view our nicely rendered OpenAPI spec!</p>
  78. <p><img fetchpriority="high" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/04/2025redocli-cli-rps-openapi.png" alt="Redocli cli rps openapi." title="redocli-cli-rps-openapi.png" border="0" width="700" height="462" /></p>
  79. ]]></content:encoded>
  80. <wfw:commentRss>https://akrabat.com/previewing-openapi-specs-using-redoclys-docker-container/feed/</wfw:commentRss>
  81. <slash:comments>0</slash:comments>
  82. </item>
  83. <item>
  84. <title>Using the Stripe CLI with with Docker Compose</title>
  85. <link>https://akrabat.com/using-the-stripe-cli-with-with-docker-compose/</link>
  86. <comments>https://akrabat.com/using-the-stripe-cli-with-with-docker-compose/#respond</comments>
  87. <dc:creator><![CDATA[Rob]]></dc:creator>
  88. <pubDate>Tue, 15 Apr 2025 10:00:00 +0000</pubDate>
  89. <category><![CDATA[Development]]></category>
  90. <category><![CDATA[Docker]]></category>
  91. <guid isPermaLink="false">https://akrabat.com/?p=7317</guid>
  92.  
  93. <description><![CDATA[One of the projects that I'm working on at the moment uses Stripe which means that we need to handle web hooks in order to ascertain what's happened as that's how Stripe communicates back to us. For production and staging, it's easy enough to register a url on Stripe's dashboard, but when developing, it's a little more complicated as our computers are generally isolated from receiving incoming web requests from the public internet, along with… <a href="https://akrabat.com/using-the-stripe-cli-with-with-docker-compose/">continue reading</a>.]]></description>
  94. <content:encoded><![CDATA[<p>One of the projects that I'm working on at the moment uses Stripe which means that we need to handle web hooks in order to ascertain what's happened as that's how Stripe communicates back to us.</p>
  95. <p>For production and staging, it's easy enough to register a url on Stripe's dashboard, but when developing, it's a little more complicated as our computers are generally isolated from receiving incoming web requests from the public internet, along with a changing IP address if we work remotely.</p>
  96. <p>The easiest way to handle this is to use <a href="https://docs.stripe.com/stripe-cli">Stripe's CLI</a> which is excellent. The instructions show you how to run it locally and you can then use `stripe listen` to receive web hooks locally.</p>
  97. <h2>Receiving webhooks using the Stripe CLI docker container</h2>
  98. <p>I'm not a fan of locally installing project tools as that makes it harder to keep the tooling consistent between team members and onboarding is that much harder too, so I turned to Docker as Stripe provides a <a href="https://docs.stripe.com/cli/docker">CLI Docker image</a>.</p>
  99. <p>We use <a href="https://docs.docker.com/compose/">Compose</a>, so I updated our <tt>compose.yaml</tt> file:</p>
  100. <pre lang="yaml">
  101.  stripe-cli:
  102.    image: stripe/stripe-cli:latest
  103.    env_file:
  104.      - .env
  105.      - .env.local
  106.    command: |
  107.      listen --api-key $$STRIPE_API_KEY
  108.      --device-name $$STRIPE_DEVICE_NAME
  109.      --skip-verify
  110.      --forward-to https://store.example.com/stripe
  111. </pre>
  112. <p>This is a pretty standard Compose configuration. The important thing is the options to the <tt>listen</tt> command. </p>
  113. <ul>
  114. <li><tt>--api-key $$STRIPE_API_KEY</tt>: Your API key is in the Stripe Dashboard and needs to be in <tt>.env.local</tt> as it's not something that belongs in git</li>
  115. <li><tt>--device-name $$STRIPE_DEVICE_NAME</tt>: Invent an device name for each installation. I use <tt>STRIPE_DEVICE_NAME=${USER}_dev</tt> in our <tt>.env</tt> file</li>
  116. <li><tt>--skip-verify</tt>: While we are using <tt>https</tt> for our development endpoints, it's self-signed, so we skip verification</li>
  117. <li><tt>--forward-to https://store.example.com/stripe</tt>: The endpoint to forward webhooks to</li>
  118. </ul>
  119. <p>The Stripe CLI does all the communication work with Stripe, and webhooks are now received by our development app.</p>
  120. <h2>Running the Docker CLI from the command line</h2>
  121. <p>As the CLI binary in the container already has our API key, we want to use use it in development at our local command line. We can do this using <tt>docker compose run --rm stripe-cli {arguments here}</tt>.</p>
  122. <p>I created <tt>bin/stripe</tt> to make it easier:</p>
  123. <pre lang="bash">
  124. #!/usr/bin/env bash
  125.  
  126. # Run the Docker Stripe CLI using the container as that already has our API key
  127. docker compose run --rm stripe-cli "$@"
  128. </pre>
  129. <p>Now, I can can use <tt>bin/stripe {arguments here}</tt></p>
  130. <p>All in all, I've found this an easy way to receive Stripe webhooks while developing and enjoy the benefit that isolation in containers brings.</p>
  131. ]]></content:encoded>
  132. <wfw:commentRss>https://akrabat.com/using-the-stripe-cli-with-with-docker-compose/feed/</wfw:commentRss>
  133. <slash:comments>0</slash:comments>
  134. </item>
  135. <item>
  136. <title>No SSL padlock on Safari on macOS 18.4</title>
  137. <link>https://akrabat.com/no-ssl-padlock-on-safari-on-macos-18-4/</link>
  138. <comments>https://akrabat.com/no-ssl-padlock-on-safari-on-macos-18-4/#comments</comments>
  139. <dc:creator><![CDATA[Rob]]></dc:creator>
  140. <pubDate>Tue, 08 Apr 2025 10:00:00 +0000</pubDate>
  141. <category><![CDATA[Computing]]></category>
  142. <category><![CDATA[Mac]]></category>
  143. <guid isPermaLink="false">https://akrabat.com/?p=7313</guid>
  144.  
  145. <description><![CDATA[With the release of macOS 18.4, Safari no longer shows a padlock when you visit a website that uses https. I assume there's a reason for this but I was scratching my head trying to work out how to view the SSL certificate when I went to a new-to-me website domain that my bank asked me to go to. This is what the address bar now looks like when you're on a secure connection to… <a href="https://akrabat.com/no-ssl-padlock-on-safari-on-macos-18-4/">continue reading</a>.]]></description>
  146. <content:encoded><![CDATA[<p>With the release of macOS 18.4, Safari no longer shows a padlock when you visit a website that uses <tt>https</tt>. I assume there's a reason for this but I was scratching my head trying to work out how to view the SSL certificate when I went to a new-to-me website domain that my bank asked me to go to.</p>
  147. <p>This is what the address bar now looks like when you're on a secure connection to a website such as <a href="https://www.google.com/">Google</a>:</p>
  148. <p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/04/2025safari-ssl.png" alt="Safari address bar when on a secure website" title="safari-ssl.png" border="0" width="600" height="35" /></p>
  149. <p>This is what it looks like if you're on an insecure connection to a website such as <a href="http://neverssl.com/online/">NeverSSL</a>:</p>
  150. <p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/04/2025safari-no-ssl.png" alt="Safari address bar when on an insecure website" title="safari-no-ssl.png" border="0" width="600" height="39" /></p>
  151. <p>It seems that they've reversed the logic and now display "Not Secure" for an <tt>http</tt> connection and leave unadorned for <tt>https</tt>. Weird.</p>
  152. <h2>Viewing the certificate</h2>
  153. <p>If you need to view the certificate for a site served over <tt>https</tt>, then it's in Safari menu's "Connection Security Details&#8230;" which will display this dialog:</p>
  154. <p><img loading="lazy" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/04/2025safari-security-connection-details.png" alt="Safari&#39;s security connection details dialog" title="safari-security-connection-details.png" border="0" width="500" height="150" /></p>
  155. <p>This menu item is disabled for websites that connect over <tt>http</tt>.</p>
  156. <h3>It is the same on iPhone &#038; iPad</h3>
  157. <p>iOS and iPadOS work the same way too. The "Connection Security Details" menu item is in the Page Menu, which is accessed by pressing the "&#8230;" button next to the font size buttons when you tap the extensions icon in the URL bar.</p>
  158. <p>Worth being aware of.</p>
  159. ]]></content:encoded>
  160. <wfw:commentRss>https://akrabat.com/no-ssl-padlock-on-safari-on-macos-18-4/feed/</wfw:commentRss>
  161. <slash:comments>1</slash:comments>
  162. </item>
  163. <item>
  164. <title>Generating scripts with LLMs</title>
  165. <link>https://akrabat.com/generating-scripts-with-llms/</link>
  166. <comments>https://akrabat.com/generating-scripts-with-llms/#comments</comments>
  167. <dc:creator><![CDATA[Rob]]></dc:creator>
  168. <pubDate>Wed, 02 Apr 2025 10:00:00 +0000</pubDate>
  169. <category><![CDATA[AI]]></category>
  170. <guid isPermaLink="false">https://akrabat.com/?p=7300</guid>
  171.  
  172. <description><![CDATA[With the recent release of the new OpenAI ChatGPT that can do image generation, I thought that I might add a feature image to my last blog post. I've had mixed experiences of AI generation of images, probably because I'm not good at prompting them. I started with this: We now have an image, but it doesn't have a credit. Now, I could go back to the LLM and ask it again, but these things… <a href="https://akrabat.com/generating-scripts-with-llms/">continue reading</a>.]]></description>
  173. <content:encoded><![CDATA[<p>With the recent release of the new OpenAI ChatGPT that can do image generation, I thought that I might add a feature image to my last  blog post. I've had mixed experiences of AI generation of images, probably because I'm not good at prompting them.</p>
  174. <p>I started with this:</p>
  175. <p><img loading="lazy" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/03/20252025-03-chatgpt-image-genration-screenshot.png" alt="Screenshot of ChatGPT interface showing a generated colored pencil drawing of a MacBook displaying the Signal app logo, with an external hard drive nearby. The image has the title &#39;Backing up Signal messages on Mac&#39; and was created in response to a prompt requesting hero image for an article about backing up Signal messages on Mac." title="2025-03-chatgpt-image-genration-screenshot.png" border="0" width="500" height="519" /></p>
  176. <p>We now have an image, but it doesn't have a credit. Now, I could go back to the LLM and ask it again, but these things are slow and I won't get the same image anyway. So I decided to write a python script to add an image credit to the bottom right of the image. </p>
  177. <p>Clearly this is a job for an LLM, so lets use <a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview">Claude Code</a>.</p>
  178. <p>I started it up and gave it this prompt:</p>
  179. <pre>
  180. &gt; Create a single file python script called add-image-credit that has executable permissions. This file starts with the line
  181.  `#!/usr/bin/env -S uv run
  182.    --script --quiet` and uses the `# /// script` system to bring in any dependencies.
  183.  
  184.  This script will take an image filename and a credit text string. Display the credit text in white with a drop shadow in
  185.  the bottom right hand corner of the image and save the image with the same name with the word &quot;credited&quot; appended. The text
  186.   font size should be no more than 18, but small enough such that the string is no more than 30% of the width of the image.
  187.  Use Helvetica as the font, but make this a variable so that it can be easily changed. If the credit text is not supplied,
  188.  then use the text &quot;Image generated by AI&quot; by default.
  189. </pre>
  190. <p>Claude does it stuff:</p>
  191. <pre>
  192. &#x23fa; I'll create a Python script that adds credit text to images as specified. Let me write the script with executable
  193.  permissions.
  194.  
  195. &#x23fa; Write(file_path: add-image-credit)&hellip;
  196.  ⎿ Wrote 80 lines to add-image-credit
  197.     #!/usr/bin/env -S uv run --script --quiet
  198.     # /// script
  199.     # /// dep pillow
  200.     import argparse
  201.     import os
  202.     from PIL import Image, ImageDraw, ImageFont
  203.     def add_credit(image_path, credit_text=None, font_name=&quot;Helvetica&quot;):
  204.         if credit_text is None:
  205.     &hellip; +70 lines (ctrl+r to see all)
  206.  
  207. &#x23fa; Bash(chmod +x /Users/rob/Projects/image-credit/add-image-credit)&hellip;
  208. &nbsp;&nbsp;⎿ &nbsp;(No content)
  209.  
  210. &#x23fa; The script has been created with executable permissions. It:
  211.  - Uses PIL to add credit text to images
  212.  - Automatically scales font size (max 18pt) to ensure text width is &le;30% of image width
  213.  - Places white text with black drop shadow in bottom right corner
  214.  - Saves output as original_filename_credited.ext
  215.  
  216.  Run it with: ./add-image-credit image.jpg --credit &quot;Your credit text&quot;
  217. </pre>
  218. <p>Interestingly, it's got the format for <a href="https://akrabat.com/defining-python-dependencies-at-the-top-of-the-file/">Inline script metadata</a> incorrect, but I didn't notice and asked it to run it.</p>
  219. <pre>
  220. &gt; Run this script against backing-up-signal-feature-image.png that is in this directory
  221.  
  222. &#x23fa; I'll run the script against the PNG file. Let me first check if the file exists and then run the script.
  223.  
  224. &#x23fa; List(path: &quot;&quot;)&hellip;
  225. &nbsp;&nbsp;⎿ &nbsp;- /Users/rob/Projects/image-credit/
  226.       - add-image-credit
  227.       - backing-up-signal-feature-image.png
  228.     &hellip; +1 item (ctrl+r to see all)
  229.  
  230. &#x23fa; Bash(./add-image-credit /Users/rob/Projects/image-credit/backing-up-signal-feature-image.png)&hellip;
  231. &nbsp;&nbsp;⎿ &nbsp;error: An opening tag (`# /// script`) was found without a closing tag (`# ///`). Ensure that every line between the
  232.     opening and closing tags (including empty lines) starts with a leading `#`.
  233. </pre>
  234. <p>The script has failed due to the error related to the Inline script metadata, Claude detects and fixes it:</p>
  235. <pre>
  236. &#x23fa; I need to fix the script format. The script tag is missing the closing marker.
  237.  
  238. &#x23fa; Write(file_path: add-image-credit)&hellip;
  239.  ⎿  Updated add-image-credit with 1 addition
  240.     1  #!/usr/bin/env -S uv run --script --quiet
  241.     2  # /// script
  242.     3  # /// dep pillow
  243.     4  # ///
  244.     5
  245.     6  import argparse
  246.     7  import os
  247.  
  248. &#x23fa; Bash(./add-image-credit /Users/rob/Projects/image-credit/backing-up-signal-feature-image.png)&hellip;
  249. &nbsp;&nbsp;⎿ &nbsp;error: TOML parse error at line 1, column 1
  250.       |
  251.     1 | /// dep pillow
  252.       | ^
  253.     invalid key
  254.  
  255. &#x23fa; Let me fix the script format again. There seems to be an issue with the uv script dependency format.
  256.  
  257. &#x23fa; Write(file_path: add-image-credit)&hellip;
  258.  ⎿  Updated add-image-credit with 1 addition and 1 removal
  259.     1  #!/usr/bin/env -S uv run --script --quiet
  260.     2  # /// script
  261.     3  # /// dep pillow
  262.     3  # dependencies = [&quot;pillow&quot;]
  263.     4  # ///
  264.     5
  265.     6  import argparse
  266. </pre>
  267. <p>Now it has got it right, but it also got something wrong with colours, so had to fix them too. I've skipped that iteration here as it's the same loop as we've just done. </p>
  268. <pre>
  269. &#x23fa; Bash(./add-image-credit /Users/rob/Projects/image-credit/backing-up-signal-feature-image.png)&hellip;
  270. &nbsp;&nbsp;⎿ &nbsp;Credited image saved as: /Users/rob/Projects/image-credit/backing-up-signal-feature-image_credited.png
  271.  
  272. &#x23fa; Great! The script now successfully adds the default credit text &quot;Image generated by AI&quot; to the image.
  273. </pre>
  274. <p>It was successful and I quit Claude:</p>
  275. <pre>
  276. &gt; exit
  277.  
  278.  
  279.  
  280. Total cost:            $0.2396
  281. Total duration (API):  1m 40.8s
  282. Total duration (wall): 5m 36.3s
  283. Total code changes:    99 lines added, 7 lines removed
  284. </pre>
  285. <p>Not bad for 24 cents and I now have a new script that I've moved to <code>~/bin</code>!</p>
  286. <p>This is the final image:</p>
  287. <p><img loading="lazy" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/03/2025backing-up-signal-feature-image_credited-1.png" alt="Illustration of a MacBook laptop displaying the Signal messaging app logo on screen, with an external hard drive connected. Text reads &#39;Backing up Signal messages on Mac&#39; against a light beige background. The image is drawn in a sketch style with colored pencils. It has the words &quot;Image generated by AI&quot; in the bottom right hand corner." title="backing-up-signal-feature-image_credited.png" border="0" width="600" height="400" /></p>
  288. <p>For one-off things like this, it's more than good enough. Letting it loose on a bigger codebase will cost a lot more as the amount of code sent to Claude and processed by it will significantly affect the price. Also, any code created needs testing quite thoroughly and if you're going to put it into production, it need to be looked at and assessed carefully.</p>
  289. ]]></content:encoded>
  290. <wfw:commentRss>https://akrabat.com/generating-scripts-with-llms/feed/</wfw:commentRss>
  291. <slash:comments>1</slash:comments>
  292. </item>
  293. <item>
  294. <title>Backing up Signal messages on Mac</title>
  295. <link>https://akrabat.com/backing-up-signal-messages-on-mac/</link>
  296. <comments>https://akrabat.com/backing-up-signal-messages-on-mac/#respond</comments>
  297. <dc:creator><![CDATA[Rob]]></dc:creator>
  298. <pubDate>Tue, 25 Mar 2025 11:00:00 +0000</pubDate>
  299. <category><![CDATA[Command Line]]></category>
  300. <category><![CDATA[Computing]]></category>
  301. <guid isPermaLink="false">https://akrabat.com/?p=7295</guid>
  302.  
  303. <description><![CDATA[I'm using Signal more now and as it's fully end-to-end encrypted, if something goes wrong with your phone or you lose it, you will lose your entire message history. Signal on Android has an official backup method, but there isn't one for iPhone or desktop. As a result, a number of backup tools have been written by various people. The one I've had most luck with on my Mac desktop is signalbackup-tools. Note that the… <a href="https://akrabat.com/backing-up-signal-messages-on-mac/">continue reading</a>.]]></description>
  304. <content:encoded><![CDATA[<p>I'm using <a href="https://signal.org">Signal</a> more now and as it's fully end-to-end encrypted, if something goes wrong with your phone or you lose it, you will lose your entire message history. Signal on Android has an <a href="https://support.signal.org/hc/en-us/articles/360007059752-Backup-and-Restore-Messages">official backup method</a>, but there isn't one for iPhone or desktop. As a result, a number of backup tools have been written by various people.</p>
  305. <p>The one I've had most luck with on my Mac desktop is <a href="https://github.com/bepaald/signalbackup-tools">signalbackup-tools</a>. Note that the Mac Desktop only has data from when you installed it. If you install today, the you will only get up to the last 45 days of messages transferred over from your iPhone.</p>
  306. <p>This works really well and rather helpfully reads the macOS keychain directly for the Signal encryption key, which means that you don't have it around in your shell's history or environment.</p>
  307. <h2>SQLite database backup</h2>
  308. <p>The easiest place to start is to export the database as <a href="https://www.sqlite.org">SQLite</a>:</p>
  309. <pre>
  310. signalbackup-tools --dumpdesktopdb signal.sqlite
  311. </pre>
  312. <p>You now have a SQLite database which you can explore at your leisure and is easy to script against.</p>
  313. <h2>HTML or text export</h2>
  314. <p>Alternatively, you can export as a set of HTML files:</p>
  315. <pre>
  316. signalbackup-tools --exportdesktophtml signal-html
  317. </pre>
  318. <p>This will create a <tt>signal-html</tt> directory that contains a separate folder per chat where the HTML file is nicely styled in a reasonable approximation of the Signal interface.</p>
  319. <p>In addition to HTML, you can export as text using:</p>
  320. <pre>
  321. signalbackup-tools --exportdesktoptxt signal-txt
  322. </pre>
  323. <p>This will create a <tt>signal-txt</tt> directory that contains a plain text file per chat.</p>
  324. <h3>Limit by date</h3>
  325. <p>In addition, you can use <tt>--limittodates &lt;date list&gt;</tt> to export messages in a specific date range. For instance, to export just February's data, you could do:</p>
  326. <pre>
  327. signalbackup-tools --limittodates "2025-02-01 00:00:00","2025-03-01 00:00:00" --exportdesktophtml .
  328. </pre>
  329. <h2>Final thoughts</h2>
  330. <p>I don't often have a need to go back to message data, but I have looked up things in family conversations before now. This is also a useful reminder that end-to-end encryption doesn't prevent the person that you're communicating with keeping a copy of whatever you say.</p>
  331. ]]></content:encoded>
  332. <wfw:commentRss>https://akrabat.com/backing-up-signal-messages-on-mac/feed/</wfw:commentRss>
  333. <slash:comments>0</slash:comments>
  334. </item>
  335. <item>
  336. <title>Getting the screen&#039;s size in a macOS screensaver</title>
  337. <link>https://akrabat.com/getting-the-screens-size-in-a-macos-screensaver/</link>
  338. <comments>https://akrabat.com/getting-the-screens-size-in-a-macos-screensaver/#respond</comments>
  339. <dc:creator><![CDATA[Rob]]></dc:creator>
  340. <pubDate>Tue, 18 Mar 2025 11:00:00 +0000</pubDate>
  341. <category><![CDATA[Development]]></category>
  342. <guid isPermaLink="false">https://akrabat.com/?p=7285</guid>
  343.  
  344. <description><![CDATA[As a hobby project, I've been writing a screensaver for my Mac that displays my photos with a caption and date taken. To get the width and height of the screen so that I could size the image correctly, I used this code in my ScreenSaverView: self.screenWidth = frame.size.width self.screenHeight = frame.size.height However, I discovered that with two or more screens, sometimes the frame size returned was wrong and so the photo was either too… <a href="https://akrabat.com/getting-the-screens-size-in-a-macos-screensaver/">continue reading</a>.]]></description>
  345. <content:encoded><![CDATA[<p>As a hobby project, I've been writing a screensaver for my Mac that displays my photos with a caption and date taken. To get the width and height of the screen so that I could size the image correctly, I used this code in my <tt>ScreenSaverView</tt>:</p>
  346. <pre lang="swift">
  347. self.screenWidth = frame.size.width
  348. self.screenHeight = frame.size.height
  349. </pre>
  350. <p>However, I discovered that with two or more screens, <em>sometimes</em> the frame size returned was wrong and so the photo was either too small in the screen, showing a black area, or larger than the screen.</p>
  351. <p>To make it more reliable, I've change the code to use <tt>self.bounds</tt> instead:</p>
  352. <pre>
  353. self.screenWidth = self.bounds.width
  354. self.screenHeight = self.bounds.height
  355. </pre>
  356. <p>Reading around, this is what I should have used in the first place!</p>
  357. ]]></content:encoded>
  358. <wfw:commentRss>https://akrabat.com/getting-the-screens-size-in-a-macos-screensaver/feed/</wfw:commentRss>
  359. <slash:comments>0</slash:comments>
  360. </item>
  361. <item>
  362. <title>Privileges app: sudo for macOS</title>
  363. <link>https://akrabat.com/privileges-app-sudo-for-macos/</link>
  364. <comments>https://akrabat.com/privileges-app-sudo-for-macos/#respond</comments>
  365. <dc:creator><![CDATA[Rob]]></dc:creator>
  366. <pubDate>Tue, 11 Mar 2025 11:00:00 +0000</pubDate>
  367. <category><![CDATA[Computing]]></category>
  368. <category><![CDATA[Mac]]></category>
  369. <guid isPermaLink="false">https://akrabat.com/?p=7279</guid>
  370.  
  371. <description><![CDATA[By default, the first user that you create on macOS is an administrator and has more power over the system than a normal user account. The reason it does this is obvious as you need this power to create other users, to install software and so on. However it also means that the computer is slightly more vulnerable to attack via a vulnerability in the software I use day to day. To combat this, I… <a href="https://akrabat.com/privileges-app-sudo-for-macos/">continue reading</a>.]]></description>
  372. <content:encoded><![CDATA[<p>By default, the first user that you create on macOS is an administrator and has more power over the system than a normal user account. The reason it does this is obvious as you need this power to create other users, to install software and so on. However it also means that the computer is slightly more vulnerable to attack via a vulnerability in the software I use day to day.</p>
  373. <p>To combat this, I run with my user account as a normal user and have a separate admin account for privilege escalation. This is remarkably easy on macOS as whenever the system needs to do something that needs more rights than my user, it pops up a dialog asking for the credentials of an administrative user.</p>
  374. <p>When I have a lot of work to do that requires an administrative user, typing in the admin user's creds for every action is tedious. What I need is something like <tt>sudo</tt>, but for my Mac, so I use the <a href="https://github.com/SAP/macOS-enterprise-privileges?tab=readme-ov-file#privileges">Privileges</a> app from SAP for this. This app provides a way to request admin rights for my current user account so that I can then do all the admin tasks without having to keep entering credentials.</p>
  375. <p><img loading="lazy" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/02/2025privileges1.png" alt="" title="privileges1.png" border="0" width="300" height="353" /></p>
  376. <p>Once I request privileges, my account has admin rights until they either expire, I remove them or a I reboot. The time limit is configured in the Settings and I have it set to 30 minutes as it's rare that I sill need admin rights after that long and I never remember to turn them off. </p>
  377. <p>It's nice and easy, and my Mac is that much more secure. I highly recommend that you try it.</p>
  378. ]]></content:encoded>
  379. <wfw:commentRss>https://akrabat.com/privileges-app-sudo-for-macos/feed/</wfw:commentRss>
  380. <slash:comments>0</slash:comments>
  381. </item>
  382. <item>
  383. <title>Cloudflare SSL/TLS setting for an encrypted connection to upstream</title>
  384. <link>https://akrabat.com/cloudflare-ssl-tls-setting-for-an-encrypted-connection-to-upstream/</link>
  385. <comments>https://akrabat.com/cloudflare-ssl-tls-setting-for-an-encrypted-connection-to-upstream/#respond</comments>
  386. <dc:creator><![CDATA[Rob]]></dc:creator>
  387. <pubDate>Tue, 04 Mar 2025 11:00:00 +0000</pubDate>
  388. <category><![CDATA[Computing]]></category>
  389. <guid isPermaLink="false">https://akrabat.com/?p=7266</guid>
  390.  
  391. <description><![CDATA[I was helping a friend set up a new website and hit a problem where the website was in an infinite redirect loop: I could see this in curl quite easily: $ curl -I https://myfriendswebsite.example.com/ HTTP/1.1 301 Moved Permanently Location: https://myfriendswebsite.example.com/ To debug, we turned off Cloudflare by setting the Proxy status on the DNS record in Cloudflare's admin to "DNS only" and the problem went away. Further investigation led to the SSL/TLS setting in… <a href="https://akrabat.com/cloudflare-ssl-tls-setting-for-an-encrypted-connection-to-upstream/">continue reading</a>.]]></description>
  392. <content:encoded><![CDATA[<p>I was helping a friend set up a new website and hit a problem where the website was in an infinite redirect loop:</p>
  393. <p>I could see this in <tt>curl</tt> quite easily:</p>
  394. <pre>
  395. $ curl -I https://myfriendswebsite.example.com/
  396. HTTP/1.1 301 Moved Permanently
  397. Location: https://myfriendswebsite.example.com/
  398. </pre>
  399. <p>To debug, we turned off Cloudflare by setting the Proxy status on the DNS record in Cloudflare's admin to "DNS only" and the problem went away.</p>
  400. <p>Further investigation led to the SSL/TLS setting in Cloudflare. When my friend had set this up, they had chosen the <a href="https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/flexible/"><em>Flexible</em></a> encryption mode, possibly as this sounded like the right choice. (Though I wonder why they didn't leave it on automatic?)</p>
  401. <p>With <em>Flexible</em> encryption mode, Cloudflare accepts an SSL connection and then uses an non-SSL connection to our server where the website is hosted (<tt>http://myfriendswebsite.example.com/</tt>). Our server has an SSL certificate set-up and an nginx rule that noted that the request was not encrypted and so returned a 301 redirect to the secure url (<tt>https://myfriendswebsite.example.com/</tt>). The browser redirected back to the SSL URL which went to Cloudflare, which made a non-SSL connection to the web server and we enter the loop.</p>
  402. <p>The fix is simple: Set the Cloudflare encryption mode to <a href="https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/full-strict/"><em>Full (Strict)</em></a> if you have your own SSL certificate on the upstream server. </p>
  403. <p>Alternatively, don't mess with this setting, leave it on <em>Automatic</em> and let Cloudflare sort it out, though this runs the risk that you'll have a non-SSL connection between Cloudflare and your web server if you've misconfigured your web server. Personally, I prefer <em>Full (Strict)</em>, so that there's a failure if my web server is no longer serving encrypted traffic to Cloudflare with a valid certificate.</p>
  404. ]]></content:encoded>
  405. <wfw:commentRss>https://akrabat.com/cloudflare-ssl-tls-setting-for-an-encrypted-connection-to-upstream/feed/</wfw:commentRss>
  406. <slash:comments>0</slash:comments>
  407. </item>
  408. <item>
  409. <title>Converting a PHPUnit TestListener to an Event Subscriber</title>
  410. <link>https://akrabat.com/converting-a-phpunit-testlistener-to-an-event-subscriber/</link>
  411. <comments>https://akrabat.com/converting-a-phpunit-testlistener-to-an-event-subscriber/#respond</comments>
  412. <dc:creator><![CDATA[Rob]]></dc:creator>
  413. <pubDate>Tue, 25 Feb 2025 11:00:00 +0000</pubDate>
  414. <category><![CDATA[PHP]]></category>
  415. <guid isPermaLink="false">https://akrabat.com/?p=7276</guid>
  416.  
  417. <description><![CDATA[One of the bigger changes in PHPUnit 10 was the introduction of the new extension system which replaced listeners and hooks. The old way On one of my projects we have a TestListener that sets up the database before we run some functional tests against it. It looks like this: &#60;?php declare(strict_types=1); namespace App\Test\Listeners; use App\Test\Functional\Helpers\DbHelper; use PHPUnit\Framework\TestListenerDefaultImplementation; use PHPUnit\Framework\TestSuite; class TestListener implements \PHPUnit\Framework\TestListener { use TestListenerDefaultImplementation; public function startTestSuite(TestSuite $suite): void { if (str_contains($suite-&#62;getName(),… <a href="https://akrabat.com/converting-a-phpunit-testlistener-to-an-event-subscriber/">continue reading</a>.]]></description>
  418. <content:encoded><![CDATA[<p>One of the bigger changes in PHPUnit 10 was the introduction of the <a href="https://docs.phpunit.de/en/10.5/extending-phpunit.html#extending-the-test-runner">new extension system</a> which replaced listeners and hooks. </p>
  419. <h2>The old way</h2>
  420. <p>On one of my projects we have a <tt>TestListener</tt> that sets up the database before we run some functional tests against it.</p>
  421. <p>It looks like this:</p>
  422. <pre>
  423. &lt;?php
  424.  
  425. declare(strict_types=1);
  426.  
  427. namespace App\Test\Listeners;
  428.  
  429. use App\Test\Functional\Helpers\DbHelper;
  430. use PHPUnit\Framework\TestListenerDefaultImplementation;
  431. use PHPUnit\Framework\TestSuite;
  432.  
  433. class TestListener implements \PHPUnit\Framework\TestListener
  434. {
  435.    use TestListenerDefaultImplementation;
  436.  
  437.    public function startTestSuite(TestSuite $suite): void
  438.    {
  439.        if (str_contains($suite-&gt;getName(), 'functional')) {
  440.            DbHelper::setup();
  441.        }
  442.    }
  443. }
  444. </pre>
  445. <p>As the PHPUnit <tt>TestListener</tt> interface defines a lot of methods, you can use the <tt>TestListenerDefaultImplementation</tt> trait to stub them all out and only write the ones you care about. In our case, we care about <tt>startTestSuite()</tt> as that's run when a test suite is started (the clue is in the name!). Our implementation is trivial: if the name of the suite is <tt>functional</tt>, then call <tt>DbHelper::setup()</tt>.</p>
  446. <p>It's registered in <tt>phpunit.xml</tt> like this:</p>
  447. <pre>
  448.    &lt;listeners&gt;
  449.        &lt;listener class=&quot;App\Test\Listeners\TestListener&quot;&gt;&lt;/listener&gt;
  450.    &lt;/listeners&gt;
  451. </pre>
  452. <h2>The new way</h2>
  453. <p><tt>TestListener</tt> was removed in PHPUnit 10 and so we had to replace this with a new extension. You need an <tt>Extension</tt> class to that is registered with PHPUnit and in turn registers as many <tt>Subscriber</tt> classes you need as a subscriber can only lister to one event.</p>
  454. <h3>The Extension</h3>
  455. <p>I called our extension <tt>SetupDatabaseBeforeFunctionalTestsExtension</tt>. It looks like this:</p>
  456. <pre>
  457. &lt;?php
  458.  
  459. declare(strict_types=1);
  460.  
  461. namespace App\Test\Extension;
  462.  
  463. use App\Test\Helper\DbHelper;
  464. use PHPUnit\Runner\Extension\Extension;
  465. use PHPUnit\Runner\Extension\Facade;
  466. use PHPUnit\Runner\Extension\ParameterCollection;
  467. use PHPUnit\TextUI\Configuration\Configuration;
  468.  
  469. final class SetupDatabaseBeforeFunctionalTestsExtension implements Extension
  470. {
  471.    public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
  472.    {
  473.        $facade-&gt;registerSubscribers(
  474.            new SetupDatabaseBeforeFunctionalTests()
  475.        );
  476.    }
  477. }
  478. </pre>
  479. <p>This class implements the <tt>Extension</tt> interface which means that we need to implement the <tt>bootstrap()</tt> method. You can set up things in here and then register your subscribers. In our case, we only need to register the <tt>SetupDatabaseBeforeFunctionalTests</tt> subscriber.</p>
  480. <p>To register an extension with PHPUnit, you add it to <tt>phpunit.xml</tt>, like this:</p>
  481. <pre>
  482.    &lt;extensions&gt;
  483.        &lt;bootstrap class=&quot;App\Test\Extension\SetupDatabaseBeforeFunctionalTestsExtension&quot;/&gt;
  484.    &lt;/extensions&gt;
  485. </pre>
  486. <p>The implementation of <tt>SetupDatabaseBeforeFunctionalTests</tt> is relatively simple:</p>
  487. <pre>
  488. &lt;?php
  489.  
  490. declare(strict_types=1);
  491.  
  492. namespace App\Test\Extension;
  493.  
  494. use App\Test\Helper\DbHelper;
  495. use PHPUnit\Event;
  496. use PHPUnit\Event\TestSuite\Started;
  497.  
  498. final class SetupDatabaseBeforeFunctionalTests implements Event\TestSuite\StartedSubscriber
  499. {
  500.    public function notify(Started $event): void
  501.    {
  502.        if (str_contains($event-&gt;testSuite()-&gt;name(), 'functional')) {
  503.            DbHelper::setup();
  504.        }
  505.    }
  506. }
  507. </pre>
  508. <p>A subscriber implements the relevant interface for the event that you want to subscribe to. There are many events, which are <a href="https://docs.phpunit.de/en/10.5/events.html">listed in the docs</a> and you can infer the interface name by appending the word "Subscriber" to the event name.</p>
  509. <p>In our case, we want to subscribe to the <tt>PHPUnit\Event\TestSuite\Started</tt> event, so we implement the <tt>PhpUnit\Event\TestSuite\StartedSubscriber</tt> interface. Our subscriber then implements the <tt>notify()</tt> method where the <tt>$event</tt> is passed in. Unsurprisingly, the code for our <tt>notify()</tt> is essentially the same as we used in our <tt>TestListener</tt>.</p>
  510. <p>That's it. We're done.</p>
  511. <h2>A simplification to one class</h2>
  512. <p>As is clear by the name of my extension, this extension will only ever do one thing; set up the database before running the functional test suite. For a case like this, we can consolidate to a single class by creating an anonymous subscriber class and put it directly into the extension class:</p>
  513. <pre>
  514. &lt;?php
  515.  
  516. declare(strict_types=1);
  517.  
  518. namespace App\Test\Extension;
  519.  
  520. use App\Test\Helper\DbHelper;
  521. use PHPUnit\Event\TestSuite\Started;
  522. use PHPUnit\Event\TestSuite\StartedSubscriber;
  523. use PHPUnit\Runner\Extension\Extension;
  524. use PHPUnit\Runner\Extension\Facade;
  525. use PHPUnit\Runner\Extension\ParameterCollection;
  526. use PHPUnit\TextUI\Configuration\Configuration;
  527.  
  528. final class SetupDatabaseBeforeFunctionalTestsExtension implements Extension
  529. {
  530.    public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
  531.    {
  532.        $facade-&gt;registerSubscribers(
  533.            new class implements StartedSubscriber {
  534.                public function notify(Started $event): void
  535.                {
  536.                    if (str_contains($event-&gt;testSuite()-&gt;name(), 'functional')) {
  537.                        DbHelper::setup();
  538.                    }
  539.                }
  540.            }
  541.        );
  542.    }
  543. }
  544. </pre>
  545. <p>Nothing else changes and my database is set up before we run the test suite and I can now upgrade the rest of my tests to support PHPUnit 10.</p>
  546. ]]></content:encoded>
  547. <wfw:commentRss>https://akrabat.com/converting-a-phpunit-testlistener-to-an-event-subscriber/feed/</wfw:commentRss>
  548. <slash:comments>0</slash:comments>
  549. </item>
  550. </channel>
  551. </rss>
  552.  

If you would like to create a banner that links to this page (i.e. this validation result), do the following:

  1. Download the "valid RSS" banner.

  2. Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)

  3. Add this HTML to your page (change the image src attribute if necessary):

If you would like to create a text link instead, here is the URL you can use:

http://www.feedvalidator.org/check.cgi?url=http%3A//akrabat.com/feed/

Copyright © 2002-9 Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda