<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>blog.iankulin.com</title><link>https://blog.iankulin.com/</link><description>Recent content on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Sat, 16 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Ubuntu Ansible 'waiting for privilege escalation prompt'</title><link>https://blog.iankulin.com/ubuntu26-bump/</link><pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ubuntu26-bump/</guid><description>&lt;p&gt;I ran into a hiccup today - provisioning a new Ubuntu VPS, the ansible playbook to apply our security hardening failed.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ~/Developer/ansible_hl % ansible-playbook ssh-harden.yml --ask-vault-pass -e @vault.yml 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Vault password: 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PLAY &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Copy the hardened SSHD_CONFIG file to the remote server&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ****************************************************
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;TASK &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Gathering Facts&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ********************************************************************************************
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;ERROR&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Task failed: Timeout &lt;span style="color:#f92672"&gt;(&lt;/span&gt;12s&lt;span style="color:#f92672"&gt;)&lt;/span&gt; waiting &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; privilege escalation prompt:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fatal: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;&amp;lt;redacted IP&amp;gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: UNREACHABLE! &lt;span style="color:#f92672"&gt;=&lt;/span&gt;&amp;gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;changed&amp;#34;&lt;/span&gt;: false, &lt;span style="color:#e6db74"&gt;&amp;#34;msg&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Task failed: Timeout (12s) waiting for privilege escalation prompt:&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;unreachable&amp;#34;&lt;/span&gt;: true&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My routine is to run Ubuntu LTS, and when I was provisioning the server, I selected Ubuntu 26.04 LTS x64 without thinking. This LTS dropped in April, and excitingly the new versions of Ubuntu have Rust coreutils including &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The cause of the issue above (where anisible waits for the sudo password request but never sees it) is that the password prompt in &lt;code&gt;sudo-rs&lt;/code&gt; is different from real sudo. Here&amp;rsquo;s the old one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@iris-orca:~$ sudo lsb_release -d
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;sudo&lt;span style="color:#f92672"&gt;]&lt;/span&gt; password &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; ian: 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;No LSB modules are available.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Description:	Ubuntu 24.04.4 LTS
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@iris-orca:~$ sudo --version
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Sudo version 1.9.15p5
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Sudoers policy plugin version 1.9.15p5
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Sudoers file grammar version &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Sudoers I/O plugin version 1.9.15p5
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Sudoers audit plugin version 1.9.15p5
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@iris-orca:~$ 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the new one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@ksd-on-syd-001:~$ sudo lsb_release -d
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;sudo: authenticate&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Password: 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Description:	Ubuntu 26.04 LTS
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@ksd-on-syd-001:~$ sudo --version
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo-rs 0.2.13-0ubuntu1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@ksd-on-syd-001:~$ 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So, &lt;code&gt;[sudo] password for ian: &lt;/code&gt; vs &lt;code&gt;[sudo: authenticate] Password: &lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a big deal, and ansible has &lt;a href="https://github.com/ansible/ansible/pull/86175"&gt;already made&lt;/a&gt; a fix for the incompatibility, it just hasn&amp;rsquo;t flowed down to me yet. Ubuntu 24 is still LTS so I&amp;rsquo;ll drop back to that.&lt;/p&gt;
&lt;p&gt;For the adventurous, another possible approach would be to create an ansible user with passwordless ssh - I&amp;rsquo;d rather wait for the ansible update before I move to a Linux version using sudo_rs.&lt;/p&gt;</description></item><item><title>VS Code Dev Containers</title><link>https://blog.iankulin.com/vs-code-dev-containers/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/vs-code-dev-containers/</guid><description>&lt;h3 id="remote-ssh"&gt;Remote-SSH&lt;/h3&gt;
&lt;p&gt;One of the things I&amp;rsquo;ve done a bit in Visual Studio Code is using it&amp;rsquo;s ability to work on a different machine over SSH. I have a couple of LXCs on a server set up for different languages - one for C++ and another for Rust. They are things I don&amp;rsquo;t work in often, and I didn&amp;rsquo;t want to set them up on my laptop, but thought I might want them again sometime in the future.&lt;/p&gt;
&lt;p&gt;This is straightforward in VS Code - You install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"&gt;remote ssh extension&lt;/a&gt; locally, then choose &lt;code&gt;Remote-SSH: Connect to Host&lt;/code&gt; in the command palette. A few seconds later, it appears that you&amp;rsquo;re working locally, but actually you are working in a remote session on the server your VS Code instance is SSH&amp;rsquo;d into (a small indicator in the bottom left is the tell). You need to clone your project from git since you&amp;rsquo;re working in the server&amp;rsquo;s file system, and there&amp;rsquo;s some complexity with extensions since you&amp;rsquo;re sort of working in a new VS Code instance, but apart from that it feels the same as working locally.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/architecture-ssh.png" width="968" alt="Remote SSH VS Code architecture from https://code.visualstudio.com/docs/remote/ssh"&gt;
&lt;p&gt;The official docs for &lt;a href="https://code.visualstudio.com/docs/remote/ssh"&gt;Remote Development using SSH are here.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="why-now"&gt;Why now?&lt;/h3&gt;
&lt;p&gt;The reason I&amp;rsquo;ve been interested in this again lately is to provide a more secure environment for using AI agentic coding tools like Claude Code. If it&amp;rsquo;s running in it&amp;rsquo;s own server (that I can easily recreate from a snapshot) I don&amp;rsquo;t have to worry about dramas like having &lt;a href="https://www.theregister.com/2025/12/01/google_antigravity_wipes_d_drive/"&gt;my hard drive deleted&lt;/a&gt; by Gemini.&lt;/p&gt;
&lt;p&gt;But what if you don&amp;rsquo;t have the ability to spin up an environment on your homelab? There&amp;rsquo;s a few options, but probably the easiest for VS Code users is to work in a &amp;lsquo;Dev Container&amp;rsquo;.&lt;/p&gt;
&lt;h3 id="dev-containers"&gt;Dev Containers&lt;/h3&gt;
&lt;p&gt;Working in a Dev Container is basically the same as remoting into another server with VS Code, but in this case the other server is a container spun up in Docker.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/architecture-containers.png" width="968" alt="VS Code Dev Container architecture. From https://code.visualstudio.com/docs/devcontainers/containers"&gt;
&lt;p&gt;There&amp;rsquo;s a couple of important differences from working on a remote server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we&amp;rsquo;re working on our local files;&lt;/li&gt;
&lt;li&gt;Dev containers are an important system for sharing repeatable development environments, so it&amp;rsquo;s well integrated into VS Code - the tooling is nice.&lt;/li&gt;
&lt;li&gt;You need Docker/Docker desktop running locally.&lt;/li&gt;
&lt;li&gt;You need a container image to work with, and a &lt;code&gt;devcontainer.json&lt;/code&gt; file to tell VS Code how to manage all this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of this post will focus on the basics of working in a Dev Container. There is also a good explanation of all this in the &lt;a href="https://code.visualstudio.com/docs/devcontainers/tutorial"&gt;VS Code docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-container"&gt;The Container&lt;/h2&gt;
&lt;p&gt;After you&amp;rsquo;ve installed the VS Code &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers"&gt;extension&lt;/a&gt;, and set up your local Docker environment, you&amp;rsquo;re going to need a Docker container to work in. There&amp;rsquo;s a &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers"&gt;big list of existing containers&lt;/a&gt; that cover most scenarios, but I prefer to roll my own. This is achieved by writing a Dockerfile.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM node:24-bookworm

# Use non-root user for development
USER node

# Development environment
ENV NODE_ENV=development

# Default command
CMD [&amp;#34;npm&amp;#34;, &amp;#34;start&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m going to name this &lt;code&gt;Dockerfile.dev&lt;/code&gt; and put it in the &lt;code&gt;.devcontainer&lt;/code&gt; directory - later we&amp;rsquo;ll point to it from our &lt;code&gt;devcontainer.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-16.57.06.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The Dockerfile describes the system that we&amp;rsquo;ll be working in when we&amp;rsquo;re working on our project - it&amp;rsquo;s like the specification of our remote &amp;lsquo;server&amp;rsquo;. Sometimes you might want to install other stuff here - for example if you are a vim enthusiast, you might &lt;code&gt;apt install vim&lt;/code&gt; in the Dockerfile, but generally you should keep it reasonably generic.&lt;/p&gt;
&lt;h3 id="devcontainerjson"&gt;devcontainer.json&lt;/h3&gt;
&lt;p&gt;Next we need to tell VS Code how to work in the container we&amp;rsquo;ve specified.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
 &amp;#34;name&amp;#34;: &amp;#34;Node.js Dev Container&amp;#34;,
 &amp;#34;build&amp;#34;: {
 &amp;#34;dockerfile&amp;#34;: &amp;#34;Dockerfile.dev&amp;#34;
 },
 &amp;#34;forwardPorts&amp;#34;: [
 3000
 ],
 &amp;#34;postCreateCommand&amp;#34;: &amp;#34;npm install&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This goes in the .devcontainer directory. It&amp;rsquo;s almost all self explanatory - we tell the extension what container we&amp;rsquo;re using - in this case building it from &lt;code&gt;Dockerfile.dev&lt;/code&gt;, export the port we&amp;rsquo;re running on, and run a command in the terminal of our new system once it exists.&lt;/p&gt;
&lt;h3 id="whats-missing"&gt;What&amp;rsquo;s missing?&lt;/h3&gt;
&lt;p&gt;Alert readers will have noticed there&amp;rsquo;s a couple of things that we need which are missing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;VS Code Server - the way that VS Code is working in this configuration is that it&amp;rsquo;s running on our local machine, but connecting to a &amp;ldquo;VS Code Server&amp;rdquo; inside the container.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bind mounts to our local directory - Once we load up the dev container, all our local files will need to be available in VS Code somehow.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So how to these get in the container? We don&amp;rsquo;t have to do anything to deal with these two issues. This is part of the magic that the VS Code Dev Container extension is doing for us. After the container is created, the extension:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;installs the server binary, and starts it&lt;/li&gt;
&lt;li&gt;bind mounts the local workspace to &lt;code&gt;/workspaces/&amp;lt;your-folder-name&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;And sets that as the WORKDIR in the container&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="lets-go"&gt;Let&amp;rsquo;s go&lt;/h3&gt;
&lt;p&gt;Okay, with the &lt;code&gt;Dockerfile.dev&lt;/code&gt; and &lt;code&gt;devcontainer.json&lt;/code&gt; in place, we&amp;rsquo;re now ready to launch our devcontainer.&lt;/p&gt;
&lt;p&gt;In the VSCode command pallet, run &lt;code&gt;Dev Containers: Reopen in container&lt;/code&gt;. The first run will take a few moments since the container has to be built first, but then it should look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.49.40.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The big clue that you&amp;rsquo;re in the dev container now is the blue message in the bottom left corner. Most everything that we could do in the local environment we can do here in the container - for example editing code and running it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.54.43.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.54.53.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="extensions"&gt;Extensions&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-18.05.45.png" width="427" alt=""&gt;
&lt;p&gt;If you have a look at your VS Code extensions while working in this dev container, you&amp;rsquo;ll see that some are still installed, but others are now greyed out. Some extensions only effect the UI (&amp;ldquo;UI Extensions&amp;rdquo; - listed under &amp;ldquo;Local - Installed&amp;rdquo;) so they live in the local VS Code, others have functionality that needs to run in the workspace environment so they (&amp;ldquo;Workspace extensions&amp;rdquo;) need to live in the VS Code server part.&lt;/p&gt;
&lt;p&gt;Any UI Extensions you had installed before will still be there since they live in the local VS Code, but they others will be greyed out and unavailable unless you install them into the dev container.&lt;/p&gt;
&lt;p&gt;In the image here you can see my &amp;ldquo;vscode-icons&amp;rdquo; and &amp;ldquo;vscode-pdf&amp;rdquo; which only effect how things are displayed are both still available, but &amp;ldquo;Beancount&amp;rdquo; &amp;amp; &amp;ldquo;Cline&amp;rdquo; that need access to files need to be installed in the container if I want to use them on this project.&lt;/p&gt;
&lt;p&gt;We can click on the &amp;ldquo;Install in Dev Container&amp;rdquo; button for any of these, and it will be installed into the dev container. I need ESLint for this project so I&amp;rsquo;ll could that - then it will appear in the &amp;lsquo;Dev Container&amp;rsquo; tab. This is a good way of mimicking the normal workflow for users, but this extension is only persisted in the container while it exists - If we make any changes to the &lt;code&gt;dockerfile&lt;/code&gt; or &lt;code&gt;devcontainer.json&lt;/code&gt; VS Code will rebuild the container and the extension will be gone again.&lt;/p&gt;
&lt;p&gt;If we want a workspace extension, a more persistent way to install it is to add it to our &lt;code&gt;devcontainer.json&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="adding-extensions-to-devcontainerjson"&gt;Adding extensions to devcontainer.json&lt;/h3&gt;
&lt;p&gt;Extensions added this way will be the same for anyone who uses our dev container definition (which is what I&amp;rsquo;m calling the combined &lt;code&gt;dockerfile&lt;/code&gt; and &lt;code&gt;devcontainer.json&lt;/code&gt;), including if they just clone the project from git. In fact, this was the original intention of dev containers - to have a fully reproducible development environment that is stored with its project.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s add a couple of extensions.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
 &amp;#34;name&amp;#34;: &amp;#34;Node.js Dev Container&amp;#34;,
 &amp;#34;build&amp;#34;: {
 &amp;#34;dockerfile&amp;#34;: &amp;#34;Dockerfile.dev&amp;#34;
 },
 &amp;#34;customizations&amp;#34;: {
 &amp;#34;vscode&amp;#34;: {
 &amp;#34;extensions&amp;#34;: [
 &amp;#34;dbaeumer.vscode-eslint&amp;#34;,
 &amp;#34;esbenp.prettier-vscode&amp;#34;
 ]
 }
 },
 &amp;#34;forwardPorts&amp;#34;: [
 3000
 ],
 &amp;#34;postCreateCommand&amp;#34;: &amp;#34;npm install&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you make these changes in VS Code, it will probably pop up and ask you if it can rebuild the container, otherwise open the command palette and select &amp;ldquo;Dev Containers: Rebuild Container&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You might be wondering where we get the extension id&amp;rsquo;s from (like &lt;code&gt;dbaeumer.vscode-eslint&lt;/code&gt;) The easiest way to to right click on one in the extension list and select &amp;ldquo;Copy Extension ID&amp;rdquo;. It&amp;rsquo;s also listed on the extension web page.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-10-at-15.23.47.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="finally"&gt;Finally&lt;/h3&gt;
&lt;p&gt;So that&amp;rsquo;s the basics of getting a simple dev container set up. The code for this is on &lt;a href="https://github.com/IanKulin/devcont-demo"&gt;GitHub here&lt;/a&gt;. There&amp;rsquo;s a crucial issue we haven&amp;rsquo;t solved though - how to pull in your ssh keys for pushing the code to external repositories. We&amp;rsquo;ll deal with that in a future post.&lt;/p&gt;</description></item><item><title>Getting Ghostty to Work on Synology</title><link>https://blog.iankulin.com/getting-ghostty-to-work-on-synology/</link><pubDate>Mon, 28 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-ghostty-to-work-on-synology/</guid><description>&lt;p&gt;Ghostty is a terminal application that I don&amp;rsquo;t really &lt;em&gt;need&lt;/em&gt; (it&amp;rsquo;s &lt;a href="https://ghostty.org/docs/about"&gt;listed features&lt;/a&gt; either already exist in the MacOS terminal, or seem so esoteric or marginal that I can&amp;rsquo;t imagine any real benefit from them in my normal use), but I &lt;em&gt;wanted&lt;/em&gt; to be one of the cool kids, so I thought I&amp;rsquo;d give it a try.&lt;/p&gt;
&lt;p&gt;After fiddling around with the themes for a bit I renamed it to &amp;rsquo;term-ghosty.app&amp;rsquo; so I&amp;rsquo;d remember to use it (ie when I pop up spotlight and type &amp;rsquo;term&amp;rsquo; it will come up) and got on with my day. Ten minutes later I&amp;rsquo;d run into a problem.&lt;/p&gt;
&lt;h3 id="the-problem"&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I was ssh&amp;rsquo;d into a Synology NAS, and needed to use a command from two commands ago, in my long experience, pressing the up arrow twice works universally well, but not in Ghostty on this host:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.31.38.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This is a purely visual glitch - if I press return at this stage, it will run &lt;code&gt;command one&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My first thought was to CTRL-U to clear the line:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.32.00.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I guess not. Oh well, lets &lt;code&gt;clear&lt;/code&gt; and try all this again.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.32.07.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So - a clue. This is a Ghostty problem, not a weird shell issue. Also, this wasn&amp;rsquo;t just a visual thing - the command was also not working.&lt;/p&gt;
&lt;p&gt;I logged in with regular terminal to confirm that everything was still working with that.&lt;/p&gt;
&lt;h3 id="xterm-ghostty"&gt;xterm-ghostty&lt;/h3&gt;
&lt;p&gt;If you google &amp;lsquo;ghostty arrow history problem&amp;rsquo; you&amp;rsquo;ll likely find a number of github issues that are all closed after the developer has posted a link to &lt;a href="https://ghostty.org/docs/help/terminfo#ssh"&gt;this part of the docs&lt;/a&gt; explaining that you need to compile the Ghostty&amp;rsquo;s terminfo into the config on this host.&lt;/p&gt;
&lt;p&gt;At first I was a bit aghast at this complicated solution - but we need to keep in mind this is a terminal program that I&amp;rsquo;m sure is only used by tech orientated people who love fiddling with things. This is reflected in other design choices in Ghostty (going into &amp;lsquo;Settings&amp;rsquo; from the menu just opens the config file in a text editor).&lt;/p&gt;
&lt;p&gt;I downloaded &lt;a href="https://iterm2.com/index.html"&gt;iTerm2&lt;/a&gt; as a likely competitor to Ghostty and tried it on the same host - everything worked perfectly. I tried Ghostty on several of my VM&amp;rsquo;s, VPS&amp;rsquo;s and LXC&amp;rsquo;s. All no problem. So what&amp;rsquo;s going on?&lt;/p&gt;
&lt;h3 id="whats-going-on"&gt;What&amp;rsquo;s Going On?&lt;/h3&gt;
&lt;p&gt;If you open your terminal, and type &lt;code&gt;echo $TERM&lt;/code&gt; it will tell you the &lt;code&gt;TERM&lt;/code&gt; value. This is used by the shell to know how to interpret the inputs it&amp;rsquo;s receiving (for example, what to do when the user wants to &lt;code&gt;clear&lt;/code&gt; the screen). Unless you are using Ghostty, it will almost certainly be set to &lt;code&gt;xterm-256color&lt;/code&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.03.36.png" width="808" alt=""&gt;
&lt;p&gt;In Ghostty, it will say &lt;code&gt;xterm-ghostty&lt;/code&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.04.55.png" width="772" alt=""&gt;
&lt;p&gt;The reason I don&amp;rsquo;t have this same problem with Ghostty on my MacBook or the Debian base hosts is that those operating systems have the Ghostty &amp;rsquo;terminfo&amp;rsquo; entries in them, whereas apparently Synology does not (or not yet anyway).&lt;/p&gt;
&lt;p&gt;But that does that explain why iTerm2 works on Synology. Let&amp;rsquo;s look at it&amp;rsquo;s TERM value.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.12.45.png" width="882" alt=""&gt;
&lt;p&gt;Ah, so it&amp;rsquo;s just claiming to be xterm-256color the same as the mac terminal emulator - which all hosts will know since it&amp;rsquo;s an ancient thing. (xterm is the terminal from the X-Windows systems of the 1980&amp;rsquo;s).&lt;/p&gt;
&lt;p&gt;This is a developer choice, Ghostty could also claim it was an xterm-256color and this problem would not have popped up. I&amp;rsquo;m assuming they have decided this short term pain is worth it for some long term gain (of being able to do things an xter-256color terminal emulator can not).&lt;/p&gt;
&lt;h3 id="choices"&gt;Choices&lt;/h3&gt;
&lt;p&gt;Now we have two choices to fix this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Override the TERM choice the Ghostty developer has made for this host&lt;/li&gt;
&lt;li&gt;Compile the correct &lt;code&gt;terminfo&lt;/code&gt; into the config on this host&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Option one means we&amp;rsquo;d lose any special sauce Ghostty does (which I already said I don&amp;rsquo;t need, but could conceivably regret in the future if they do something cool).&lt;/p&gt;
&lt;p&gt;Option two feels like the proper (and is the one that Ghostty recommends).&lt;/p&gt;
&lt;p&gt;I suppose there is a third choice - wait until Synology includes the Ghostty terminfo in their distro. I get the vibe from the slightly scary MOTD when I ssh in that they would really prefer you did not, so I can&amp;rsquo;t seem them going out of their way to include it. Also they are not based on another distro as far as I can see, so they are not going to accidentally include it from an upstream. I feel this choice will never bear fruit.&lt;/p&gt;
&lt;h3 id="overriding-the-term"&gt;Overriding the TERM&lt;/h3&gt;
&lt;p&gt;ssh can have a config set for a host so we can override the TERM value. If you have something like this in &lt;code&gt;~/.ssh/config&lt;/code&gt; we can fix the issue.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Host NAS-DS2
 SetEnv TERM=xterm-256color
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.46.46.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And everything works again - I can up arrow to access the history without problems and the &lt;code&gt;clear&lt;/code&gt; command works as advertised. Note that I didn&amp;rsquo;t need to reload the ssh config - it gets read every time you run the ssh command.&lt;/p&gt;
&lt;p&gt;An extra reason for using this approach is that if you have a good naming convention for your hosts (I worked in the &amp;lsquo;data processing&amp;rsquo; department of a bank in the 1990&amp;rsquo;s so I learned good naming conventions for hosts) then you can wild-card this entry to for all of them. You might have guessed that all the Synology NAS&amp;rsquo;s I manage are named &lt;code&gt;NAS-DS&amp;lt;some positive integer&amp;gt;&lt;/code&gt;. Let&amp;rsquo;s change the config to say:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Host NAS-DS*
 SetEnv TERM=xterm-256color
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.51.17.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Nice. If I had hundreds of these to deal with, that&amp;rsquo;s definitely the solution I&amp;rsquo;d be going for. Also, if you&amp;rsquo;ve come here because you had this exact problem stop here. This is the best you are going to do.&lt;/p&gt;
&lt;h3 id="installing-the-terminfo"&gt;Installing the terminfo&lt;/h3&gt;
&lt;p&gt;The alternate approach is to extract the xterm-ghostty &lt;code&gt;terminfo&lt;/code&gt; off the current machine (in my case a MacBook) to the other host, and compile it into the available &lt;code&gt;terminfo&lt;/code&gt;&amp;rsquo;s there. This seems more invasive, but it&amp;rsquo;s a per user thing and can be reversed.&lt;/p&gt;
&lt;p&gt;The command given in the &lt;a href="https://ghostty.org/docs/help/terminfo#ssh"&gt;Ghostty docs&lt;/a&gt; is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;infocmp -x xterm-ghostty | ssh YOUR-SERVER -- tic -x -
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let&amp;rsquo;s try it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-13.02.16.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This is not that surprising - Synology DSM is minimal (I think it uses busybox) so it&amp;rsquo;s missing lots of these commands. You might think that&amp;rsquo;s okay, I&amp;rsquo;ll compile it on this machine then &lt;code&gt;scp&lt;/code&gt; it across. That would be something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;infocmp -x xterm-ghostty &amp;gt; xterm-ghostty.src
tic -x -o ./terminfo xterm-ghostty.src
scp -r ./terminfo/78 ds2_admin@NAS-DS2:~/.terminfo/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But that won&amp;rsquo;t work because you don&amp;rsquo;t have &lt;code&gt;scp&lt;/code&gt; on the Synology either. So perhaps you think you&amp;rsquo;ll enable rysnc in the NAS GUI and rsync the file in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -av ./terminfo/78 ds2_admin@NAS-DS2:~/.terminfo/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which will copy the file over, but it still won&amp;rsquo;t work. It&amp;rsquo;s just too minimal.&lt;/p&gt;
&lt;p&gt;I imagine this process works for other distros or it wouldn&amp;rsquo;t be in the Ghostty docs. But it does not work for Synology NASs in 2025.&lt;/p&gt;</description></item><item><title>State of AI tooling (for me)</title><link>https://blog.iankulin.com/state-of-ai-tooling-for-me/</link><pubDate>Mon, 07 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/state-of-ai-tooling-for-me/</guid><description>&lt;p&gt;I&amp;rsquo;ve been meaning to write this for a couple of weeks, so let&amp;rsquo;s get to it - things are moving to fast to reflect too long; which is it&amp;rsquo;s own risk.&lt;/p&gt;
&lt;p&gt;In March, I wrote about &lt;a href="https://blog.iankulin.com/where-im-up-to-with-ai-for-coding/"&gt;how I was using AI in coding&lt;/a&gt;, which was Codeium (now Windsurf) in VS Code for completions, and ChatGPT and Claude online for architecture questions and code gen that was more than half a function.&lt;/p&gt;
&lt;h3 id="media"&gt;Media&lt;/h3&gt;
&lt;p&gt;In my usual keeping-current media consumption I hit a couple of surprises:&lt;/p&gt;
&lt;p&gt;Steve Yegge on Changelog &amp;ldquo;&lt;a href="https://changelog.com/friends/96"&gt;Adventures in babysitting coding agents&lt;/a&gt;&amp;rdquo; - Steve is the author of a book on Vibe Coding which is not due out till later in the year, by which time it will surely be out of date, but also works for &lt;a href="https://sourcegraph.com/amp"&gt;Sourcegraph on Amp&lt;/a&gt; which is an agentic tool aimed at enterprise. His pitch was that agentic coding (where the AI can do things - read and edit files, run command line tools etc) is ready now for most tasks, and that the returns on the minimal effort required to code something with prompts are so good that it opens up a lot of projects you wouldn&amp;rsquo;t have bothered with. So you should pick a utility and write it with one of the agentic coding tools. He mentioned a heap - Amp (obviously) but also Cursor, Cline, Claude code and so on.&lt;/p&gt;
&lt;p&gt;I think this probably blew up the ChangeLog peeps discord, and it certainly took me aback a bit - like who&amp;rsquo;d be letting a hallucinating bot loose in their terminal??&lt;/p&gt;
&lt;p&gt;I wanted a bit more science input, and got that from another podcast - &lt;a href="https://ocdevel.com/mlg"&gt;Machine Learning Guide&lt;/a&gt; from &lt;a href="https://www.youtube.com/@ocdevel"&gt;Tyler Renelle&lt;/a&gt;, specifically &lt;a href="https://ocdevel.com/mlg/mla-22"&gt;episodes 22-24&lt;/a&gt;. I can&amp;rsquo;t recommend Tyler highly enough - a very clear thoughtful communicator. I&amp;rsquo;ll be going back to listen to his whole course in machine learning.&lt;/p&gt;
&lt;p&gt;So all that was enough for me to think there&amp;rsquo;s definitely something here, so I&amp;rsquo;d better look at it and see if I need to change.&lt;/p&gt;
&lt;h3 id="tokens"&gt;Tokens&lt;/h3&gt;
&lt;p&gt;Unfortunately this decision was a few days after I&amp;rsquo;d canceled my monthly Claude plan on the basis that I could just buy $35 of tokens and use them in my selfhosted &lt;a href="https://www.librechat.ai/"&gt;LibreChat. (LibreChat&lt;/a&gt; is basically the Claude/ChatGPT interface that you can connect to any model and pay with tokens).&lt;/p&gt;
&lt;p&gt;My thought was that that way, I could swap between different AI companies models as I fancied, and it would probably end up being cheaper. Which, maybe it would have if I&amp;rsquo;d kept using LLMs the way I had been&amp;hellip;&lt;/p&gt;
&lt;h3 id="cline"&gt;Cline&lt;/h3&gt;
&lt;p&gt;Instead what I did was install the &lt;a href="https://cline.bot/"&gt;Cline&lt;/a&gt; add-on in VS Code and gave it my API keys so it could gobble up tokens. The interface is like a chat - you can say things like &amp;ldquo;Turn this node/express app this into a TypeScript project&amp;rdquo; and if you&amp;rsquo;re using Claude as the backend, it will go ahead and make a plan to do that. But then, it will ask you to switch from &amp;ldquo;Plan&amp;rdquo; to &amp;ldquo;Act&amp;rdquo; and jump in and edit the files, run the tests, run the linter etc then loop on that until that is finished. It asks permission for things as it needs, and at first I&amp;rsquo;d carefully inspect what it was doing and why before granting any of them, but very quickly just trusted it with everything. (No doubt there will be a big attack based on this in 2025 - why not take screenshots of my open BitWarden and send them to Russia?).&lt;/p&gt;
&lt;p&gt;If you haven&amp;rsquo;t seen an agentic tool powered by Claude Sonnet doing this stuff, prepare to be amazed. Tool use by AI&amp;rsquo;s is definitely the future, and probably not just for code. It still does sometimes get stuck in a rabbit hole - I find if it hasn&amp;rsquo;t solved it&amp;rsquo;s own problems after a couple of helpful interventions from me, it&amp;rsquo;s probably not going to (I guess it&amp;rsquo;s poisoned it&amp;rsquo;s own context too much) and it&amp;rsquo;s easier to kill it and give it a different (often more focused) prompt on a fresh start. I just used git for my rollback on those occasions though I understand others (perhaps Cursor?) have &amp;lsquo;checkpoints&amp;rsquo; built into the tool.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/do-all-the-things-meme-template-full-e9a85cb2.webp" width="800" alt=""&gt;
&lt;p&gt;A feature of Cline is that it shows you the token use and dollar amount as it&amp;rsquo;s working. I burned through USD20 of Anthropic tokens in about 4 days of coding all the things. I would just open up a project I use in VS Code, and have the Forgejo issues for it on the other screen, and copy them across to Cline one at a time.&lt;/p&gt;
&lt;p&gt;Since these were serious projects I was making branches and code-checking them manually at the pull request stage, but for utilities that can fit on a single web page (something like &amp;ldquo;I want to drop a word doc of school report comments on here, and have you switch out the names to fakes ones that you can replace later with the real ones, and there&amp;rsquo;s a button for me to download the fake name version&amp;rdquo;) I wouldn&amp;rsquo;t look at the code, just the results.&lt;/p&gt;
&lt;p&gt;I topped up the Anthropic money, but also gave Cline my OpenAI, Deep Seek and Gemini API keys and quickly came to these (probably not reliable) conclusions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Sonnet 4 - the GOAT.&lt;/li&gt;
&lt;li&gt;OpenAI ChatGPT 4 - okay, but not as good as Sonnet. The cost is 2/3 of Sonnet but it&amp;rsquo;s probably 85% as good. So mathematically good value, but in practice that last little bit makes Sonnet way more useful.&lt;/li&gt;
&lt;li&gt;Deepseek - 1/10th of the price of Sonnet. Less good than either of the other two, but I still found myself using it for low intelligence tasks. For example I might get Sonnet to make a detailed plan for renaming a concept in a code base eg &lt;em&gt;&amp;ldquo;I&amp;rsquo;ve been referring to these little files as URLs but now I want them called download jobs everywhere&amp;rdquo;&lt;/em&gt;. Then with that written by Sonnet into a markdown file with things like &amp;ldquo;&lt;code&gt;[ ] in utilities.js on line 321 rename function validateURL() to validateJob()&lt;/code&gt;&amp;rdquo; I&amp;rsquo;d let Deepseek do the grunt work of all the file edits before swapping back to Claude to do the linting and test error fixing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see all these cost details right in Cline as you are switching between the models.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-07-at-15.43.54.png" width="841" alt=""&gt;
&lt;h3 id="claude-code"&gt;Claude Code&lt;/h3&gt;
&lt;p&gt;It is also possible to add your Anthropic API key into Claude code and let it eat your tokens in exchange for a in-browser Space Invaders clone or whatever, so I tried this weird idea of just vibing from the CLI instead of my editor. It worked really, really well. I&amp;rsquo;d still have the code open in VS Code, and review it at the commit stage (I still do this now), but it was very impressive. Sadly, the model or system prompt is so tuned for action that I very quickly ran out of tokens again.&lt;/p&gt;
&lt;h3 id="dollar-dollar-bill-yall"&gt;Dollar, dollar bill y&amp;rsquo;all&lt;/h3&gt;
&lt;p&gt;About this time, I became aware that some level of Claude Code use is included in the &lt;a href="https://www.anthropic.com/pricing"&gt;Pro Plan&lt;/a&gt; - ie the USD20/month plan I&amp;rsquo;d been on before. A trick for new players is that if you&amp;rsquo;re changing from tokens to a plan you need to &lt;code&gt;/logout&lt;/code&gt; and &lt;code&gt;/login&lt;/code&gt; in Claude Code to switch it over to your plan (yes I had to top up my Anthropic credits again).&lt;/p&gt;
&lt;p&gt;With that hurdle passed I was now 100% in on Claude Code. On this plan I can generally code for a couple of hours a night without ever seeing the warning appear. On the weekend, I might get to about lunchtime, then have to wait a couple of hours to start again.&lt;/p&gt;
&lt;h3 id="gemini-drops"&gt;Gemini drops&lt;/h3&gt;
&lt;p&gt;At the end of June, Google launched &lt;a href="https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/"&gt;Gemini CLI&lt;/a&gt;. I&amp;rsquo;d somehow ended up with $50 of free tokens without entering any billing details and had been using them in Cline, so I had some feeling about Gemini 2.5 and what it is capable of. I think the Gemini CLI is free (ie no token use) for personal use at the moment. It is not as good as Claude Code + Sonnet yet. I understand it has a very large context window, so perhaps if you&amp;rsquo;re working on a very big project it will be comparatively stronger - &lt;a href="https://www.youtube.com/watch?v=nfOVgz_omlU"&gt;Armin Ronacher says he uses it from inside Claude Code to summarise large code bases&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Still - &amp;ldquo;free&amp;rdquo; is a compelling value argument. I&amp;rsquo;ll often break out Gemini CLI if I&amp;rsquo;ve run out of Claude Code time, but not on serious projects.&lt;/p&gt;
&lt;h3 id="tips-and-tricks-for-agentic-coding-with-claude-code"&gt;Tips and Tricks for Agentic Coding with Claude Code&lt;/h3&gt;
&lt;p&gt;So, from the sources above, my own experience and the vibes from the zeitgeist, here is some things that work, right now in July 2025.&lt;/p&gt;
&lt;p&gt;Plan - &amp;gt; Act&lt;/p&gt;
&lt;p&gt;I guess I learned this from Cline. Ask for a detailed plan for the change - if it&amp;rsquo;s heading on a wrong track you&amp;rsquo;ll usually pick it up here. I&amp;rsquo;ll often ask for this as a markdown file when it completely understands the job and has explained it back to me.&lt;/p&gt;
&lt;p&gt;Mistakes are Cheaper Early&lt;/p&gt;
&lt;p&gt;Once it gets going on a big task, it will often run for for ten minutes or so. I don&amp;rsquo;t want to do ten minutes worth of AI datacentre environmental damage for code I&amp;rsquo;m going to throw away. So I will read through the proposal before I let it get to work. In Claude Code, [SHIFT][TAB] switches between plan and act. I don&amp;rsquo;t let it out of plan until there is a good plan that I&amp;rsquo;m happy with.&lt;/p&gt;
&lt;p&gt;Guardrails&lt;/p&gt;
&lt;p&gt;I think this was in my previous article. Have it set up linting, tests &amp;amp; formatting, and make a rule that it runs them. It needs a feedback loop, this is one of them. I&amp;rsquo;ve also jumped fully into TypeScript for Javascript with AI coding. It&amp;rsquo;s like a slightly over-enthusiastic junior developer who has read books on every subject and API in the world. The guardrails force it to do things better.&lt;/p&gt;
&lt;p&gt;Good Coding Practices&lt;/p&gt;
&lt;p&gt;Small files, good architecture, clear names, good project organisation. Small amount of up to date documentation that describes the shape and why of things. I like to keep the &amp;lsquo;sprints&amp;rsquo; small. So I&amp;rsquo;m going from one working app state to another. I also frequently refactor - probably more than in my own handcoding.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-07-at-14.17.04.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;CLAUDE.md&lt;/p&gt;
&lt;p&gt;These agent instructions get sent up with every chat. Keep them succinct. I take my cue about what it needs by watching it work. If it&amp;rsquo;s grepping all the time to find the main functions, I write a section about where they are and what they do - it&amp;rsquo;s a token saver mechanism. If you get Claude Code to update it, I think it puts too much in - since it&amp;rsquo;s going with every request it will chew up tokens reading it so this is a balancing act.&lt;/p&gt;
&lt;p&gt;Tell it what tools, and where things are. For example if I don&amp;rsquo;t tell it that it can use the Playwright MCP to check any UI changes it makes it usually won&amp;rsquo;t bother. I give it a &lt;code&gt;temp/&lt;/code&gt; directory to write disposable scripts in.&lt;/p&gt;
&lt;p&gt;MCP&lt;/p&gt;
&lt;p&gt;MCP was everywhere a couple of weeks ago, but it&amp;rsquo;s possible that it will be a fad - you don&amp;rsquo;t need a git or a github MCP server, just tell Claude to use it on the command line. The only MCP server I install is Playwright.&lt;/p&gt;
&lt;p&gt;Start Over&lt;/p&gt;
&lt;p&gt;Since the whole chat history is going up with every request, you want the least amount of baggage. As soon as we don&amp;rsquo;t need what&amp;rsquo;s in the context for the next job, I clear it. If I do need it, but it&amp;rsquo;s gotten long or contains direction changes, I ask for a markdown file of the plan, then restart with that.&lt;/p&gt;
&lt;p&gt;Chat is still helpful&lt;/p&gt;
&lt;p&gt;I have browser tabs open with ChatGPT and Claude and use them for all the self-contained queries, for instance &lt;em&gt;&amp;ldquo;Should I hand code the interfaces to different LLMs or is there a good library that does this?&amp;rdquo;&lt;/em&gt; isn&amp;rsquo;t something to do in Claude Code - it doesn&amp;rsquo;t need access to your project. Do that somewhere else with VC money.&lt;/p&gt;
&lt;h3 id="keep-learning"&gt;Keep Learning&lt;/h3&gt;
&lt;p&gt;This is fast moving. I am getting great value out of these tools right now with these techniques, but we are in a new, changing, exciting, world with this stuff.&lt;/p&gt;</description></item><item><title>Writing a Browser Extension</title><link>https://blog.iankulin.com/writing-a-browser-extension/</link><pubDate>Sun, 22 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/writing-a-browser-extension/</guid><description>&lt;p&gt;Web pages are mostly just a collection of HTML, CSS, and JavaScript, so if we had some way of adding some of these into a web page, perhaps from our browser we could add new behaviour to a web page, right?&lt;/p&gt;
&lt;p&gt;Yes; users have long used tools like Greasemonkey (or similar userscript managers) to inject scripts into pages. Better still, modern browsers expose JavaScript APIs that let us interact directly with the browser itself. Enter: browser extensions.&lt;/p&gt;
&lt;p&gt;It turns out this is quite simple to do. And, it&amp;rsquo;s well documented (here&amp;rsquo;s the &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension"&gt;step-by-step for Firefox&lt;/a&gt;), but if you want, follow along with me while I solve a gripe with a browser extension for Firefox.&lt;/p&gt;
&lt;h3 id="extension-or-add-on"&gt;Extension or Add-On?&lt;/h3&gt;
&lt;p&gt;Before we get started, let&amp;rsquo;s clear up some naming ambiguity. Firefox has &lt;a href="https://addons.mozilla.org/en-US/firefox/"&gt;add-on&amp;rsquo;s&lt;/a&gt; - these are extensions, plus some goodies like themes. I&amp;rsquo;m going to continue to call them &lt;em&gt;extensions&lt;/em&gt; - it&amp;rsquo;s browser non-specific and you&amp;rsquo;re using the &lt;a href="https://extensionworkshop.com/documentation/develop/about-the-webextensions-api/"&gt;Web-Extension API&lt;/a&gt; which is mostly-browser agnostic way of doing these things.&lt;/p&gt;
&lt;h3 id="the-gripe"&gt;The Gripe&lt;/h3&gt;
&lt;p&gt;I often want to steal an image from the web, so I right click to open it in a new tab, and save it, only to find it&amp;rsquo;s a .&lt;code&gt;webp&lt;/code&gt; that can&amp;rsquo;t be used for whatever I wanted to steal it for. I often notice is that the original image is a .jpg but it&amp;rsquo;s been converted (often by a SASS image conversion product such as &lt;a href="https://docs.imgix.com/en-US/apis/rendering/overview"&gt;imgx&lt;/a&gt;, &lt;a href="https://imagekit.io/guides/image-optimization/#chapter-7---resizing-images-to-fit-the-layout"&gt;imagekit&lt;/a&gt;, or &lt;a href="https://sirv.com/help/articles/dynamic-imaging/"&gt;sirv&lt;/a&gt; which apply transformations via query parameters like ?w=600). They&amp;rsquo;ll have links like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;https://demo.sirv.com/look.jpg?w=600
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://demo.sirv.com/look.jpg?w=600"&gt;View embed&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;https://demo.sirv.com/look.jpg?w=200
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/look.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="the-solution"&gt;The solution&lt;/h3&gt;
&lt;p&gt;These are &amp;ldquo;query parameters&amp;rdquo;. It&amp;rsquo;s a smart, simple way of applying transformations to images. On the server end, the parameters are interpreted as what to do to each image. Of course, if you just remove the parameters, you get the original image.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;https://demo.sirv.com/look.jpg
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/look-c0a2d705-f421-4d99-9ba9-1b8694354e78.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And if I&amp;rsquo;m stealing it, that&amp;rsquo;s what I want. So my plan is to write a browser extension that allows me right click on an image, and open it up in a new tab with all the query parameters removed.&lt;/p&gt;
&lt;h3 id="manifest"&gt;Manifest&lt;/h3&gt;
&lt;p&gt;You might have heard about the new &amp;ldquo;Manifest 3&amp;rdquo; that limits what extensions can do (and breaks ad blockers) in Chrome? Manifest v2 remains fully supported in Firefox, and offers simpler APIs for things like background scripts, which are perfect for small utility extensions like this. Here&amp;rsquo;s our manifest.js:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
 &amp;#34;manifest_version&amp;#34;: 2,
 &amp;#34;name&amp;#34;: &amp;#34;Clean Image Opener&amp;#34;,
 &amp;#34;version&amp;#34;: &amp;#34;1.1&amp;#34;,
 &amp;#34;description&amp;#34;: &amp;#34;Open images in new tabs with query parameters stripped&amp;#34;,

 &amp;#34;permissions&amp;#34;: [&amp;#34;contextMenus&amp;#34;, &amp;#34;tabs&amp;#34;],
 &amp;#34;incognito&amp;#34;: &amp;#34;spanning&amp;#34;,

 &amp;#34;background&amp;#34;: {
 &amp;#34;scripts&amp;#34;: [&amp;#34;background.js&amp;#34;]
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The manifest is the meta-data portion of the extension. Most of these fields are pretty obvious, but let&amp;rsquo;s talk about a couple:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;**&amp;quot;permissions&amp;quot;: [&amp;quot;contextMenus&amp;quot;, &amp;quot;tabs&amp;quot;]**&lt;/code&gt; - here we are specifying what permissions our code is going to need. The browser uses this to block API calls that would need any other permissions. It&amp;rsquo;s part of &lt;em&gt;principle of least privilege&lt;/em&gt; system that makes clear to users what can be done, then builds those restrictions into the execution.&lt;/p&gt;
&lt;p&gt;In this case were asking for &lt;code&gt;&amp;quot;contextMenus&amp;quot;&lt;/code&gt; because we want to add something to the right click menu, and &lt;code&gt;&amp;quot;tabs&amp;quot;&lt;/code&gt; because we want to open one with a URL we pass it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also worth noting that these are the browser functions that are being restricted - our code still has access to the web page data (ie the URL of the image we&amp;rsquo;re right clicking on) since the DOM is all in the user scope anyway - for example you can open the developer tools to access that information. This is still a potential risk though - for example if a malicious extension wanted to collect that viewed image url and export it as telemetry. Browser extensions need defenses other than the manifest for those types of attacks.&lt;/p&gt;
&lt;p&gt;There are many different permissions - &amp;ldquo;history&amp;rdquo;, &amp;ldquo;clipboard&amp;rdquo;, &amp;ldquo;webrequest&amp;rdquo;. The important intent is that the user can reasonably be aware of what&amp;rsquo;s being asked and weigh it up against what the extension is doing for them. The first layer of security for add-ons lies with us - the developer. Browser extensions have wide access to user activity and should be kept as minimal as possible to avoid abuse or privacy leakage.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;**&amp;quot;incognito&amp;quot;: &amp;quot;spanning&amp;quot;**&lt;/code&gt; - Here we&amp;rsquo;re saying how we want the extension to work in incognito mode. In the Manifest 2 specification there are three options for incognito - &amp;ldquo;not_allowed&amp;rdquo;, &amp;ldquo;split&amp;rdquo; and &amp;ldquo;spanning&amp;rdquo;. &amp;ldquo;split&amp;rdquo; was intended to allow the extension in both modes, but not allow data to be shared between them - essentially to run separate copies of the extension in each mode. It&amp;rsquo;s not implemented in modern Firefox - I suspect because of earlier security problems, so we&amp;rsquo;re using &amp;ldquo;spanning&amp;rdquo; which should indicate to the user of the extension that data may be passed between the two modes.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;**&amp;quot;background&amp;quot;: { &amp;quot;scripts&amp;quot;: [&amp;quot;background.js&amp;quot;] }**&lt;/code&gt; - we want our script to run in the background (to listen for right-clicks and act on them) so it goes in here.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the first part of background.js:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// Create the context menu item when the extension starts
browser.contextMenus.create({
 id: &amp;#34;open-clean-image&amp;#34;,
 title: &amp;#34;Open image in new tab (no parameters)&amp;#34;,
 contexts: [&amp;#34;image&amp;#34;],
 documentUrlPatterns: [&amp;#34;&amp;lt;all_urls&amp;gt;&amp;#34;],
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;title:&lt;/code&gt; is just the text that appears in the context menu when the user write clicks on an image (this is the &lt;code&gt;contexts: [&amp;quot;image&amp;quot;]&lt;/code&gt; part). This can happen on &lt;code&gt;[&amp;quot;&amp;lt;all_urls&amp;gt;&amp;quot;]&lt;/code&gt; (we could have restricted it to a particular domain or sub-domain). The &lt;code&gt;id:&lt;/code&gt; is just how we are going to reference it in the click handler. Speaking of:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// Handle context menu clicks
browser.contextMenus.onClicked.addListener((info, tab) =&amp;gt; {
 if (info.menuItemId === &amp;#34;open-clean-image&amp;#34;) {
 // Get the image URL and strip query parameters
 const originalUrl = info.srcUrl;
 const cleanUrl = stripQueryParameters(originalUrl);

 // Open the clean URL in a new tab
 browser.tabs
 .create({
 url: cleanUrl,
 active: true,
 })
 .catch((error) =&amp;gt; {
 console.error(&amp;#34;Failed to create tab:&amp;#34;, error);
 });
 }
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Not much explanation needed here due to good naming and generous comments ;-)&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s pretty much the whole extension. Of course there&amp;rsquo;s a &lt;code&gt;stripQueryParameters()&lt;/code&gt; somewhere. For now, just imagine it says:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function stripQueryParameters(url) {
 return url.split(&amp;#39;?&amp;#39;)[0];
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="testing"&gt;Testing&lt;/h3&gt;
&lt;p&gt;To load our new Firefox extension to try it out, go to the url &lt;a href="debugging#/runtime/this-firefox"&gt;about:debugging#/runtime/this-firefox&lt;/a&gt; where we want to &amp;ldquo;Load a Temporary Add-on&amp;rdquo; - in the open dialog choose your manifest file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-06-21-at-20.23.08.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now the extension should be working for normal browser mode, to enable it in incognito, you&amp;rsquo;ll need to head into the Firefox menu &amp;ldquo;Add-ons and themes&amp;rdquo; (&lt;a href="addons"&gt;about:addons&lt;/a&gt;) then open the &amp;ldquo;Manage&amp;rdquo; menu for the extension and turn on &amp;ldquo;Run in Private Windows&amp;rdquo;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-06-22-at-08.27.38.png" width="913" alt=""&gt;
&lt;h3 id="publishing"&gt;Publishing&lt;/h3&gt;
&lt;p&gt;Publishing an extension to the Firefox Add-ons store could be a whole post, but it&amp;rsquo;s the sort of thing you can follow your nose through, by starting at a&lt;a href="https://addons.mozilla.org/en-US/developers/"&gt;ddons.mozilla.org/en-US/developers&lt;/a&gt;. There is a semi-manual review process, so don&amp;rsquo;t expect it to be instant.&lt;/p&gt;
&lt;p&gt;The source for this project is &lt;a href="https://github.com/IanKulin/clean-image/tree/v1.1"&gt;available here&lt;/a&gt;, or install it into Firefox from &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/clean-image-opener/"&gt;here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>End to end testing - Cypress basics</title><link>https://blog.iankulin.com/end-to-end-testing-cypress-basics/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/end-to-end-testing-cypress-basics/</guid><description>&lt;p&gt;When you&amp;rsquo;ve made a change to your web-app, do you run it then click around the new bits to check it works? Good start, but instead of doing that yourself, do it in a faster, more comprehensive and automated way with an end-to-end (E2E) testing setup using &lt;a href="https://www.cypress.io/"&gt;Cypress&lt;/a&gt;. Here&amp;rsquo;s how.&lt;/p&gt;
&lt;h3 id="e2e"&gt;E2E&lt;/h3&gt;
&lt;p&gt;End to End testing is testing your app as a user might - by clicking links, entering data, looking at the screen and checking everything is okay, but it&amp;rsquo;s scripted like a unit test and the results are checked with assertions. Like unit testing this allows you to build up a collection of comprehensive tests that easily detect for unexpected behaviours - not just in the results of functions in your app, but in the user experience of the app.&lt;/p&gt;
&lt;p&gt;In the case of Cypress, this works by running your app in an instrumented browser. The tests are written in JavaScript and might ask things like &amp;ldquo;Click the &amp;lsquo;Home&amp;rsquo; link&amp;rdquo; and have an assertion similar to &amp;ldquo;check the home page loaded&amp;rdquo;. Let&amp;rsquo;s see how that will look.&lt;/p&gt;
&lt;h3 id="how-it-looks"&gt;How it looks&lt;/h3&gt;
&lt;p&gt;In the app I&amp;rsquo;m working on, if you view an individual customer (say at &amp;ldquo;&lt;a href="http://127.0.0.1:3002/customers/1"&gt;http://127.0.0.1:3002/customers/1&lt;/a&gt;&amp;rdquo;) there&amp;rsquo;s a &amp;ldquo;Home&amp;rdquo; link at the top which takes you to the list of customers (at &amp;ldquo;&lt;a href="http://127.0.0.1:3002/customers"&gt;http://127.0.0.1:3002/customers&lt;/a&gt;&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-10.22.34.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the test code:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;describe(&amp;#39;Page Navigation&amp;#39;, () =&amp;gt; {
 it(&amp;#39;should navigate to the customers list when clicking the Home link&amp;#39;, () =&amp;gt; {
 // visit the customer details page
 cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
 
 // find and click the Home link
 cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
 
 // verify we navigated to the customers list page
 cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
 });
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you&amp;rsquo;ve been writing unit tests before, this format will be familiar, but let&amp;rsquo;s look at the steps:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You guessed it - we&amp;rsquo;re telling Cypress to visit that page.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m not sure if Cypress uses &lt;a href="https://jquery.com/"&gt;JQuery&lt;/a&gt;, or just a JQuery like syntax, either way, what we&amp;rsquo;re doing here is selecting the &amp;lsquo;&amp;lt;a &amp;hellip;&amp;gt;&amp;rsquo; tag. Of course our page probably contains several anchor tags, so we&amp;rsquo;re refining this search to the anchor tag that contains &amp;lsquo;Home&amp;rsquo;. Note that there&amp;rsquo;s an implied assertion here. If there is no &lt;a&gt; link on the page containing &amp;lsquo;Home&amp;rsquo;, this test will fail with an error saying something like &amp;ldquo;Expected to find content: &amp;lsquo;Home&amp;rsquo; within the element: &lt;a&gt; but never did.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Finally the &lt;code&gt;click()&lt;/code&gt; at the end of the statement tells Cypress to click this link.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Before we look at this statement, consider that we haven&amp;rsquo;t told Cypress to wait for a bit for the results of our click() to process - one of the benefits of Cypress is it just figures that out magically.&lt;/p&gt;
&lt;p&gt;This statement is an assertion - the URL &lt;code&gt;should&lt;/code&gt; equal (&lt;code&gt;eq&lt;/code&gt;) the URL we&amp;rsquo;ve provided.&lt;/p&gt;
&lt;p&gt;So that gives us a quick overview of a simple test. Naturally Cypress has a heap more operators and assertion types to help us test our application - basically everything you could think of as user-facing testing. Let&amp;rsquo;s look at a simple demo app then work through the tests we might try for this.&lt;/p&gt;
&lt;h3 id="the-app"&gt;The App&lt;/h3&gt;
&lt;p&gt;This app is a simple demo I wrote for an earlier blog post about using the Express router. We have &lt;em&gt;Customers&lt;/em&gt; and &lt;em&gt;Orders&lt;/em&gt;, a single &lt;em&gt;customer&lt;/em&gt; can have zero-many &lt;em&gt;orders&lt;/em&gt;. The opening page is a list of all customers. Clicking on a customer shows the details for that customer, including a list of their orders. Clicking on an order shows the detail for that order, including a link the customer it belongs to.&lt;/p&gt;
&lt;p&gt;The Customer and Order detail views have delete links, and a deletion of a customer should cascade to delete that customer&amp;rsquo;s orders.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-11.50.32-1.png" width="946" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-11.50.22-1.png" width="999" alt=""&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-10.22.34-2.png" alt=""&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-12-at-13.12.28-1.png" width="861" alt=""&gt;
&lt;h3 id="installing-cypress"&gt;Installing Cypress&lt;/h3&gt;
&lt;p&gt;Installing Cypress is straightforward. The install steps from the docs are &lt;a href="https://docs.cypress.io/app/get-started/install-cypress"&gt;here&lt;/a&gt;, but really it&amp;rsquo;s just starting your Node project (so you&amp;rsquo;ve got a package.json) then &lt;code&gt;npm install cypress --save-dev&lt;/code&gt; to add it as a dev dependency. It&amp;rsquo;s a big download so expect it to take a bit. It includes lodash, some AWS stuff, tldts, day.js, a heap of vue stuff - just, it&amp;rsquo;s a lot of big dependencies. Also since Cypress itself does some cool stuff linking into the browser - that functionality requires some code.&lt;/p&gt;
&lt;h3 id="the-tests"&gt;The Tests&lt;/h3&gt;
&lt;p&gt;Actually - the code in our very simple demo above covers about 70% of the testing I do, and the pattern of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Select a user element&lt;/li&gt;
&lt;li&gt;Click it or add some text&lt;/li&gt;
&lt;li&gt;Select another user element to check that worked&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;comes up again and again. So I&amp;rsquo;m going to try not to repeat myself too much. Most of what&amp;rsquo;s new in the following tests will be extra selectors, and assertions. We won&amp;rsquo;t cover all of them, but rather a smattering to get started with.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; // test for customers list page
 describe(&amp;#34;Customers Page&amp;#34;, () =&amp;gt; {
 it(&amp;#34;should have the home page redirect to customers page&amp;#34;, () =&amp;gt; {
 cy.visit(&amp;#34;http://localhost:3002&amp;#34;);
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
 cy.get(&amp;#34;h1&amp;#34;).contains(&amp;#34;Customers&amp;#34;);
 });

 it(&amp;#34;should display a list of customers&amp;#34;, () =&amp;gt; {
 cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
 cy.get(&amp;#34;li&amp;#34;).should(&amp;#34;have.length.at.least&amp;#34;, 5);
 cy.get(&amp;#34;li&amp;#34;).eq(0).contains(&amp;#34;Alice&amp;#34;);
 });

 it(&amp;#34;should have working links to customer details&amp;#34;, () =&amp;gt; {
 cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
 // click the first customer (Alice)
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;).click();
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers/&amp;#34;);
 cy.get(&amp;#34;h2&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;);
 });
 });
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="should"&gt;.should()&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s a &lt;a href="https://docs.cypress.io/api/commands/should"&gt;massive list of should() assertions&lt;/a&gt;, and they depend a bit on what you&amp;rsquo;ve chained on to. In the first example we looked at we used &lt;code&gt;&amp;quot;eq&amp;quot;&lt;/code&gt; for equals, in the example directly above we&amp;rsquo;ve used &lt;code&gt;&amp;quot;include&amp;quot;&lt;/code&gt; for a partial match, and &lt;code&gt;&amp;quot;have.length.at.least&amp;quot;&lt;/code&gt; for what it says on the box.&lt;/p&gt;
&lt;p&gt;Another handy thing might be testing for &lt;code&gt;&amp;quot;not.exist&amp;quot;.&lt;/code&gt; In my example app if I want to test deleting a &lt;em&gt;customer&lt;/em&gt;, I can check they exist in the customers list, click delete, then check that they no longer exist in the list:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; it(&amp;#34;should delete a customer when delete link is clicked&amp;#34;, () =&amp;gt; {
 // first check the customer exists
 cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;exist&amp;#34;);

 // visit the customer page and delete
 cy.visit(&amp;#34;http://localhost:3002/customers/8&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Delete customer&amp;#34;).click();

 // verify the customer is deleted
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;not.exist&amp;#34;);
 });
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="get"&gt;.get()&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ve already seen selecting an anchor tag with get(&amp;ldquo;a&amp;rdquo;) - this will work for any HTML tag, but of course you&amp;rsquo;ll frequently need more specificity than that. As &lt;a href="https://docs.cypress.io/api/commands/get#__docusaurus_skipToContent_fallback"&gt;described in the docs&lt;/a&gt;, most of the JQuery selectors will also work with get.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// Select by element type
cy.get(&amp;#39;button&amp;#39;)

// Select by class
cy.get(&amp;#39;.my-class&amp;#39;)

// Select by ID
cy.get(&amp;#39;#my-id&amp;#39;)

// Combining selectors
cy.get(&amp;#39;button.primary#submit&amp;#39;)

// Select by attribute
cy.get(&amp;#39;[data-test=&amp;#34;submit-button&amp;#34;]&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those first four are straightforward, but you might not know about attributes.&lt;/p&gt;
&lt;h3 id="attributes"&gt;Attributes&lt;/h3&gt;
&lt;p&gt;As part of the HTML specification, tags can have attributes. You&amp;rsquo;ve been using them all along. For example. this button:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;button id=&amp;#34;submit&amp;#34; class=&amp;#34;btn primary&amp;#34; type=&amp;#34;submit&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;has attributes for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id&lt;/li&gt;
&lt;li&gt;class&lt;/li&gt;
&lt;li&gt;type&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These all have particular meanings for HTML, CSS and JavaScript, but actually we can make up our own. For example we could say:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;button type=&amp;#34;submit&amp;#34; data-test=&amp;#34;submit-button&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s no specification for &amp;lsquo;data-test&amp;rsquo;, it&amp;rsquo;s just a convention, we could just have easily said:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;button type=&amp;#34;submit&amp;#34; data-green-zebra=&amp;#34;submit-button&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that I&amp;rsquo;ve kept the &lt;code&gt;data-&lt;/code&gt; prefix - that is part of the &lt;a href="https://www.w3schools.com/tags/att_global_data.asp"&gt;HTML5 specification&lt;/a&gt;. We could probably make up anything and it would work, but maybe it would conflict with something in a future HTML version, so best stick to &amp;ldquo;data-&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Using attributes for specifying the element we want is highly recommended. Although the element you want to click might currently be the third &lt;a&gt; in a &lt;ul&gt; inside the &lt;nav&gt; - it&amp;rsquo;s easy to imagine it being moved in a future update to your app. Once we&amp;rsquo;ve written an E2E test, we want it to continue to work across future app development, and using a &amp;lsquo;data-test&amp;rsquo; attribute supports this.&lt;/p&gt;
&lt;h3 id="invoke-and-then"&gt;.invoke() and then()&lt;/h3&gt;
&lt;p&gt;Sometimes the exact test you need might not be available, or you need to do some operation as part of your testing that requires a bit more processing. In that case, you can chain the &lt;code&gt;invoke()&lt;/code&gt; method. This allows you to call any jQuery method on an element you&amp;rsquo;ve selected with Cypress, letting you extract specific properties or manipulate the element in ways that aren&amp;rsquo;t covered by Cypress&amp;rsquo;s built-in assertions.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve got it, you can use &lt;code&gt;then()&lt;/code&gt; to run an arrow function against it to do something. The pseudo codes looks a bit like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cy.get(selector)
 .invoke(jQueryMethod) // Extract what you need
 .then((result) =&amp;gt; { // Process it with your own logic
 // Custom processing
 });
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let&amp;rsquo;s look at an example. Imagine the HTML of our page looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;span class=&amp;#34;price&amp;#34;&amp;gt;$24.99&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And we want to check that the price was greater than $20 - perhaps we are supposed to have added tax or something. Our test could look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cy.get(&amp;#39;.price&amp;#39;)
 .invoke(&amp;#39;text&amp;#39;)
 .then((priceText) =&amp;gt; {
 const priceValue = parseFloat(priceText.replace(&amp;#39;$&amp;#39;, &amp;#39;&amp;#39;));
 expect(priceValue).to.be.greaterThan(20.0);
 });
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This selects this span based on it&amp;rsquo;s class, then saves the text &amp;lsquo;$24.99&amp;rsquo; to &lt;code&gt;priceText&lt;/code&gt;, extracts the value to a JavaScript number, and asserts it to be greater than 20.&lt;/p&gt;
&lt;p&gt;In my demo app, I use this to check the cascading delete - when we delete the customer, the orders for that customer should also be deleted. Rather than hard code the order number we can use invoke/then to extract it from the text which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;8 - 2025-03-08 - $200&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then, after deleting the customer (and orders) we navigate to the orders page to make sure that order number does not exist there any more.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; describe(&amp;#34;Cascading Deletions&amp;#34;, () =&amp;gt; {
 it(&amp;#34;should delete owned orders when a customer is deleted&amp;#34;, () =&amp;gt; {
 // first make a note of an order for a specific customer
 cy.visit(&amp;#34;http://localhost:3002/customers/4&amp;#34;);
 cy.get(&amp;#34;h2&amp;#34;).contains(&amp;#34;Diana Prince&amp;#34;);

 // note an order ID that belongs to this customer
 cy.get(&amp;#34;ul li a&amp;#34;)
 .first()
 .invoke(&amp;#34;text&amp;#34;)
 .then((orderText) =&amp;gt; {
 // extract the full order text to use for matching later
 const orderTextFull = orderText.trim();
 // extract just the order ID number
 const orderId = orderText.split(&amp;#34; &amp;#34;)[0].trim();

 // delete the customer
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Delete customer&amp;#34;).click();

 // verify customer is deleted
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Diana Prince&amp;#34;).should(&amp;#34;not.exist&amp;#34;);

 // check that the order is also deleted 
 cy.visit(&amp;#34;http://localhost:3002/orders&amp;#34;);
 // make sure we&amp;#39;re matching the exact order (not just a substring)
 cy.get(`a[href=&amp;#34;/orders/${orderId}&amp;#34;]`).should(&amp;#34;not.exist&amp;#34;);
 });
 });
 });
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There is a lot, lot more to Cypress to this, but with what we&amp;rsquo;ve covered here it&amp;rsquo;s possible to write a comprehensive suite of tests that will test all of the functionality in this demo app in &lt;a href="https://github.com/IanKulin/route-demo/blob/main/cypress/e2e/home.cy.js"&gt;about 200 lines&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="using-cypress"&gt;Using Cypress&lt;/h3&gt;
&lt;p&gt;So that&amp;rsquo;s all the code, but how does it look to test like this? For me this is one of the things that makes end-to-end testing cool. I love seeing it&amp;rsquo;s click away in the browser at super speed as my tests turn green.&lt;/p&gt;
&lt;p&gt;First of all I start my app - so however you normally do this. For me, it&amp;rsquo;s dropping to the terminal in VSCode and running it in Node. Something like &lt;code&gt;node index.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s going we can start Cypress. Since the first terminal is running my node app, we need to spawn another terminal to run Cypress in. This is a simple matter in VSCode - just hit that + button I&amp;rsquo;ve circled in the screen shot below. You can swap between the different terminals you have open by clicking on them in the list underneath that + button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.02.23.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We start Cypress in the new terminal with &lt;code&gt;npx cypress open&lt;/code&gt; but the magic does not happen in the terminal, this thing pops up:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.06.png" width="900" alt=""&gt;
&lt;p&gt;We&amp;rsquo;re doing E2E testing, so select that.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.16.png" width="900" alt=""&gt;
&lt;p&gt;I&amp;rsquo;m in a Chrome mood today, so next we see this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.29.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve only got one test file - &lt;code&gt;home.cy.js&lt;/code&gt;, so I click that. The tests are listed down the left side of the browser, and my app in an iFrame to the right. As the tests are running, I can see the app flicking through each step. In a couple of seconds the seventeen tests that comprise many page manipulations and assertions are finished and I can see the results.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.14.27.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If we click on a test, the details for it open up, and a screenshot of the application state at the time of that test is displayed.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.16.52.png" width="900" alt=""&gt;
&lt;p&gt;The most common debugging problem I&amp;rsquo;ve run into is when I didn&amp;rsquo;t write the selection correctly (and didn&amp;rsquo;t use a data- attribute). These are easily checked in this view by hovering over the one we&amp;rsquo;re interested in - the element that Cypress used in this step will be highlighted.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.20.34.png" width="900" alt=""&gt;
&lt;p&gt;So, how does a failed test look. I can create that in this test suit just by running the tests again. It won&amp;rsquo;t be able to delete orders or customers it deleted in the earlier run.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.24.56.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So, at the start of this &amp;lsquo;delete order&amp;rsquo; test, I&amp;rsquo;m checking if the order exists, and it doesn&amp;rsquo;t (because we didn&amp;rsquo;t reset after deleting it last time). We can see from the error message that Cypress waited 4 seconds in case it was a timing issue. It&amp;rsquo;s displayed the test case where the failure has occurred. This along with the before and after snapshots of the app around each test make locating problems a breeze.&lt;/p&gt;
&lt;h3 id="resets"&gt;Resets&lt;/h3&gt;
&lt;p&gt;The pattern above (where you run a test twice and it fails the second time because the first execution changed the state) is common. To avoid this, we need some system of resetting the state. Cypress has a mocha like &amp;lsquo;beforeEach&amp;rsquo; ability. You most always need this for logging things in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;describe(&amp;#39;My app tests&amp;#39;, () =&amp;gt; {
 beforeEach(() =&amp;gt; {
 // This code runs before each test in this block
 cy.visit(&amp;#39;/login&amp;#39;); // for example
 cy.get(&amp;#39;input[name=username]&amp;#39;).type(&amp;#39;user&amp;#39;);
 cy.get(&amp;#39;input[name=password]&amp;#39;).type(&amp;#39;password&amp;#39;);
 cy.get(&amp;#39;button[type=submit]&amp;#39;).click();
 });

 it(&amp;#39;should show the dashboard after login&amp;#39;, () =&amp;gt; {
 cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/dashboard&amp;#39;);
 cy.contains(&amp;#39;Welcome&amp;#39;).should(&amp;#39;be.visible&amp;#39;);
 });

 it(&amp;#39;should navigate to settings&amp;#39;, () =&amp;gt; {
 cy.get(&amp;#39;nav&amp;#39;).contains(&amp;#39;Settings&amp;#39;).click();
 cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/settings&amp;#39;);
 });
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But for apps that need things like a fresh database before testing, it&amp;rsquo;s a bit trickier. In the past I&amp;rsquo;ve sometimes created some sort of /test-reset endpoint which feels like an unreasonable security risk. The proper answer is to shell out with a Cypress task. That way we can do things like copy in test data, or spin up a whole test environment in a container. These are meaty topics for another post - but really, our tests should be re-run-able, and run-able in any order so we might come back to that.&lt;/p&gt;
&lt;h3 id="test"&gt;Test&lt;/h3&gt;
&lt;p&gt;Any testing is better than none, and if you use these sorts of tools that make it easier you&amp;rsquo;ll find you&amp;rsquo;ll add to them, especially when errors crop up. If I sit down to add end-to-end tests to an existing app, I nearly always find things I want to change to make it better. Use end to end testing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/route-demo"&gt;code&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Express router for better code organisation</title><link>https://blog.iankulin.com/express-router-for-better-code-organisation/</link><pubDate>Mon, 28 Apr 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/express-router-for-better-code-organisation/</guid><description>&lt;p&gt;A Node/Express app I&amp;rsquo;m working on has been sprouting routes so much that the &lt;code&gt;server.js&lt;/code&gt; file has swollen to 800 lines - way past my 200-250 comfort zone, so it&amp;rsquo;s time to organise the routes into their own files. That seems like a good topic for a beginner blog post, so let&amp;rsquo;s dive in.&lt;/p&gt;
&lt;p&gt;Imagine we&amp;rsquo;ve written this little Node/Express app.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import express from &amp;#34;express&amp;#34;;
import {
 dbCustomersGet,
 dbCustomersGetById,
 dbCustomersDelete,
 dbOrdersGet,
 dbOrdersGetById,
 dbOrdersGetByCustomerId,
 dbOrdersDelete,
} from &amp;#34;./db.js&amp;#34;;

const app = express();
app.set(&amp;#34;view engine&amp;#34;, &amp;#34;ejs&amp;#34;);
const port = 3002;

app.use(express.urlencoded({ extended: true }));

app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 res.redirect(&amp;#34;/customers&amp;#34;);
});

app.get(&amp;#34;/customers&amp;#34;, (req, res) =&amp;gt; {
 const customers = dbCustomersGet();
 res.render(&amp;#34;customers&amp;#34;, { customers });
});

app.get(&amp;#34;/customers/:id&amp;#34;, (req, res) =&amp;gt; {
 const customer = dbCustomersGetById(req.params.id);
 const orders = dbOrdersGetByCustomerId(req.params.id);
 res.render(&amp;#34;customer&amp;#34;, { customer, orders });
});

app.get(&amp;#34;/customers/:id/delete&amp;#34;, (req, res) =&amp;gt; {
 dbCustomersDelete(req.params.id);
 res.redirect(&amp;#34;/customers&amp;#34;);
});

app.get(&amp;#34;/orders&amp;#34;, (req, res) =&amp;gt; {
 const orders = dbOrdersGet();
 res.render(&amp;#34;orders&amp;#34;, { orders });
});

app.get(&amp;#34;/orders/:id&amp;#34;, (req, res) =&amp;gt; {
 const order = dbOrdersGetById(req.params.id);
 const customer = dbCustomersGetById(order.customerId);
 res.render(&amp;#34;order&amp;#34;, { order, customer });
});

app.get(&amp;#34;/orders/:id/delete&amp;#34;, (req, res) =&amp;gt; {
 dbOrdersDelete(req.params.id);
 res.redirect(&amp;#34;/orders&amp;#34;);
});

app.listen(port, () =&amp;gt; {
 console.log(`Listening on http://127.0.0.1:${port}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Although concocted, this would seem familiar to anyone who&amp;rsquo;s built a CRUD business app.&lt;/p&gt;
&lt;p&gt;One thing I&amp;rsquo;ve done better here than in the real app I&amp;rsquo;m fixing is that the routes are carefully named - all the &amp;lsquo;orders&amp;rsquo; routes begin with &lt;code&gt;/orders&lt;/code&gt;, all the &amp;lsquo;customers&amp;rsquo; routes with &lt;code&gt;/customers&lt;/code&gt;. As we&amp;rsquo;ll see, this is going to make separating them out much easier.&lt;/p&gt;
&lt;h3 id="express-router"&gt;Express Router&lt;/h3&gt;
&lt;p&gt;Like almost everything in Express, the router is middleware. Let&amp;rsquo;s look at how our index.js has changed once we&amp;rsquo;ve moved the routes out into a &lt;code&gt;customers.js&lt;/code&gt; and an &lt;code&gt;orders.js&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import express from &amp;#34;express&amp;#34;;
import customersRouter from &amp;#34;./routes/customers.js&amp;#34;;
import ordersRouter from &amp;#34;./routes/orders.js&amp;#34;;

const app = express();
app.set(&amp;#34;view engine&amp;#34;, &amp;#34;ejs&amp;#34;);
const port = 3002;

app.use(express.urlencoded({ extended: true }));

// routers
app.use(&amp;#34;/customers&amp;#34;, customersRouter);
app.use(&amp;#34;/orders&amp;#34;, ordersRouter);

// root route redirect to customers
app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 res.redirect(&amp;#34;/customers&amp;#34;);
});

app.listen(port, () =&amp;gt; {
 console.log(`Listening on http://127.0.0.1:${port}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So much neater!&lt;/p&gt;
&lt;p&gt;First of all, the imports for all my database functions are gone - they&amp;rsquo;ll be in the files for our two routes.&lt;/p&gt;
&lt;p&gt;There are a couple of new imports though - our two &amp;lsquo;routers&amp;rsquo;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import customersRouter from &amp;#34;./routes/customers.js&amp;#34;;
import ordersRouter from &amp;#34;./routes/orders.js&amp;#34;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then further down, they are installed as middleware:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// routers
app.use(&amp;#34;/customers&amp;#34;, customersRouter);
app.use(&amp;#34;/orders&amp;#34;, ordersRouter);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can pretty much see from this code how this works. Any routes that begin with &amp;ldquo;/customers&amp;rdquo; are sent off to the &lt;code&gt;customersRouter&lt;/code&gt; which we&amp;rsquo;ve imported from &lt;code&gt;&amp;quot;./routes/customers.js&amp;quot;&lt;/code&gt;, and the routes for &amp;ldquo;/orders&amp;rdquo; go to the &lt;code&gt;ordersRouter&lt;/code&gt;. Any route requests that don&amp;rsquo;t match those will be sought in the main file where the app is declared.&lt;/p&gt;
&lt;p&gt;You might have noticed how we&amp;rsquo;re organising the routes - there&amp;rsquo;s a &amp;ldquo;routes&amp;rdquo; folder and they&amp;rsquo;re dropped in there. That&amp;rsquo;s not a requirement, but it&amp;rsquo;s a common convention.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-04-at-20.42.50.png" width="800" alt=""&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at one of the route files:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import express from &amp;#34;express&amp;#34;;
import {
 dbCustomersGet,
 dbCustomersGetById,
 dbCustomersDelete,
 dbOrdersGetByCustomerId,
} from &amp;#34;../db.js&amp;#34;;

const router = express.Router();

// GET /customers
router.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 const customers = dbCustomersGet();
 res.render(&amp;#34;customers&amp;#34;, { customers });
});

// GET /customers/:id
router.get(&amp;#34;/:id&amp;#34;, (req, res) =&amp;gt; {
 const customer = dbCustomersGetById(req.params.id);
 const orders = dbOrdersGetByCustomerId(req.params.id);
 res.render(&amp;#34;customer&amp;#34;, { customer, orders });
});

// GET /customers/:id/delete
router.get(&amp;#34;/:id/delete&amp;#34;, (req, res) =&amp;gt; {
 dbCustomersDelete(req.params.id);
 res.redirect(&amp;#34;/customers&amp;#34;);
});

export default router;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is nice. We&amp;rsquo;re only importing the customer database functions, and we&amp;rsquo;ve got all the customer routes in one place in an easily comprehensible file.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s really only one gotcha here which we alluded to earlier. You&amp;rsquo;ll notice how I&amp;rsquo;ve added a comment over each route?&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// GET /customers/:id
router.get(&amp;#34;/:id&amp;#34;, (req, res) =&amp;gt; {
 const customer = dbCustomersGetById(req.params.id);
 const orders = dbOrdersGetByCustomerId(req.params.id);
 res.render(&amp;#34;customer&amp;#34;, { customer, orders });
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is because in the process of specifying that this file deals with all the &amp;ldquo;/customers&amp;rdquo; routes, that part of the request URL has been stripped off - so a call to &lt;code&gt;http://127.0.0.1:3002/customers/5&lt;/code&gt; arrives here as &lt;code&gt;/5&lt;/code&gt;. It&amp;rsquo;s another common practice to put the route path in a comment as I&amp;rsquo;ve done here as a reminder to myself. I wish the Express team had just left the requests unaltered.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Really, that&amp;rsquo;s about all there is to using the Express Router to split your routes out into files; it&amp;rsquo;s quite straightforward. A good naming convention for your routes so that logical groups of routes all start with the same specifier will be a great help.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/route-demo"&gt;Code on GitHub&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Functional Javascript array methods</title><link>https://blog.iankulin.com/functional-javascript-array-methods/</link><pubDate>Mon, 14 Apr 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/functional-javascript-array-methods/</guid><description>&lt;p&gt;I&amp;rsquo;ve been whipping up a little mock-database unit that has a few access functions but actually stores the data as arrays for a demo project for a post I&amp;rsquo;m writing. In the process I wrote this gem:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;export function dbOrdersAdd(order) {
 const orderCopy = { ...order };
 // since id is a stringified number, finding the max is a bit of a mess
 const maxId = orders.reduce((max, o) =&amp;gt; Math.max(max, parseInt(o.id)), 0);
 orderCopy.id = String(maxId + 1);
 orders.push(orderCopy);
 return { ...orderCopy };
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the comment I&amp;rsquo;m claiming the code is a bit of a mess (and from a readability point that&amp;rsquo;s true) but actually I love the elegance of using the &lt;code&gt;reduce()&lt;/code&gt; method here.&lt;/p&gt;
&lt;p&gt;It also occurred to me, that a year or so ago, these functional array methods were completely novel to me. So I thought it my be interesting to talk about &lt;code&gt;reduce()&lt;/code&gt;, and why I&amp;rsquo;m calling it a &amp;ldquo;functional&amp;rdquo; method.&lt;/p&gt;
&lt;h2 id="reduce"&gt;Reduce&lt;/h2&gt;
&lt;p&gt;If you think of all the things you&amp;rsquo;re likely to do with a collection like an array, the most common thing is to iterate over it. Javascript has you covered - with the &lt;code&gt;forEach()&lt;/code&gt; method. This just executes the callback function you pass:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[1, 2, 3].forEach(x =&amp;gt; console.log(x));
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s super handy, and gets used a lot. But what if I wanted to do something like summing all the values in an array. In the olden days we might write something like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const numbers = [1, 2, 3];
let sum = 0; 
for (let i = 0; i &amp;lt; numbers.length; i++) {
 sum = sum + numbers[i];
}
console.log(sum); // 6
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But all the cool young things be like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const numbers = [1, 2, 3];
const sum = numbers.reduce((acc, num) =&amp;gt; acc + num, 0);
console.log(sum); // 6
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;reduce() is doing a little bit more work for us than forEach(). It is a machine for &amp;ldquo;reducing&amp;rdquo; the values in an array to a single number. It takes two parameters. The first one is a function, and the second is the starting value for the &amp;lsquo;accumulator&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;In our function above &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt;, &amp;lsquo;acc&amp;rsquo; is used by reduce() as the accumulator, and &amp;rsquo;num&amp;rsquo; is the value from the array. So the steps being carried out in the code above are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set acc = 0&lt;/li&gt;
&lt;li&gt;apply &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt; where num is 1 - so 0+1=1, acc is now equal to 1&lt;/li&gt;
&lt;li&gt;apply &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt; where num is 2 - so 1+2=3, acc is now 3&lt;/li&gt;
&lt;li&gt;apply &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt; where num is 3 - so 2+3=6, acc is now 6&lt;/li&gt;
&lt;li&gt;save that into &lt;code&gt;sum&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If we wanted to find the max in an array of numbers we could:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const numbers = [1, 2, 3];
const maxnum = numbers.reduce((acc, num) =&amp;gt; Math.max(acc, num), 0);
console.log(maxnum); // 3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Very nice!&lt;/p&gt;
&lt;h2 id="functional-programming"&gt;Functional Programming&lt;/h2&gt;
&lt;p&gt;There are a million explanations about what functional programming is and what it&amp;rsquo;s strengths are, so for here, let&amp;rsquo;s just summarise it as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;functions are &amp;lsquo;pure&amp;rsquo; - ie they have no side effects, and access and set no values from outside the function. Because of this, the same inputs always result in a particular output. They are fully encapsulated.&lt;/li&gt;
&lt;li&gt;The data passed in, and returned is immutable (data in general is immutable)&lt;/li&gt;
&lt;li&gt;Lot&amp;rsquo;s of functions - functions as &amp;lsquo;first-class-citizens&amp;rsquo;, functions passed into functions. It&amp;rsquo;s function focused - hence the name.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The strengths I like about a functional approach is it&amp;rsquo;s high unit-test-ability, and the elegance of passing a function as a parameter to broaden the scope of a function.&lt;/p&gt;</description></item><item><title>Manually adding SSL certs in Nginx Proxy Manager</title><link>https://blog.iankulin.com/manually-adding-ssl-certs-in-nginx-proxy-manager/</link><pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/manually-adding-ssl-certs-in-nginx-proxy-manager/</guid><description>&lt;p&gt;A large part of the reason for my use of Nginx Proxy manager over vanilla NGINX, is that it has built-in Let&amp;rsquo;s Encrypt certificate requesting and renewing. This works perfectly for all my public facing services, and until recently, my homelab services. Before I dive into how I&amp;rsquo;ve fixed the problem I ran into, I better explain how my homelab domain is set up, and before I do that, an over-simplified description of how the SSL system works is required&lt;/p&gt;
&lt;h3 id="ssl"&gt;SSL&lt;/h3&gt;
&lt;p&gt;SSL (Secure Socket Layer) on a web site (the little padlock you see in the browser when you visit an https:// site) does three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It tells you that this web site you&amp;rsquo;ve visited is controlled by the same people who own the domain name. This is important to prevent someone hijacking your request to &amp;ldquo;mysecurebank.com&amp;rdquo; and sending it to their password stealing website.&lt;/li&gt;
&lt;li&gt;It encrypts the traffic between the web-browser and the website so it can&amp;rsquo;t be spied on.&lt;/li&gt;
&lt;li&gt;It detects is someone has tried to tamper with the traffic between the web-browser and the website.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For all this to work, there needs to be some way of assuring the certificate issuer that the entity claiming a certificate for the domain, has control of the website that the domain is pointing to. The simplest way to do this is for the certificate issuer to give you a token, you install that in a secret directory on the web server, then the certificate issuer can check it exists. This proves to them that you own the website, and they can issue you the certificate.&lt;/p&gt;
&lt;p&gt;This process is essentially what happens when you use &lt;a href="https://certbot.eff.org/"&gt;Certbot&lt;/a&gt; to obtain a &lt;a href="https://letsencrypt.org/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; SSL certificate. It works great as long as your website is public to the internet so it can be contacted by the certificate issuer. But, that&amp;rsquo;s not the case for my homelab services. They are on an internal network, deliberately not contactable from the wild internet.&lt;/p&gt;
&lt;h3 id="ssl-on-an-internal-network"&gt;SSL on an internal network&lt;/h3&gt;
&lt;p&gt;There is less need for SSL on an internal network. You probably assume there&amp;rsquo;s no one to spy on your traffic, or to fiddle with it, and you know you have control of your apps. Apart from not trusting that, there&amp;rsquo;s a couple of other benefits - you often can&amp;rsquo;t save your passwords for unsecured sites, you can get annoying warning messages, and some apps just straight up won&amp;rsquo;t let you use some functionality without it. This is the case for my Forgejo instance that wants working SSL to allow me to git push to it.&lt;/p&gt;
&lt;h3 id="homelab-ssl"&gt;Homelab SSL&lt;/h3&gt;
&lt;p&gt;There is a way around this - basically we need to assure the certificate issuer that we&amp;rsquo;re in control, but instead of exposing a token on the (unreachable) web server, we add it as a record in the DNS of the domain (called DNS Challenge). The logic of this is that if you control the DNS you control where it points and therefore the web server. This is the first part of the Homelab SSL setup - obtaining the certificate with DNS challenge. The second part is using the DNS to point the domain at an internal website address. My domain and DNS are public - you could enter the domain name in a browser, but it resolves to an address inside my network. This is all much better explained by Wolfgang than me.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/qlcVx-k-02E?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;h3 id="the-problem-ive-run-into"&gt;The problem I&amp;rsquo;ve run into&lt;/h3&gt;
&lt;p&gt;This all worked perfectly when I first set it up. Nginx Proxy Manager (NPM) has a plugin for my domain provider (&lt;a href="http://porkbun.com"&gt;porkbun&lt;/a&gt;) which uses their API to save the token into my DNS settings. The UX for this is that I tell Nginx Proxy Manager that I want to use &amp;ldquo;DNS Challenge&amp;rdquo; for a certificate request, and (by using an API key I&amp;rsquo;ve setup on Porkbun and given to NPM) it does all the fiddling around to obtain the certificate then installs them.&lt;/p&gt;
&lt;p&gt;I must of had that running for a year or so, with the certificates magically being renewed every couple of months with no input from my until just recently. I&amp;rsquo;m not exactly sure what&amp;rsquo;s happened - the error messages that I&amp;rsquo;m not smart enough to sort out suggest that the plugin&amp;rsquo;s operations to install the token at the domain provider is not working. I don&amp;rsquo;t know if it&amp;rsquo;s an API problem, or there&amp;rsquo;s been an NPM update that&amp;rsquo;s broken the plugin, or just something else has changed in my setup. What ever it is, turning everything off and on again, updating everything, and trying manually have not worked. So time for plan B.&lt;/p&gt;
&lt;h3 id="manual-certificates"&gt;Manual Certificates&lt;/h3&gt;
&lt;p&gt;Porkbun (and for all I know other domain sellers) provide a facility to download a &amp;lsquo;certificate bundle&amp;rsquo; directly from them - they are just doing that Let&amp;rsquo;s Encrypt dance directly - missing the NPM and Porkbun API step from the above.&lt;/p&gt;
&lt;p&gt;The certificate bundle contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;public.key.pem&lt;/li&gt;
&lt;li&gt;private.key.pem&lt;/li&gt;
&lt;li&gt;domain.cert.pem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And when you go to add a custom certificate in NPM you have these options:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-03-08-at-15.47.04.png" width="981" alt=""&gt;
&lt;p&gt;As you can see your certificate (domain.cert.pem) goes in the certificate slot, and the Certificate Key it&amp;rsquo;s asking for is your private key (private.key.pem). You don&amp;rsquo;t need the intermediate key - this is the same for all Let&amp;rsquo;s Encrypt certificates.&lt;/p&gt;
&lt;p&gt;Doing the certificates this way is less good than having them automatically renewed. Currently Let&amp;rsquo;s Encrypt certificates are good for 90 days, so every three months my monitoring system will let me know they only have a couple of weeks left and I&amp;rsquo;ll have to repeat this manual process. There has been talk of shortening this time which would make that process even more annoying, so hopefully I can sort out the issue in NPM, or find out if Traefik or Caddy have the necessary plugins to do DNS challenge certificates.&lt;/p&gt;
&lt;p&gt;But in the meantime, my internal web apps are all up and secure.&lt;/p&gt;
&lt;h3 id="this-is-not-free"&gt;This is not free&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s Encrypt, and to a lesser extent Certbot have changed the web substantially. Obtaining and installing certificates used to be a difficult and costly process, but these two &amp;lsquo;free&amp;rsquo; services have turned that around. If you run a website and use these services I highly recommend you support the non-profits that keep them in existence as I do.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt - &lt;a href="https://letsencrypt.org/donate/"&gt;have a donation page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Certbot - is provided by the EFF who do other work can be &lt;a href="https://supporters.eff.org/donate/support-work-on-certbot"&gt;donated to here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Node.js built in test runner</title><link>https://blog.iankulin.com/node-js-built-in-test-runner/</link><pubDate>Mon, 17 Mar 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/node-js-built-in-test-runner/</guid><description>&lt;p&gt;For the longest time, I&amp;rsquo;ve been using &lt;a href="https://mochajs.org/"&gt;Mocha&lt;/a&gt; (test runner) and &lt;a href="https://www.chaijs.com/"&gt;Chai&lt;/a&gt; (assertion library) for my JS testing. They are reliable old friends.&lt;/p&gt;
&lt;p&gt;One of the effects of the existence of &lt;a href="https://bun.sh/"&gt;Bun&lt;/a&gt; and &lt;a href="https://deno.com/"&gt;Deno&lt;/a&gt; has been to spur Node onto adding some new features, so after appearing as an experimental feature in 18, the Node test runner dropped in Node 20.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if the familiar unit test layout of Mocha and Node is inherited from Jest, or comes from older testing frameworks of which JUnit and NUnit were the first ones I&amp;rsquo;d ever used. Before that I just used to write tests as lumps of assertions in regular code - which worked but wasn&amp;rsquo;t as pleasant to use as a proper unit test setup. Regardless, the system of bundling a few tests together and having them all run and spit out green ticks is not a new one.&lt;/p&gt;
&lt;p&gt;If you are coming from Mocha, there are very few changes to your practice to make. I didn&amp;rsquo;t read &lt;a href="https://nodejs.org/api/test.html#skipping-tests"&gt;the docs&lt;/a&gt; to check that I had to begin my test files with &amp;rsquo;test&amp;rsquo; or to put them into a &amp;rsquo;test&amp;rsquo; directory. But I discovered that dragging them into a &amp;rsquo;test-dont&amp;rsquo; directory didn&amp;rsquo;t stop them from running.&lt;/p&gt;
&lt;h3 id="testing"&gt;Testing&lt;/h3&gt;
&lt;p&gt;As well as what ever you&amp;rsquo;re testing, you need to import a couple of things from node:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { isEven } from &amp;#34;../index.js&amp;#34;;
import { describe, it } from &amp;#34;node:test&amp;#34;;
import assert from &amp;#34;node:assert&amp;#34;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we can write our tests, grouping them in a describe:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;describe(&amp;#34;isEven&amp;#34;, () =&amp;gt; {
 it(&amp;#34;returns true for even numbers&amp;#34;, (t) =&amp;gt; {
 assert.strictEqual(isEven(4), true);
 assert.strictEqual(isEven(0), true);
 assert.strictEqual(isEven(-2), true);
 });

 it(&amp;#34;returns false for odd numbers&amp;#34;, (t) =&amp;gt; {
 assert.strictEqual(isEven(7), false);
 assert.strictEqual(isEven(-3), false);
 });

 it(&amp;#34;returns false for most floating point numbers&amp;#34;, (t) =&amp;gt; {
 assert.strictEqual(isEven(3.5), false);
 // sadly, this is true because JavaScript
 // assert.strictEqual(isEven(4.0000000000000000000001), false);
 });

 it(&amp;#34;returns false for non-numbers&amp;#34;, (t) =&amp;gt; {
 assert.strictEqual(isEven(&amp;#34;a&amp;#34;), false);
 assert.strictEqual(isEven(null), false);
 assert.strictEqual(isEven(undefined), false);
 });
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then to run them, just &lt;code&gt;node --test&lt;/code&gt; and we&amp;rsquo;ll get a nice summary&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-03-07-at-20.15.43.png" width="800" alt=""&gt;
&lt;p&gt;Of course there are a heap of other &lt;a href="https://nodejs.org/api/assert.html"&gt;assertions&lt;/a&gt;, as well as stuff to set-up and tear-down and to &lt;a href="https://nodejs.org/en/learn/test-runner/mocking"&gt;mock things such as times&lt;/a&gt;. What I&amp;rsquo;ve shown here is very much a getting started, but it also deals with about 80% of my testing needs.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not entirely sure how I feel about moving to a built in test runner. The small convenience I gain has to be weighed up against a small amount of lock in to a run time. I haven&amp;rsquo;t yet been tempted to Deno or Bun, but I love that they exist and are spurring on innovation. Possibly I&amp;rsquo;ll continue with portable testing tools for big projects, but for little ones use the built in.&lt;/p&gt;</description></item><item><title>Where I'm up to with AI for coding</title><link>https://blog.iankulin.com/where-im-up-to-with-ai-for-coding/</link><pubDate>Mon, 03 Mar 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/where-im-up-to-with-ai-for-coding/</guid><description>&lt;p&gt;There&amp;rsquo;s still plenty of controversy about LLMs for coding, and not without reason. But I thought I&amp;rsquo;d run through what I&amp;rsquo;ve tried, and where I&amp;rsquo;ve landed for using AI. Also what the pitfalls are, where it&amp;rsquo;s useful and how it&amp;rsquo;s changed my practice.&lt;/p&gt;
&lt;h3 id="issues"&gt;Issues&lt;/h3&gt;
&lt;h5 id="training-data"&gt;Training data&lt;/h5&gt;
&lt;p&gt;The training data for large language models generally is problematic. There&amp;rsquo;s no doubt that they have been trained on copyright material. With code it&amp;rsquo;s slightly less murky since there is a high availability of good quality open source data with attached licenses to train models on. No doubt this include code written by people who don&amp;rsquo;t approve of it being used by AI, but I think the popular reading of most open source licenses is that using it for training is fine.&lt;/p&gt;
&lt;h4 id="accuracy"&gt;Accuracy&lt;/h4&gt;
&lt;p&gt;Another area where AI code is better than other AI use is in verifiability. It&amp;rsquo;s possible to write good tests to verify a lot of software behaviour. This somewhat negates the problem of hallucinations.&lt;/p&gt;
&lt;h4 id="energy-use"&gt;Energy Use&lt;/h4&gt;
&lt;p&gt;Energy use is an issue I don&amp;rsquo;t really have an answer for. When IT companies are investigating owning their own power stations that&amp;rsquo;s a clear sign that this is a problem that the experts expect to get worse than better. I&amp;rsquo;ve lived through so many IT bubbles now that I&amp;rsquo;m sure that the hype around AI will die down somewhat and there won&amp;rsquo;t be VC money for adding AI to products to make them worse in a few years. Hopefully, AI will be left running in the areas only where it&amp;rsquo;s genuinely helpful like most of the previous IT fashions.&lt;/p&gt;
&lt;p&gt;I also have a growing suspicion that we might have got to the end of the performance gains of making models bigger. Surely by now all of the data that can be gobbled up has been, and the improvements seem to be coming in smaller steps. I imagine future gains won&amp;rsquo;t involve making models bigger, but integrating them into tasks more effectively or building them to be more focused.&lt;/p&gt;
&lt;p&gt;Nevertheless, for the moment, the power usage, especially for training, and especially that the US energy mix now looks like it&amp;rsquo;s moving away from renewables, is my main concern about AI use.&lt;/p&gt;
&lt;h4 id="leaking-data"&gt;Leaking Data&lt;/h4&gt;
&lt;p&gt;Another issue is leaking data. This does not overly affect me since I open source my code anyway, but anyone using it in a real job would have to be following policy on this which in most cases would be - don&amp;rsquo;t use it. There are a couple of problems related to the AI vacuuming up all it&amp;rsquo;s context from everything in your projects that does worry me - Because I&amp;rsquo;m so comfortable in VS Code and git, I keep all my work notes as markdown and manage them in VS Code, and I also use plain text accounting (BeanCount). I don&amp;rsquo;t want any of that data heading out into the AI behemoths, so I&amp;rsquo;m constantly turning the plugins off and on.&lt;/p&gt;
&lt;p&gt;It is possible to use local models, especially if you&amp;rsquo;re on a Mac. I&amp;rsquo;ve used &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=Continue.continue"&gt;Continue&lt;/a&gt; plugin for code completion and kept my data to myself. More about this experience later.&lt;/p&gt;
&lt;h3 id="what-ive-tried"&gt;What I&amp;rsquo;ve tried&lt;/h3&gt;
&lt;p&gt;I used Github Copilot for the trial period and was so impressed with it I paid for the service for a couple of months. This was mainly for code completion although I did use the chat a bit - it just wasn&amp;rsquo;t as comfortable in the editor.&lt;/p&gt;
&lt;p&gt;I switched to &lt;a href="https://codeium.com/"&gt;Codeium&lt;/a&gt; after hearing Kevin Howe on a &lt;a href="https://syntax.fm/show/728/ai-superpowers-with-kevin-hou-and-codeium"&gt;Syntax episode&lt;/a&gt;. For code, this seems right on par with my (now outdated) experience of Github Copilot. Copilot did seem a bit better at figuring things out from the context though - for example my plain text accounting format is probably not in the training data for either service, but when I was letting it they both would produce suggestions in the correct format, but Copilot was making better suggestions. For example it would suggest an expense was for fuel if the payee was a petrol station who appeared elsewhere in my current file.&lt;/p&gt;
&lt;p&gt;I then discovered Ollama, and with an M1 MacBook it&amp;rsquo;s a really simple matter to just pull models down and play with them. Mostly at the command line, but I did use &lt;a href="https://github.com/open-webui/open-webui"&gt;Open Web UI&lt;/a&gt; a bit for a more ChatGPT like experience. I played around with trying to do RAG via Open Web UI but with poor results.&lt;/p&gt;
&lt;p&gt;Using Ollama (which provides a REST type API to your models) I switched to the Continue VS Code plugin so I could do code-completion locally. This worked fine, but, 1) it was a bit slower than Copilot or Codeium. Only by a bit, but the difference was it was thinking slower than me, so I would have to wait for it, whereas with the big online services I was constantly typing over their suggestions, so I gave up on it. If my current M1 MacBook dies I&amp;rsquo;ll buy an M4 and try this again.&lt;/p&gt;
&lt;p&gt;I have used, and continue to use, a combination of Claude, ChatGPT, V0, and DeepSeek Coder in the web browser chat modes. In fact, this is probably my main use. I don&amp;rsquo;t pay for any of them (thank you venture capitalists) and just move across to a different one when I run out of free queries.&lt;/p&gt;
&lt;p&gt;Most of this use is the sort of questions you might ask your mates at work - how would you tackle this? what a good library for? what do you think of this approach? can you have a look over my code and suggest improvements? Working in webchat mode reduces the context available (compared to your entire project) but I&amp;rsquo;ve grown to actually prefer the tight control it gives me when I&amp;rsquo;m asking specific code questions.&lt;/p&gt;
&lt;h3 id="how-i-use-it-now"&gt;How I use it now&lt;/h3&gt;
&lt;p&gt;I use Codeium via its VS Code plugin for code completion. Sometimes this is amazing - it spits out what&amp;rsquo;s in your head, and follows your naming conventions etc. Other times it doesn&amp;rsquo;t and I just keep typing.&lt;/p&gt;
&lt;p&gt;What it&amp;rsquo;s really good at is anything repetitive. I especially love it for tests, once I&amp;rsquo;ve written a couple of tests against edge cases in my code, it gets the flavour of what I want and starts writing good ones, including some I wouldn&amp;rsquo;t have thought of which is gold. This is often a tab, tab, tab, exercise.&lt;/p&gt;
&lt;p&gt;I spend a lot of time in long form conversations in the web interfaces of the major chatbots. Usually this is quite fruitful. I often get it to generate code, or to add behaviours to code I&amp;rsquo;ve given it which I then transfer over manually. If it gets into a muddle, I usually clear it&amp;rsquo;s memory and start a new chat or move over to a different service. Having the wrong ideas or code in the context seems to lead to a chain of stupider and stupider attempts to fix the symptoms of a problem rather than going back and identifying it. It&amp;rsquo;s possible that my fresh explanation of what I&amp;rsquo;m trying to do, the code I&amp;rsquo;ve got and what the issue is is also helpful in this restart.&lt;/p&gt;
&lt;h3 id="how-its-changed-my-style"&gt;How it&amp;rsquo;s changed my style&lt;/h3&gt;
&lt;p&gt;With any tool, using it well involves understanding it&amp;rsquo;s strengths and leaning into them. AI is no different, and here&amp;rsquo;s the things I do to help it help me, or things that it&amp;rsquo;s made possible.&lt;/p&gt;
&lt;p&gt;The first change has just been to improve my craft in ways I should have been otherwise, but as a solo developer you can let slide. This is stuff like clear comments, thoughtful descriptive names, and good separation of ideas. This helps the AI as much as it would help someone reviewing your code, or future you when you come back to maintain it. I like my files to be smaller than I used to. 500 lines is a guideline for me.&lt;/p&gt;
&lt;p&gt;I already liked old and popular tech before, but now I really like it. Think of the difference of the training corpus for Node/Express vs the latest iteration of SveltKit V2. You just get better answers and suggestions for things the AI knows better.&lt;/p&gt;
&lt;p&gt;The last change is that I&amp;rsquo;m much more likely to change to an appropriate library or technology. The annoying friction of not knowing the exact syntax for things disappears since the AI can generate code with correct syntax for me. It makes my programming skills much more portable. Of course you need to invest in some of the high level understandings to know what you should want to do, but once you know that, you don&amp;rsquo;t need to know what to type to achieve that in the way you did a couple of years ago.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m sure I should know better how to regex, and to remember the common ffmpeg or rsync flags, but I&amp;rsquo;m never going back to spend time on those jobs!&lt;/p&gt;</description></item><item><title>A bit of web-scraping with Cheerio</title><link>https://blog.iankulin.com/a-bit-of-web-scraping-with-cheerio/</link><pubDate>Mon, 17 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/a-bit-of-web-scraping-with-cheerio/</guid><description>&lt;p&gt;I had an idea for a little holiday project that required a list of episodes from &lt;a href="https://therestishistory.supportingcast.fm/"&gt;The Rest Is History&lt;/a&gt; podcast. On their &amp;lsquo;Episodes&amp;rsquo; page, they have a player, and a list of post entries for the most recent eighteen podcasts. There is a &amp;lsquo;show all&amp;rsquo; button, but it doesn&amp;rsquo;t work.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2025-01-05-at-8.47.03-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2025-01-05-at-8.47.03-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The player does contain the full list of episodes (about 600) including a number of duplicates, so I expected if I inspected the network calls that I&amp;rsquo;d see a JSON package arriving with what I wanted. This is what I almost always find these days so I&amp;rsquo;ve had very little call to do any real web scraping - it&amp;rsquo;s normally just a matter of locating the endpoint and perhaps extracting an API key from a header.&lt;/p&gt;
&lt;p&gt;So the list must be in the HTML - let&amp;rsquo;s have a look. This is a big file (4000 lines formatted) with a lot of divs and jQuery, but here&amp;rsquo;s our &lt;ul&gt; with the list of episodes.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2025-01-05-at-10.49.48-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The list is nicely named with a unique class (which I&amp;rsquo;ve highlighted above), so this is going to be a simple job, and therefore a good demo.&lt;/p&gt;
&lt;p&gt;We might just dive into the code then pull it apart.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function enumeratePlaylist(html) {
 // Load the HTML into cheerio
 const $ = cheerio.load(html);

 // Find all list items within the playlist
 const $playlistItems = $(&amp;#34;ul.sm2-playlist-bd li&amp;#34;);
 if ($playlistItems.length === 0) {
 console.warn(&amp;#34;Warning: No playlist items found&amp;#34;);
 return;
 }

 console.log(`Info: Found ${$playlistItems.length} items in playlist`);

 // Process each playlist item
 for (const item of $playlistItems) {
 const title = $(item).find(&amp;#34;a&amp;#34;).text().trim();
 const link = $(item).find(&amp;#34;a&amp;#34;).attr(&amp;#34;href&amp;#34;);

 if (!title || !link) {
 console.warn(&amp;#34;Warning: Skipping item with missing title or link&amp;#34;);
 continue;
 }

 outputEpisode(title, link);
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://cheerio.js.org/"&gt;Cheerio&lt;/a&gt; is a library often used for this purpose, if you&amp;rsquo;re familiar with &lt;a href="https://jquery.com/"&gt;jQuery&lt;/a&gt; which is used to manipulate the DOM on the browser side, it&amp;rsquo;s not unreasonable to think of Cheerio as the same thing running in the server. In fact, a lot of the conventions established by jQuery are brought over to Cherio which brings us to our first code snippet.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; const $ = cheerio.load(html);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;lsquo;html&amp;rsquo; is just the HTML we&amp;rsquo;ve fetched into a string, and here we&amp;rsquo;re initialising a cheerio object with it and assigning it to a variable named &amp;lsquo;$&amp;rsquo;. If this is the first time you are encountering this, if would be reasonable to be affronted by this variable name - but this is the convention, so roll with it.&lt;/p&gt;
&lt;p&gt;In jQuery, we just have one &amp;lsquo;$&amp;rsquo; - the document we&amp;rsquo;re in, but in Cheerio working on the server we might want to load multiple - hence the load step that doesn&amp;rsquo;t exist in jQuery.&lt;/p&gt;
&lt;p&gt;You can think of &amp;lsquo;$&amp;rsquo; as now containing a collection of DOM elements. We can select a sub-set of them with a CSS like syntax:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; // Find all list items within the playlist
 const $playlistItems = $(&amp;#34;ul.sm2-playlist-bd li&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this case, we&amp;rsquo;re selecting all the list items &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; inside the unordered list &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; with a &lt;code&gt;class=&amp;quot;sm2-playlist-bd&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://cheerio.js.org/docs/basics/selecting"&gt;Cheerio docs&lt;/a&gt; do a great job of explaining the selectors, but basically you are selecting elements, classes get a period in front of them, having elements separated by spaces means you want all the descendants (as in the example above), a &amp;lsquo;&amp;gt;&amp;rsquo; limits this to the direct descendants, and there&amp;rsquo;s a bunch of pseudo selectors such as odd, find, first etc which are used with a colon. The underlying library is css-select, so you can read all the fine details in their &lt;a href="https://github.com/fb55/css-select/blob/master/README.md#supported-selectors"&gt;readme&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Notice we&amp;rsquo;ve used the &amp;lsquo;$&amp;rsquo; at the start of our variable. Once again, this is the convention, but not as rigorously used for these sub-sets as the single &amp;lsquo;$&amp;rsquo; is for the base Cheerio object.&lt;/p&gt;
&lt;p&gt;Next we loop through the $playListItems and break down the HTML anchor into the title and the link texts.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; const title = $(item).find(&amp;#34;a&amp;#34;).text().trim();
 const link = $(item).find(&amp;#34;a&amp;#34;).attr(&amp;#34;href&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Cheerio can do a bit more with the DOM - including manipulating the elements, but really we&amp;rsquo;ve explained everything you need to know for web scraping with it - it&amp;rsquo;s a very simple library to use, encapsulating some very complex code we don&amp;rsquo;t want to write - the perfect reason for using such an abstraction.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s our final code:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import * as cheerio from &amp;#34;cheerio&amp;#34;;

function outputEpisode(title, link) {
 console.log();
 console.log(`Title: ${title}`);
 console.log(`Link: ${link}`);
}

function enumeratePlaylist(html) {
 // Load the HTML into cheerio
 const $ = cheerio.load(html);

 // Find all list items within the playlist
 const $playlistItems = $(&amp;#34;ul.sm2-playlist-bd li&amp;#34;);
 if ($playlistItems.length === 0) {
 console.warn(&amp;#34;Warning: No playlist items found&amp;#34;);
 return;
 }

 console.log(`Info: Found ${$playlistItems.length} items in playlist`);

 // Process each playlist item
 for (const item of $playlistItems) {
 const title = $(item).find(&amp;#34;a&amp;#34;).text().trim();
 const link = $(item).find(&amp;#34;a&amp;#34;).attr(&amp;#34;href&amp;#34;);

 if (!title || !link) {
 console.warn(&amp;#34;Warning: Skipping item with missing title or link&amp;#34;);
 continue;
 }

 outputEpisode(title, link);
 }
}

async function loadHtmlFromUrl(url) {
 try {
 // Fetch the webpage content
 const response = await fetch(url);
 if (!response.ok) {
 console.error(`Error: Received ${response.status} for URL: ${url}`);
 return;
 }
 return await response.text();
 } catch (error) {
 console.error(`Error: Error fetching HTML from ${url}:`, error.message);
 return;
 }
}

async function main() {
 const url = &amp;#34;https://therestishistory.com/episodes/&amp;#34;;
 console.log(`Info: Fetching HTML from: ${url}`);
 const html = await loadHtmlFromUrl(url);
 if (html) {
 enumeratePlaylist(html);
 }
}

main().catch((err) =&amp;gt; console.error(&amp;#34;Error:&amp;#34;, err));
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Command chaining with NTFY for long running commands</title><link>https://blog.iankulin.com/command-chaining-with-ntfy-for-long-running-commands/</link><pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/command-chaining-with-ntfy-for-long-running-commands/</guid><description>&lt;p&gt;&lt;a href="https://ntfy.sh/"&gt;NTFY&lt;/a&gt; is a great open-source push notification service that&amp;rsquo;s self-hostable or free to use (although I suggest you &lt;a href="https://liberapay.com/ntfy"&gt;pay for it&lt;/a&gt; as I do). I&amp;rsquo;ve written before how I use it with &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;UptimeKuma&lt;/a&gt; for my uptime monitoring, but another common use is just when I&amp;rsquo;m initiating long-running commands and backgrounding them.&lt;/p&gt;
&lt;p&gt;This magic is possible since we can just &lt;code&gt;curl&lt;/code&gt; to send a NTFY notification. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -d &amp;#34;😀 demo push message via NTFY&amp;#34; ntfy.sh/blog_demo
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since I&amp;rsquo;m subscribed to the &amp;ldquo;blog_demo&amp;rdquo; topic in NTFY, this message will be pushed to my phone and watch:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_0056.png" width="640" alt=""&gt;
&lt;p&gt;How I use this is with &amp;lsquo;command chaining&amp;rsquo;. In Linux, you can stack commands together with the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; characters like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir test_dir &amp;amp;&amp;amp; echo &amp;#34;success&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will create the directory, then print &amp;ldquo;success&amp;rdquo; to the shell. I could use it like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nohup rsync -rvits --bwlimit=20 &amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34; ds1_admin@100.78.2.105:&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34; &amp;gt; output.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; curl -d &amp;#34;💾 upload to vm500-kr complete&amp;#34; ntfy.sh/blog_demo &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Both commands will run in the background, and the output of the first command is directed into the &amp;lsquo;output.log&amp;rsquo; file. If the rsync file transfer (that is going to take all night) finishes successfully, then the message saying it&amp;rsquo;s complete will be sent.&lt;/p&gt;
&lt;p&gt;What about if it fails? Well, posix has you covered here too. There&amp;rsquo;s a &lt;code&gt;||&lt;/code&gt; chaining operator that only runs if a command fails.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir invalid/name &amp;amp;&amp;amp; (echo &amp;#34;Directory created successfully.&amp;#34;) || (echo &amp;#34;Failed to create directory.&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the command above, if we already have a directory called &lt;code&gt;invalid&lt;/code&gt;, the &lt;code&gt;mkdir&lt;/code&gt; will work and we&amp;rsquo;ll get the message &amp;ldquo;Directory created successfully.&amp;rdquo;. If &lt;code&gt;invalid&lt;/code&gt; doesn&amp;rsquo;t exist, the command will fail and we&amp;rsquo;ll get the message &amp;ldquo;Failed to create directory.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Note that I&amp;rsquo;ve added some parenthesis - it makes things clearer for the reader, and the command line parser.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s apply this to our slow file transfer:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nohup rsync -rvits --bwlimit=20 &amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34; ds1_admin@100.78.2.105:&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34; &amp;gt; output.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; curl -d &amp;#34;💾 upload to vm500-kr complete&amp;#34; ntfy.sh/blog_demo || curl -d &amp;#34;⚠️ upload to vm500-kr failed!&amp;#34; ntfy.sh/blog_demo &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we&amp;rsquo;ll get a push message for completion or failure. There is one more little bit of housekeeping to do though. When we curl ntfy like this, it actually returns some JSON:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-12-28-at-11.00.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-12-28-at-11.00.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re running this whole thing backgrounded, we really want that to go to the &lt;code&gt;output.log&lt;/code&gt; file with the other output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nohup rsync -rvits --bwlimit=20 &amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34; ds1_admin@100.78.2.105:&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34; &amp;gt; output.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; curl -d &amp;#34;💾 upload to vm500-kr complete&amp;#34; ntfy.sh/blog_demo &amp;gt;&amp;gt; output.log || curl -d &amp;#34;⚠️ upload to vm500-kr failed!&amp;#34; ntfy.sh/blog_demo &amp;gt;&amp;gt; output.log &amp;amp;
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Share files securely with Enclosed</title><link>https://blog.iankulin.com/share-files-securely-with-enclosed/</link><pubDate>Mon, 27 Jan 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/share-files-securely-with-enclosed/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-12-05-at-7.53.56-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-12-05-at-7.53.56-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My accountant works for one of those giant firms, and it bugs me that I&amp;rsquo;m emailing him password protected zip files of my accounts rather than to a secure upload facility at his firm. I can fix this with the power of self hosting, by running my own secure file dropping app on a VPS.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a number of applications that &lt;a href="https://github.com/awesome-selfhosted/awesome-selfhosted?tab=readme-ov-file#file-transfer---single-click--drag-n-drop-upload"&gt;do this sort of thing&lt;/a&gt; - allow you to upload a file, get a link in return which you can then share to people to download the file. For this to be more secure than emailing, the file needs to be encrypted on the server, and we want to be able to set a password, impose limits on downloads, and limit how long the link lives for. I&amp;rsquo;ve previously looked at &lt;a href="https://github.com/eikek/sharry"&gt;Sharry&lt;/a&gt; which adds the ability for unauthenticated users to &lt;em&gt;upload&lt;/em&gt; files to you securely, but for this slightly simpler job, I chose &lt;a href="https://github.com/CorentinTh/enclosed"&gt;Enclosed&lt;/a&gt; by &lt;a href="https://corentin.tech/"&gt;Corentin Thomasset&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The docs provide a &lt;a href="https://docs.enclosed.cc/self-hosting/docker-compose"&gt;simple compose file&lt;/a&gt; to get going docker. Mine is slightly more complex because it&amp;rsquo;s proxy-ed with Nginx Proxy Manager, so it needs to share it&amp;rsquo;s network.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 enclosed:
 container_name: enclosed
 image: corentinth/enclosed
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="authentication"&gt;Authentication&lt;/h3&gt;
&lt;p&gt;What&amp;rsquo;s not well explained in the docs is how to set up authenticated login. By default, if you throw this up on a VPS, the entire world can use it to share their files. What I&amp;rsquo;d like is that I log in to share a file, but the person I send the link to can download the file without logging in. This is easy to do, we just need to add a couple of environment variables to our compose file. I always like to keep my secrets in an .env file since I source control all my home-lab and VPS setups, and I don&amp;rsquo;t want the secrets in source control.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a sample .env file. This just goes in the same directory as our docker-compose.yml&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AUTHENTICATION_USERS=example@example.com:$$2a$$10$$n4StEr5Tcat7jItq
PUBLIC_IS_AUTHENTICATION_REQUIRED=true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The AUTHENTICATION_USERS string is just the username and a bcrypt salted/hashed password. You don&amp;rsquo;t need to do anything hard to create this, the project kindly provides a &lt;a href="https://docs.enclosed.cc/self-hosting/users-authentication-key-generator"&gt;tool for it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The tool includes an option for escaping the &amp;lsquo;$&amp;rsquo; character correctly for docker compose files (hence the double $ in the string above.&lt;/p&gt;
&lt;p&gt;To use this &lt;code&gt;.env&lt;/code&gt; file, we pull in the values in the docker-compose thus:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 enclosed:
 container_name: enclosed
 image: corentinth/enclosed
 environment:
 - AUTHENTICATION_USERS=${AUTHENTICATION_USERS}
 - PUBLIC_IS_AUTHENTICATION_REQUIRED=${PUBLIC_IS_AUTHENTICATION_REQUIRED}
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once that&amp;rsquo;s running, the user name and password will required to upload files or write notes. The interface is clean and self-explanatory:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-12-05-at-8.34.47-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-12-05-at-8.34.47-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Moving a Docker image as a file</title><link>https://blog.iankulin.com/moving-a-docker-image-as-a-file/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-a-docker-image-as-a-file/</guid><description>&lt;p&gt;I&amp;rsquo;m having a super annoying problem at the moment, I can&amp;rsquo;t pull down containers from DockerHub. If I hotspot my laptop off my phone it works fine, so it&amp;rsquo;s some drama with the home internet connection that rebooting the router does not fix.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve had a couple of different errors including &lt;code&gt;Error response from daemon: Get &amp;quot;https://registry-1.docker.io/v2/&amp;quot;: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)&lt;/code&gt; and &lt;code&gt;Error response from daemon: Get &amp;quot;https://registry-1.docker.io/v2/&amp;quot;: dial tcp: lookup registry-1.docker.io&lt;/code&gt;. I can&amp;rsquo;t actually ping &lt;code&gt;registry-1.docker.io&lt;/code&gt; or &lt;code&gt;hub.docker.com&lt;/code&gt;, although I can open hub.docker.com in a browser, so it works for ports 80 and 443, but not some other udp ports.&lt;/p&gt;
&lt;p&gt;Anyway, I needed to update my Jellyfin server as the app on my TV was complaining that it would stop working on the next Android app update if I didn&amp;rsquo;t upgrade to 10.10. Since all my homelab gear is connected through the home internet, I needed a way to download the container to my laptop (connected to my phone), then reconnect to the home network and shift the container to the server.&lt;/p&gt;
&lt;p&gt;Turns out this is no drama.&lt;/p&gt;
&lt;h3 id="what-is-an-oci-image"&gt;What is an OCI image?&lt;/h3&gt;
&lt;p&gt;Before I run through the commands, it&amp;rsquo;s worth appreciating exactly what a container is. It&amp;rsquo;s comprised of &lt;em&gt;layers&lt;/em&gt; - you will have noticed this pulling or building them. And if you already have a layer (perhaps you&amp;rsquo;re pulling two containers based on debian:stable) it will only download that layer once, and the second time it will find it via the giant sha256 hex string and reuse it. Of course if we have all these layers hanging around (don&amp;rsquo;t worry about exactly where - it&amp;rsquo;s abstracted away for us) you need some sort of document that says which exact layers in what order make up the container image. We could call that the manifest.&lt;/p&gt;
&lt;p&gt;So if we wanted to export an image, it would basically be a collection of binaries named with those sha256 names, and a manifest that describes how to rebuild it. We&amp;rsquo;ll see later it&amp;rsquo;s a tiny bit more complex than that, but not by much.&lt;/p&gt;
&lt;h3 id="commands"&gt;Commands&lt;/h3&gt;
&lt;p&gt;First you need to have pulled the image down from the repository, so when you list your images with &lt;code&gt;docker image ls&lt;/code&gt; you can see it in the list. In my case, since I was working on an M1 (ARM) MacBook and wanted the Linux/64 image (to run on my Debian VMs) I had to specify the platform I needed from the multi-architecture image&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker pull --platform linux/amd64 jellyfin/jellyfin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once it&amp;rsquo;s pulled down, we output it to a file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker save -o jellyfin.image jellyfin/jellyfin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;jellyfin.image&lt;/code&gt; is just what I&amp;rsquo;m calling my file, it could be anything. In fact, lets call it &lt;code&gt;jellyfin.tar&lt;/code&gt;, that will be more fun.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker save -o jellyfin.tar jellyfin/jellyfin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Only because it actually is a zipped up file. You can probably guess what&amp;rsquo;s going to be in it:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.02.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.02.28-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Yep - a folder of layer binaries named with their sha256s, and a manifest file saying how to put it together.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve got your image in it&amp;rsquo;s file, you move it to the machine where you need it, then make it available to docker there with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker load -i jellyfin.image
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once that&amp;rsquo;s done, it will be available with it&amp;rsquo;s original name and tag, and you&amp;rsquo;re good to go.&lt;/p&gt;</description></item><item><title>Perils of Benchmarking</title><link>https://blog.iankulin.com/perils-of-benchmarking/</link><pubDate>Mon, 06 Jan 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/perils-of-benchmarking/</guid><description>&lt;p&gt;I&amp;rsquo;ve been containerising my websites, with their servers to make deployment simple and robust, and to move to a CI/CD workflow. Since an install of a production web server is large, I would be running about ten of these containers, and there&amp;rsquo;s already a good server facing the net and doing the reverse-proxying (NGINX Proxy Manager), I chose to bundle the Busy-Box httpd server with my sites inside the Docker containers.&lt;/p&gt;
&lt;p&gt;I had a vague feeling that there was a performance vs size compromise involved, and during some googling found this &lt;a href="https://github.com/nerkn/nginx-busybox-apache/tree/main"&gt;github repo&lt;/a&gt; where nerkn has bench-marked busy-box vs apache vs nginx with, to me (because of my choice above), alarming results.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-10.37.19-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If NGINX is doing twice the throughput, and is two orders of magnitude quicker, then busy-box is not going to be a good choice for me.&lt;/p&gt;
&lt;p&gt;Before I panicked, I thought I&amp;rsquo;d do my own A/B tests, which since it&amp;rsquo;s containerised is simple. I used the &lt;a href="https://httpd.apache.org/docs/2.4/programs/ab.html"&gt;apache &lt;code&gt;ab&lt;/code&gt; testing tool&lt;/a&gt; - it spits out the basics - times for connecting, processing, and waiting. It does multiple tests and gives you the mean and standard deviation for them. Perfect.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the results for a series of tests. I included a commercial website I suspect is in the same data centre as a sanity check.&lt;/p&gt;
&lt;table class="has-fixed-layout"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Test&lt;/td&gt;&lt;td&gt;Mean time (ms)&lt;/td&gt;&lt;td&gt;St dev&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nextdc.com.au&lt;/td&gt;&lt;td&gt;834&lt;/td&gt;&lt;td&gt;293&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;450&lt;/td&gt;&lt;td&gt;93&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;411&lt;/td&gt;&lt;td&gt;24&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;423&lt;/td&gt;&lt;td&gt;24&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;410&lt;/td&gt;&lt;td&gt;26&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;398&lt;/td&gt;&lt;td&gt;19&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;419&lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;403&lt;/td&gt;&lt;td&gt;16&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;398&lt;/td&gt;&lt;td&gt;23&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nextdc.com.au&lt;/td&gt;&lt;td&gt;759&lt;/td&gt;&lt;td&gt;306&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Huh. A couple of things jump out. One is that the site is probably fast enough, and the other is that the performance of busy-box and NGINX are similar, like very suspiciously similar. I wonder what happens if I &lt;code&gt;docker compose down&lt;/code&gt; the website and run the test again?&amp;hellip;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;This is ApacheBench, Version 2.3 &amp;lt;$Revision: 1903618 $&amp;gt;
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
...
Concurrency Level: 10
Time taken for tests: 4.608 seconds
Complete requests: 100
Failed requests: 0
Non-2xx responses: 100
Total transferred: 30300 bytes
HTML transferred: 15400 bytes
Requests per second: 21.70 [#/sec] (mean)
Time per request: 460.777 [ms] (mean)
Time per request: 46.078 [ms] (mean, across all concurrent requests)
Transfer rate: 6.42 [Kbytes/sec] received

Connection Times (ms)
 min mean[+/-sd] median max
Connect: 274 318 33.6 306 451
Processing: 82 95 14.5 92 170
Waiting: 82 95 14.3 92 169
Total: 362 413 37.5 401 580
...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;lol. Okay. I guess I&amp;rsquo;ve been testing the cache in NGINX Proxy Manager this whole time. There is a setting for that, so perhaps I should turn that off. Sadly, even with that turned off, and the container not running, I&amp;rsquo;m still getting that good performance which would be the 500 error coming back from NGINX Proxy Manager.&lt;/p&gt;
&lt;p&gt;Time to trick it into not using the cache by making unique requests each time. I&amp;rsquo;ll use these:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ab -n 100 -c 10 &amp;#34;https://example.com.au/index.html?nocache=$(date +%s%N)&amp;#34;
ab -n 100 -c 10 &amp;#34;https://www.nextdc.com/index.html?nocache=$(date%20+%s%N)&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m also able to see that it&amp;rsquo;s hitting the container with all the requests by running the compose up in the foreground and having the logs output. So now I&amp;rsquo;m much more confident about the output. Here&amp;rsquo;s the summary of a much larger group of tests run in that round robin style.&lt;/p&gt;
&lt;table class="has-fixed-layout"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Situation&lt;/td&gt;&lt;td&gt;Mean time (ms)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Nextdc.com&lt;/td&gt;&lt;td&gt;617&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NGINX Proxy with no site&lt;/td&gt;&lt;td&gt;412&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NGINX-apline site&lt;/td&gt;&lt;td&gt;420&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Busy-box (uclibc)&lt;/td&gt;&lt;td&gt;424&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The comparison with nextdc is of course unfair. They are returning a lot more html, and some of it could be server rendered. I don&amp;rsquo;t have an explanation of why my results are so different from nerkn&amp;rsquo;s. He&amp;rsquo;s using a different tool, and I imagine on a local network (mine is over a mobile data link, to a VPS in a data centre).&lt;/p&gt;
&lt;p&gt;As far as the container size comparisons go, the NGINX-alpine one is 48.98MB and the uclibc version of BusyBox is 1.35MB. I think I&amp;rsquo;ll be sticking with that.&lt;/p&gt;</description></item><item><title>Moving a domain from Wordpress</title><link>https://blog.iankulin.com/moving-a-domain-from-wordpress/</link><pubDate>Mon, 30 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-a-domain-from-wordpress/</guid><description>&lt;p&gt;I love the convenience of a hosted blog on wordpress.com, but one of the justifications for my &amp;lsquo;investment&amp;rsquo; in homelab hardware and learning time was that I&amp;rsquo;d reduce my spend on hosted platforms by self-hosting them. I&amp;rsquo;ve already quit Evernote and dropped back to the free plan on Dropbox by building systems to replace them for less money and more data sovereignty. And now, the recent &lt;a href="https://techcrunch.com/2024/09/25/wordpress-org-bans-wp-engine-blocks-it-from-accessing-its-resources/"&gt;Wordpress drama&lt;/a&gt; has made me uneasy about Matt having control of domains I&amp;rsquo;ve got registered with wordpress.&lt;/p&gt;
&lt;p&gt;For the moment, I&amp;rsquo;m leaving content there, but I&amp;rsquo;d like to keep my options open for the future, so that means moving any domains to an independent registrar, in my case, &lt;a href="https://porkbun.com/"&gt;porkbun&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Wordpress have a &lt;a href="https://wordpress.com/support/domains/transfer-domain-registration/"&gt;good article&lt;/a&gt; explaining their part of the process (kudos to them for not trying to make it difficult) but I ran into a bump not mentioned there, so it&amp;rsquo;s worth writing out the steps for future travelers.&lt;/p&gt;
&lt;h4 id="make-sure-your-email-is-correct"&gt;Make sure your email is correct&lt;/h4&gt;
&lt;p&gt;It probably is fine, but this process is going to rely on you having control of the email address attached to your wordpress account. If you don&amp;rsquo;t currently receive the emails for renewals etc, then you need to fix that first. Registrars like to be careful that they are not giving away people&amp;rsquo;s domains to bad actors, so there will be a bit of a &amp;ldquo;verify you own this email that is the contact for the domain&amp;rdquo; dance as part of this process.&lt;/p&gt;
&lt;h4 id="be-settled"&gt;Be settled&lt;/h4&gt;
&lt;p&gt;For reasons outside WordPress&amp;rsquo;s control, you can&amp;rsquo;t be moving domains around all the time. It needs to have been with the current registrar for 60 days. If not, you&amp;rsquo;ll just need to wait that out.&lt;/p&gt;
&lt;p&gt;Even if that&amp;rsquo;s not your situation, still keep in mind this transfer will take about a week. There are ways of pointing a domain elsewhere a bit quicker, but actually moving it takes five days or more.&lt;/p&gt;
&lt;h4 id="turn-the-transfer-lock-off"&gt;Turn the transfer lock off&lt;/h4&gt;
&lt;p&gt;Most domain registrars allow you to (probably be default) &lt;a href="https://www.icann.org/resources/pages/locked-2013-05-03-en"&gt;&amp;rsquo;lock&amp;rsquo; a domain&lt;/a&gt; to prevent changes. To get to this on Wordpress, go into your site, and look in &amp;ldquo;Upgrades&amp;rdquo; | &amp;ldquo;Domains&amp;rdquo; for &amp;ldquo;Transfer&amp;rdquo;. There&amp;rsquo;s a toggle to turn that off.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.19.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.19.45-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is also where you can request the &amp;ldquo;Authorization Code&amp;rdquo;. This is the key that you&amp;rsquo;ll take over to your new domain registrar. But don&amp;rsquo;t do that yet - that&amp;rsquo;s what I did and got this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.29.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.29.57-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lol. What?! Someone objected by fax to me moving my domain? I feel like the only people who could have done that to this transfer I initiated two seconds ago could be Wordpress.&lt;/p&gt;
&lt;h4 id="turn-private-registration-off"&gt;Turn Private Registration off&lt;/h4&gt;
&lt;p&gt;To their credit (again) this was explained in another email shortly after:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-7.02.20-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-7.02.20-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ah, so I need to turn &amp;lsquo;private registration&amp;rsquo; off. This is the mechanism that hides your personal details as a domain owner from scammers and grifters. Apparently it has to be &amp;lsquo;off&amp;rsquo; to transfer the site. This is not a source of stress to me, as soon as the domain is transferred to PorkBun, the apparent owner of the domain when someone does a &lt;code&gt;whois&lt;/code&gt; on it, will be &amp;ldquo;Private By Design LLC&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Once again, this setting is in the Wordpress site settings under &amp;ldquo;Domain&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.32.29-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.32.29-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="get-your-authorization-code"&gt;Get your Authorization Code&lt;/h4&gt;
&lt;p&gt;Now is the time to hit the button in Wordpress to request the &amp;ldquo;Authorization Code&amp;rdquo;. It will be sent to the email attached to the domain. This is the hex string you&amp;rsquo;ll need to take to your domain registrar to request the transfer.&lt;/p&gt;
&lt;h4 id="start-the-transfer"&gt;Start the transfer&lt;/h4&gt;
&lt;p&gt;I guess every domain registrar will have a slightly different set up. With porkbun, I just went to &lt;a href="https://porkbun.com/transfer"&gt;https://porkbun.com/transfer&lt;/a&gt; and entered the domain name and the authorisation code. They did charge me $11 for this, then advised that the transfer would take about five days. Maybe that&amp;rsquo;s built into the domain transfer system to allow more people to object by fax.&lt;/p&gt;
&lt;p&gt;On the porkbun status page for the transfer, I was able to set up an A record to the wordpress IP where by blog still lives, so that the second the transfer went through, it would be set up to direct traffic there with a minimal downtime. I guess in this case that would have no effect since the wordpress name servers would still be in place (see further down), but it&amp;rsquo;s a good idea since often when you&amp;rsquo;re moving a domain, the losing registrar would be deleting your name-server entry.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/5later.jpg" width="300" alt=""&gt;
&lt;p&gt;Once the email came through on the sixth day, I checked the domain was still pointing to the blog, and it was all good. But we&amp;rsquo;re not done yet.&lt;/p&gt;
&lt;h4 id="change-the-nameservers"&gt;Change the nameservers&lt;/h4&gt;
&lt;p&gt;Although I&amp;rsquo;ve now got control of the domain, we&amp;rsquo;re still using WordPress&amp;rsquo;s nameservers. That&amp;rsquo;s not a big deal for me, but I do want to bring them over to porkbun so it&amp;rsquo;s the same setup as all my other domains. Before I nuke the wordpress nameservers, we need to check what records are in it.&lt;/p&gt;
&lt;p&gt;First step is to see who are the nameservers for a domain. We do this with the &lt;code&gt;dig NS &amp;lt;domain-name&amp;gt;&lt;/code&gt; command:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.27.37-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.27.37-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this example the name servers are &lt;code&gt;b.iana-servers.net&lt;/code&gt; and &lt;code&gt;a.iana-servers.net&lt;/code&gt; In the case of your wordpress blog they are probably &lt;code&gt;ns1.wordpress.com&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;Once you know the name of the nameservers you can query them with the domain name to see what the records are. The most important will be the A records, but you probably want to go ahead and check the MX (mail) and TXT records as well so you can reproduce them on the new registrar.&lt;/p&gt;
&lt;p&gt;This is done with &lt;code&gt;dig @&amp;lt;name-server&amp;gt; &amp;lt;domain-name&amp;gt; &amp;lt;record-type&amp;gt;&lt;/code&gt; for example &lt;code&gt;dig @b.iana-servers.net example.com A&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.33.21-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In this case there is a single A record pointing the domain to 93.184.215.14 We need to note all of these to reproduce them in the domain settings in your new registrar. Again this is going to be different for each one, but if you&amp;rsquo;ve ever pointed a domain anywhere, you&amp;rsquo;ll know how to do it on yours.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.40.11-am-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.40.11-am-1.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="change-the-nameservers-1"&gt;Change the nameservers&lt;/h4&gt;
&lt;p&gt;Now those records are all in, it&amp;rsquo;s time to change the nameservers. There will be an option somewhere in your domain management tools at the registrar to allow for this. In my case, I&amp;rsquo;ll be switching to porkbun&amp;rsquo;s default ones.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.45.30-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.45.30-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="profit"&gt;Profit&lt;/h4&gt;
&lt;p&gt;That all was a bit of a dance, but it feels good to be in control of the domain so I can redirect it in the future if needed.&lt;/p&gt;
&lt;p&gt;Edit from the future: This (pointing the domain I now controlled at my wordpress.com blog) died after a couple of weeks. I&amp;rsquo;m not sure if they changed something, but when I went into the wordpress settings to check it was still set up to use an external domain name, it greeted me with an &amp;lsquo;upgrade&amp;rsquo; offer to turn that on at an annual cost greater than my old plan. So, I had to hurriedly set up a wordpress instance on a VPS - which turned out to be not much drama and will probably be the subject of a future post.&lt;/p&gt;</description></item><item><title>Updating a deployment on fly.io</title><link>https://blog.iankulin.com/updating-a-deployment-on-fly-io/</link><pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/updating-a-deployment-on-fly-io/</guid><description>&lt;img src="https://blog.iankulin.com/images/flyio_picture.png" width="620" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve had my external UptimeKuma chugging away on &lt;a href="https://fly.io/"&gt;fly.io&lt;/a&gt;, for free, for months now, and the container image it was based on was a bit out of date, so I wanted to update it. I hadn&amp;rsquo;t looked at fly.io for months, and couldn&amp;rsquo;t really recall what I&amp;rsquo;d done to create it.&lt;/p&gt;
&lt;p&gt;The way this works is that that you create a fly.toml file that sets out the details of your app. From memory I think I used the one from the docs and gave it a unique name, the name of the Docker image, the port, the datacentre location, and the directory for the persisted data. The you run &lt;code&gt;fly deploy&lt;/code&gt; from the directory with the toml file (having already installed the CLI tool and logged in) and you&amp;rsquo;re in business.&lt;/p&gt;
&lt;p&gt;Fly doesn&amp;rsquo;t actually run your container, it deconstructs it and uses the layer information to launch a firecracker instance, but of course, none of this matters to the user - it&amp;rsquo;s just as if your containerised app is magically live on the internet with hardly any effort or money (so far I&amp;rsquo;ve paid $0.00 for eight months of good service).&lt;/p&gt;
&lt;p&gt;I was sort of dreading the upgrade, I guessed I&amp;rsquo;d need to kill the old instance, start the new one and connect it back to my persistent storage, but here&amp;rsquo;s what I actually did.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Went to the folder with my fly.toml file&lt;/li&gt;
&lt;li&gt;Typed &lt;code&gt;fly deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-28-at-6.22.46-am-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-28-at-6.22.46-am-1.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Fly.io is such a great way to deploy stuff. If I wasn&amp;rsquo;t such a committed self-hoster I would use it a lot more. They used to be hosted on Heroku (which is on AWS) but I understand they have moved to their own worldwide data centers. Their secret sauce is the dev experience. So good.&lt;/p&gt;
&lt;h3 id="edit-update-from-the-future"&gt;Edit: Update from the future&lt;/h3&gt;
&lt;p&gt;So, very day or so since I did that update, which was to version 1.23, I&amp;rsquo;ve been getting these emails from Fly.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Fly.io] ikuptime ran out of memory and crashed
Fly &amp;lt;support@fly.io&amp;gt;
	
4:12 AM (16 hours ago)
	

Hello! Your “ikuptime” application hosted on Fly.io crashed because it ran out of memory. Specifically, the instance 3d8d7de3b20738. Adding more RAM to your application might help!
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hmm. In theory the free machine I&amp;rsquo;m using on Fly includes 256MB of RAM, and when I look at the average use, it&amp;rsquo;s sitting around 200MB, but it does say out of 223MB, so I guess that&amp;rsquo;s the real limit.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.37.18-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.37.18-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Looking at the graph of memory use, it does look like there&amp;rsquo;s something in the container with a memory leak, then it&amp;rsquo;s being restarted once it hits about 205MB for a while.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.31.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.31.28-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;An easy fix might be to swap to a lighter weight container. You can see at the end of the graph above I&amp;rsquo;ve dropped it down to about 160MB. That was by using the image tagged with &lt;code&gt;:1-alpine&lt;/code&gt;. I&amp;rsquo;ll keep and eye on it and see what happens.&lt;/p&gt;
&lt;p&gt;I am running the full size container in the home lab inside an LXC, and it doesn&amp;rsquo;t seem to have the leak.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.40.49-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.40.49-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is not quite an apples for apples comparison. Fly.io doesn&amp;rsquo;t actually run the container, it uses the container layers to build the app in a tiny VM called &lt;a href="https://firecracker-microvm.github.io/"&gt;firecracker&lt;/a&gt;. This is the technology used by AWS to run serverless functions.&lt;/p&gt;
&lt;p&gt;I guess I&amp;rsquo;ll be able to see in a day or so if I&amp;rsquo;ve solved the problem.&lt;/p&gt;
&lt;h3 id="edit-update-from-the-distant-future"&gt;Edit: Update from the distant future&lt;/h3&gt;
&lt;p&gt;Perhaps the memory growth is still there (after an update it drops down 12ish MB):&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-03-17-at-12.56.40.png" width="900" alt=""&gt;
&lt;p&gt;but in any case, running the Alpine base image has kept the memory use well under the limits for my free instance.&lt;/p&gt;
&lt;p&gt;In other news, I was on a new laptop when I tried to run the &lt;code&gt;fly deploy&lt;/code&gt; command today, so things were a tiny bit more complex. I had to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;install the fly command line stuff with &lt;code&gt;brew install flyctl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;then when I ran &lt;code&gt;fly deploy,&lt;/code&gt; it asked me to sign in, and opened a web page for me to do so.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>NGINX proxy manager - setting headers to use basic auth in your apps</title><link>https://blog.iankulin.com/nginx-proxy-manager-setting-headers-to-use-basic-auth-in-your-apps/</link><pubDate>Mon, 09 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-proxy-manager-setting-headers-to-use-basic-auth-in-your-apps/</guid><description>&lt;p&gt;When I&amp;rsquo;m spinning up side projects, I frequently ignore auth, and just rely on NGINX basic auth - one of the side benefits of reverse-proxying everything.&lt;/p&gt;
&lt;h3 id="regular-nginx"&gt;Regular NGINX&lt;/h3&gt;
&lt;p&gt;This &lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/"&gt;article in the docs&lt;/a&gt; explains how to set up basic auth to protect different paths. To make it work in my node apps, I need the successful user name passed in so I check it against the user table to work out access rights etc.&lt;/p&gt;
&lt;p&gt;To get it passed in with every request, we need to stick it in the headers. We do this in the NGINX conf for the site:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
	root /var/www/ct.example.com;
	index index.html;
	server_name ct.example.com;
	location / {
		auth_basic &amp;#34;Secure app&amp;#34;;
	 auth_basic_user_file /etc/nginx/.htpasswd;
	 
		proxy_pass http://localhost:3000;	
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection &amp;#39;upgrade&amp;#39;;
		proxy_set_header Host $host;
		proxy_set_header X-Username $remote_user;
		proxy_cache_bypass $http_upgrade;
	}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in my app, I just check the header like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const USERNAME_HEADER = &amp;#34;x-username&amp;#34;;

// basic auth middleware
app.use((req, res, next) =&amp;gt; {
 const username = req.headers[USERNAME_HEADER];

 if (!username) {
 return res.status(400).send(`Missing header: ${USERNAME_HEADER}`);
 }

 const user = usersArray.find((user) =&amp;gt; user.user === username);

 if (user) {
 // Save user details to req object
 req.role = user.role;
 req.username = username;
 next();
 } else {
 res.status(401).send(`User unauthorised: ${username}`);
 }
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s a bit of a fudge, but for personal use apps it&amp;rsquo;s quick to set up, and pretty robust from a security point of view.&lt;/p&gt;
&lt;h3 id="nginx-proxy-manager"&gt;NGINX Proxy Manager&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve moved to using NGINX Proxy Manager (NPM) rather than raw NGINX since it makes getting SSL certificates super simple. NPM basically wraps all the reverse proxy functionality of NGINX into a nice click ops web GUI.&lt;/p&gt;
&lt;p&gt;We now specify the user names and passwords in the gui (instead of faffing around installing apache2-utils and running it to build the &lt;code&gt;.htpasswd&lt;/code&gt; file. This is done by creating an access list:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.43.16-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.43.16-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then in the Authorization tab, adding your users:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.42.44-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.42.44-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s saved, we apply it to the proxy record:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.47.38-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.47.38-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But then how do we set the x-username header? Since there&amp;rsquo;s an advanced tab, you may think it&amp;rsquo;s in there, and that looks promising till you read the warning below the text box.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.52.24-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.52.24-am.png" width="839" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Setting a header is exactly what we want to do, so let&amp;rsquo;s head to the &amp;ldquo;locations&amp;rdquo; tab.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.55.04-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.55.04-am.png" width="674" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We just use the root path &amp;ldquo;/&amp;rdquo; for our path so it applies to every request, proxy those requests to our app, and set the header in the text box (which appears when you click the gear button).&lt;/p&gt;</description></item><item><title>Controlling Docker container startup order</title><link>https://blog.iankulin.com/controlling-docker-container-startup-order/</link><pubDate>Mon, 02 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/controlling-docker-container-startup-order/</guid><description>&lt;p&gt;A very common scenario when running services in Docker containers is that one service is going to depend on another. The most common example is going to be if you have a service that needs a database - you&amp;rsquo;re going to want the container running the database to be ready for business before the service that needs it starts. And conversely, when you shut things down, you want to stop the service before you kill the database or you may lose some data.&lt;/p&gt;
&lt;p&gt;Both of these things are easily catered for with containers started with docker compose, but there&amp;rsquo;s a few caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The services need to share a docker-compose file&lt;/li&gt;
&lt;li&gt;The services need to share a network (which they will by default if they&amp;rsquo;re in the same compose file and you don&amp;rsquo;t override it)&lt;/li&gt;
&lt;li&gt;The service that is depended on, must telegraph it&amp;rsquo;s state with a health check.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="application"&gt;Application&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been doing monitoring by running an app (&lt;a href="https://github.com/IanKulin/vitals-glimpse"&gt;vitals-glimpse&lt;/a&gt;) on all my services &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;that exposes some very basic metrics&lt;/a&gt; as an API endpoint. Then a couple of instances of UptimeKuma (one on fly.io, for monitoring outside services and one inside the homelab network) monitor all those, and check an okay flag for &lt;em&gt;up&lt;/em&gt; vs &lt;em&gt;down&lt;/em&gt;. If something changes status, I get an &lt;a href="https://ntfy.sh/"&gt;ntfy&lt;/a&gt; message on my watch.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.15.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.15.57-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a great setup, and I&amp;rsquo;ll be keeping it, but I have &lt;a href="https://grafana.com/"&gt;Graphana&lt;/a&gt; envy, so I need to be grabbing those values and saving them in a time series database. There&amp;rsquo;s not much thought needed to be put into which database, it&amp;rsquo;s &lt;a href="https://www.influxdata.com/"&gt;InfluxDB&lt;/a&gt;. As for pulling in all the data, there&amp;rsquo;s probably a highly configurable open source solution for this, or, I could just write my own and call it &lt;a href="https://github.com/IanKulin/glimpse-scan"&gt;glimpse-scan&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But now I need to run glimpse-scan and InfluxDB in containers together, and have glimpse-scan wait for Influx to be ready before it gets to work. We&amp;rsquo;re going to do that with docker-compose.&lt;/p&gt;
&lt;h2 id="healthcheck"&gt;healthcheck&lt;/h2&gt;
&lt;p&gt;For this to work, there needs to be some CLI command you can run to check the health of the service we&amp;rsquo;re going to wait on. If the service was a website, you might just curl the index page like &lt;code&gt;curl http://localhost&lt;/code&gt; and since InfluxDB actually does contain a little web server that would work, but they actually provide a better one:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;curl -f http://localhost:8086/ping&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Having some sort of health check like this is super common for containerised databases so some googling will find what you&amp;rsquo;re after. The command, what ever it is, needs to return an error code if it&amp;rsquo;s not successful. This is almost universal for unix commands.&lt;/p&gt;
&lt;p&gt;Now we just add that into the compose file.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 influxdb:
 image: influxdb:2 
 container_name: influxdb
 healthcheck:
 test: [&amp;#34;CMD-SHELL&amp;#34;, &amp;#34;curl -f http://localhost:8086/ping&amp;#34;]
 interval: 5s
 timeout: 10s
 retries: 5
 ports:
 - &amp;#34;8086:8086&amp;#34;
 environment:
 - DOCKER_INFLUXDB_INIT_MODE=setup
 - DOCKER_INFLUXDB_INIT_USERNAME=${INFLUXDB_ADMIN_USER}
 - DOCKER_INFLUXDB_INIT_PASSWORD=${INFLUXDB_ADMIN_PASSWORD}
 - DOCKER_INFLUXDB_INIT_ORG=${INFLUXDB_ORG}
 - DOCKER_INFLUXDB_INIT_BUCKET=${INFLUXDB_BUCKET}
 - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${INFLUXDB_ADMIN_TOKEN}
 - INFLUXD_METRICS_DISABLED=true
 volumes:
 - ./influxdb/data:/var/lib/influxdb2
 restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even if you don&amp;rsquo;t need your containers to depend on one another, it might still be a good idea to add health checks like this since it makes the &lt;code&gt;docker ps&lt;/code&gt; information a bit more helpful.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.41.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.41.53-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="depends_on"&gt;depends_on&lt;/h2&gt;
&lt;p&gt;The next step is to tell the other container (in our example, the glimpse-scan app) which other service it depends on.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; glimpse-scan:
 image: ghcr.io/iankulin/glimpse_scan:latest
 container_name: glimpse-scan
 depends_on:
 influxdb:
 condition: service_healthy
 build:
 context: .
 dockerfile: Dockerfile
 volumes:
 - ./glimpse-scan/data:/app/data
 restart: unless-stopped
 env_file:
 - .env
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="go-time"&gt;Go time&lt;/h2&gt;
&lt;p&gt;And, compose it up (remember the two lots of code above are together in a single &lt;code&gt;docker-compose.yml&lt;/code&gt; file).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.43-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.43-pm.png" width="906" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.47-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.47-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.52-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.52-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Fixing TLS for wget in BusyBox</title><link>https://blog.iankulin.com/fixing-tls-for-wget-in-busybox/</link><pubDate>Mon, 25 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fixing-tls-for-wget-in-busybox/</guid><description>&lt;p&gt;I&amp;rsquo;ve been containerising my static websites with BusyBox (because it&amp;rsquo;s small), and in &lt;a href="https://blog.iankulin.com/fancier-website-in-a-docker-container/"&gt;an earlier post&lt;/a&gt; showed how to even get the container to update parts of the site by reaching out with &lt;code&gt;wget&lt;/code&gt; to download resources from elsewhere and saving them inside the container where we are serving the &amp;lsquo;static&amp;rsquo; site from. I&amp;rsquo;d done this by including a bash script in the container with the &lt;code&gt;wget&lt;/code&gt; in a loop with a &lt;code&gt;sleep&lt;/code&gt;. Then started the script and the httpd server in the CMD line of the &lt;code&gt;dockerfile&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the dockerfile.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busybox:latest

# Add shell script and set executable
COPY update_content.sh /usr/local/bin/update_content.sh
RUN chmod +x /usr/local/bin/update_content.sh

# Create the directory for the web content, and copy files in
RUN mkdir -p /var/www/html
COPY www/. /var/www/html

# Expose port 80 for the web server
EXPOSE 80

# Start the httpd server
CMD [&amp;#34;sh&amp;#34;, &amp;#34;-c&amp;#34;, &amp;#34;/usr/local/bin/update_content.sh &amp;amp; busybox httpd -f -p 80 -h /var/www/html&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And the bash script:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/sh

# Define the URL and the destination path
URL=&amp;#34;http://httpbin.org/image/jpeg&amp;#34;
DEST_PATH=&amp;#34;/var/www/html/image.jpg&amp;#34;
FETCH_INTERVAL=120 # 2 minutes

while true; do
 # Use wget to download the file
 wget -O &amp;#34;$DEST_PATH&amp;#34; &amp;#34;$URL&amp;#34;

 # Check the exit status of wget
 if [ $? -eq 0 ]; then
 echo &amp;#34;File downloaded successfully to $DEST_PATH&amp;#34;
 else
 echo &amp;#34;Failed to download the file.&amp;#34;
 fi
 sleep $FETCH_INTERVAL
done
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This all works perfectly, as long as the file you&amp;rsquo;re downloading is over http. Trying over an SSL connection (which must be 98% of the internet by now) fails. The reason for this is that &lt;a href="https://blog.iankulin.com/fancier-website-in-a-docker-container/"&gt;BusyBox does not contain the certificates for the root CA&lt;/a&gt;. In a normal distribution, you&amp;rsquo;d just do ahead and install them, but BusyBox also does not have a package manager to help you do that, so there&amp;rsquo;s no &amp;lsquo;&lt;code&gt;apk update &amp;amp;&amp;amp; apk add ca-certificates&lt;/code&gt;&amp;rsquo; to help us out.&lt;/p&gt;
&lt;p&gt;A viable solution might be to just switch to an Alpine container, but I&amp;rsquo;d be going up to 12MB per containerised website then (from 4) which seems a bit much.&lt;/p&gt;
&lt;p&gt;In a &lt;a href="https://blog.iankulin.com/fancier-website-in-a-docker-container/"&gt;Stack Overflow post&lt;/a&gt;, &lt;a href="https://stackoverflow.com/users/2830850/tarun-lalwani"&gt;Tarun Lalwani&lt;/a&gt; offers a couple of suggestions. One is having a multi-stage docker image build where you create an Alpine container, copy the certs out to a volume, then copy them into your busybox image. To my mind that would be a good idea to create a new image (essentially BusyBox with certs) to chuck up on a repository somewhere. Such an &lt;a href="https://hub.docker.com/r/odise/busybox-curl"&gt;image does exist,&lt;/a&gt; but it&amp;rsquo;s very old.&lt;/p&gt;
&lt;p&gt;Another suggestion is just to bind mount the certs directory in the container to the host (as read only), and use the host&amp;rsquo;s certificates. This seems like a much simpler approach to me. It&amp;rsquo;s just an edit to the &lt;code&gt;docker-compose.yml&lt;/code&gt; or the run command.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 example.com:
 container_name: httpd-example.com
 image: ghcr.io/iankulin/example.com:latest
 restart: unless-stopped
 volumes:
 - /etc/ssl/certs:/etc/ssl/certs:ro # Bind mount host&amp;#39;s SSL certs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run --name httpd-example.com -p 80:80 -v /etc/ssl/certs:/etc/ssl/certs:ro ghcr.io/iankulin/example.com:latest
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Fancier Website in a Docker Container</title><link>https://blog.iankulin.com/fancier-website-in-a-docker-container/</link><pubDate>Mon, 18 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fancier-website-in-a-docker-container/</guid><description>&lt;p&gt;The previous post went over how to bundle a static website into a Docker container. That&amp;rsquo;s a neat little trick - keeping the entire website and making it trivial to install on a VPS behind Nginx Proxy Manager. It worked great for most of my little websites.&lt;/p&gt;
&lt;h3 id="but"&gt;But&amp;hellip;&lt;/h3&gt;
&lt;p&gt;A couple of my websites had very minor &amp;lsquo;dynamic&amp;rsquo; content. One was pulling down the local temperature from OpenWeather, then exposing a cut-down version of that as a REST endpoint so all my servers could grab it without me being rate-limited by OpenWeather for abusing my free API key. Another one re-hosted an image that changes a couple of times a day from an unreliable service.&lt;/p&gt;
&lt;p&gt;So, can we do those sorts of jobs in our BusyBox web containers? Well yes, of course. Let&amp;rsquo;s look at the image re-hosting problem, but the approach is going to be similar for other small internet tasks.&lt;/p&gt;
&lt;p&gt;We need the container (as well as hosting the website) to repeatedly download an image from the internet, and save it into the directory in the container where the static files are being hosted. In my first attempt at this, I messed around with cron, but I was over complicating it, and since BusyBox is not a full distro with all the regular tools, lots of things (including cron) just didn&amp;rsquo;t work the way I expected the first try.&lt;/p&gt;
&lt;p&gt;Where I ended up was having a script called &lt;code&gt;update_content.sh&lt;/code&gt; that has a loop with a &lt;code&gt;sleep()&lt;/code&gt; in the bottom, but at the top, downloads the file we want into our &lt;code&gt;/var/www/html/&lt;/code&gt; directory. Here&amp;rsquo;s the script:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/sh

# Define the URL and the destination path
URL=&amp;#34;http://httpbin.org/image/jpeg&amp;#34;
DEST_PATH=&amp;#34;/var/www/html/image.jpg&amp;#34;
FETCH_INTERVAL=120 # 2 minutes

while true; do
 # Use wget to download the file
 wget -O &amp;#34;$DEST_PATH&amp;#34; &amp;#34;$URL&amp;#34;

 # Check the exit status of wget
 if [ $? -eq 0 ]; then
 echo &amp;#34;File downloaded successfully to $DEST_PATH&amp;#34;
 else
 echo &amp;#34;Failed to download the file.&amp;#34;
 fi
 sleep $FETCH_INTERVAL
done
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in our &lt;code&gt;dockerfile&lt;/code&gt;, the &lt;code&gt;CMD&lt;/code&gt; launches the script as well as the httpd server:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busybox:latest

# Add shell script and set executable
COPY update_content.sh /usr/local/bin/update_content.sh
RUN chmod +x /usr/local/bin/update_content.sh

# Create the directory for the web content, and copy files in
RUN mkdir -p /var/www/html
COPY www/. /var/www/html

# Expose port 80 for the web server
EXPOSE 80

# Start the httpd server
CMD [&amp;#34;sh&amp;#34;, &amp;#34;-c&amp;#34;, &amp;#34;/usr/local/bin/update_content.sh &amp;amp; busybox httpd -f -p 80 -h /var/www/html&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The docker-compose.yml remains the same as in the previous post - if you haven&amp;rsquo;t read that, I was running all these website containers behind Nginx Proxy Manager. If you are not, then just go ahead and delete out the &amp;ldquo;networks&amp;rdquo; parts.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 example.com:
 container_name: httpd-example.com
 image: ghcr.io/iankulin/example.com:latest
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Eagle eyed readers (or people with experience of using the BusyBox version of wget) will have noticed the oddly particular image file I chose to download for this demo code:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;URL=&amp;quot;http://httpbin.org/image/jpeg&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I chose this, because it&amp;rsquo;s one of the last image files in the world to be served over http, and wget in BusyBox chokes on TLS for reasons I&amp;rsquo;ll discuss next week.&lt;/p&gt;</description></item><item><title>Website in a Docker Container</title><link>https://blog.iankulin.com/website-in-a-docker-container/</link><pubDate>Mon, 11 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/website-in-a-docker-container/</guid><description>&lt;p&gt;Having figured out how to use the GitHub package registry, I was a bit inspired by &lt;a href="https://lipanski.com/posts/smallest-docker-image-static-website"&gt;this blog post&lt;/a&gt; from Florin Lipan to deliver all my little static websites as Docker containers. I&amp;rsquo;m not as focused as he is about making them tiny, but I did steal the idea of using &lt;a href="https://busybox.net/about.html"&gt;BusyBox&lt;/a&gt; httpd for serving them, resulting in about 4MB containers. That&amp;rsquo;s small enough for me, and since they are all very similar, there&amp;rsquo;s a fair bit of layer reuse going on.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the setup. I dump the static (html, css, js etc) files for the website into a &amp;lsquo;www&amp;rsquo; sub-directory.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-6.55.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-6.55.02-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The dockerfile pulls in BusyBox then copies those files into the container. Note these are in the container, it&amp;rsquo;s not going to be bound to an external directory (where we could change them), the container carries its website files with it. Any change to the website content will require a container rebuild.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EXPOSE 80&lt;/code&gt; doesn&amp;rsquo;t really do anything, it&amp;rsquo;s pretty much just documentation. Then the &lt;code&gt;CMD&lt;/code&gt; directive starts the server on port 80 and points to the static files that we copied in earlier.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busybox:latest

# Create the directory for the web content, and copy files in
RUN mkdir -p /var/www/html
COPY www/. /var/www/html

# Expose port 80 for the web server
EXPOSE 80

# Start the httpd server
CMD [&amp;#34;sh&amp;#34;, &amp;#34;-c&amp;#34;, &amp;#34;busybox httpd -f -p 80 -h /var/www/html&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To use this dockerfile to build our container, just docker build it and give it a tag:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker build -t ghcr.io/iankulin/example.com:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then if we run it, and go to http://localhost, there&amp;rsquo;s our website.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run --name httpd-example.com -p 80:80 ghcr.io/iankulin/example.com:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="with-nginx-proxy-manager"&gt;With NGINX Proxy Manager&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; file I use on the VPS host is slightly more complicated. We want each of the website containers to run in the same docker network as Nginx Proxy Manager - since docker networks have their own little dns server based on the container names, that&amp;rsquo;s going to make hooking it up trivial.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 example.com:
 container_name: httpd-example.com
 image: ghcr.io/iankulin/example.com:latest
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="architecture"&gt;Architecture&lt;/h3&gt;
&lt;p&gt;Since I develop on an M1 MacBook, but host all my workloads on regular AMD64 Linux LXC containers or VMs, I need to build for that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker build --platform linux/amd64 -t ghcr.io/iankulin/example.com:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In actual fact, I could have built that way for the Mac as well - Docker Desktop would have just run it in a Linux VM with a small performance penalty which wouldn&amp;rsquo;t be noticeable for my purposes. Once it&amp;rsquo;s built, we push it up to the GitHub Container Registry.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker push ghcr.io/iankulin/example.com:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Working with the registry is well covered in my previous post, so I won&amp;rsquo;t go into those details here.&lt;/p&gt;
&lt;h3 id="on-the-host"&gt;On the host&lt;/h3&gt;
&lt;p&gt;On the host where the website is to run, I just make a directory for it and drop the &lt;code&gt;docker-compose.yml&lt;/code&gt; in. Then &lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re running in the Nginx Proxy Manager docker network, when we specify the host name for the new web site for NPM to proxy to, it&amp;rsquo;s just the container name we gave it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then the DNS settings for your domain need to be pointed to this host. Once that&amp;rsquo;s propagated, you&amp;rsquo;ll be able to request the SSL certificate in NPM and your website is live.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png" width="886" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Using the GitHub Container Registry</title><link>https://blog.iankulin.com/using-the-github-container-registry/</link><pubDate>Mon, 04 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-the-github-container-registry/</guid><description>&lt;p&gt;As the number of little projects I&amp;rsquo;m running on VPSs grows, I need to have a regimented system for managing all that. I could be using something like &lt;a href="https://coolify.io/"&gt;Coolify&lt;/a&gt;, but, at least for the moment, I&amp;rsquo;d rather build my own system.&lt;/p&gt;
&lt;p&gt;Currently my system is Nginx Proxy Manager (dockerised) in front of each app. If it&amp;rsquo;s a static website, that&amp;rsquo;s another dockerised Nginx, started with a compose file and with &lt;code&gt;www&lt;/code&gt; and &lt;code&gt;conf&lt;/code&gt; sub-directories that I&amp;rsquo;ve &lt;code&gt;git pull&lt;/code&gt;ed from the project. It&amp;rsquo;s not pretty.&lt;/p&gt;
&lt;p&gt;It occurs to me that I could just be bundling each static website &lt;em&gt;inside&lt;/em&gt; a Docker image, then the only content for each website on the VPS would be a compose file. This has the extra appeal that eventually I could use GitHub CI/CD to rebuild the container so changing a website would be pushing my edits to main, then &lt;code&gt;compose down&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, and &lt;code&gt;up&lt;/code&gt; on the VPS.&lt;/p&gt;
&lt;p&gt;Currently I&amp;rsquo;ve only been using DockerHub for my containers, but the free plan only allows for a single private image, whereas on GitHub the free plan doesn&amp;rsquo;t have a number of packages limit - rather it has a total storage (500MB) and monthly transfers (1GB) &lt;a href="https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-packages/about-billing-for-github-packages"&gt;limits&lt;/a&gt;. Along with the aforementioned integration with GitHub CI/CD, this makes it the obvious place to store these images until my scale is large enough to set up my own registry (which I would probably do with &lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt; on a VPS since the limits of running that inside my Tailscale network is starting to be a friction point anyway).&lt;/p&gt;
&lt;p&gt;Long story short - I want to start using the GitHub container registry, and this post steps through that.&lt;/p&gt;
&lt;h2 id="access-tokens"&gt;Access Tokens&lt;/h2&gt;
&lt;p&gt;If you set up your Docker Hub a while ago, you&amp;rsquo;ve probably forgotten that early on, you had to log into it from the command line with the &lt;code&gt;docker login&lt;/code&gt; command. With Docker Hub, that&amp;rsquo;s your usual username/password combo, but GitHub has a more fine-grained system of permissions, where you generate &amp;lsquo;Personal Access Tokens&amp;rsquo;. This is quite cool, for example you can generate a token with add/update/delete access for your main development laptop, but then generate a different token that only has read access to use on the server where you need to deploy the image.&lt;/p&gt;
&lt;p&gt;The Personal Access Token (PAT) is just used in place of a password when you log in. As well as the benefit of being able to control the permissions for each PAT when you create it, you also name them. This would be helpful for example if your software lead had their laptop stolen, and you needed to revoke the PAT for that device. It&amp;rsquo;s also possible to set expiry dates for the PATs.&lt;/p&gt;
&lt;p&gt;Generating the PATs is done in:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GitHub | Profile | Settings | Developer Settings (left column, bottom) | Personal Access Tokens | Tokens Classic | Generate Access Token Classic&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If you have MFA set up (you should) it will ask for that. Then give it a &amp;lsquo;Note&amp;rsquo; and the permissions you want for &amp;ldquo;Packages&amp;rdquo; - Container images are stored in the &amp;ldquo;Packages&amp;rdquo; section of GitHub (where it&amp;rsquo;s also possible to store other artifacts such as your private NPM packages). In the example below we&amp;rsquo;ve asked for Read/Write/Delete access.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.49.54-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.49.54-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s completed, you&amp;rsquo;ll be shown a list of your existing PATs, plus the new one you&amp;rsquo;ve just generated. This is the only time it will ever be displayed in GitHub, so you need to copy it out to where you need it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.50.11-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.50.11-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="logging-in"&gt;Logging in&lt;/h2&gt;
&lt;p&gt;Before we can push an image to the GitHub Container Registry (ghcr.io) we&amp;rsquo;ll need to use this PAT to log in. I&amp;rsquo;m using the command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker login --username &amp;lt;github username&amp;gt; --password &amp;lt;PAT we just generated&amp;gt; ghcr.io
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;ghcr.io&lt;/code&gt; is just the URI for the container registry. It will complain about you pasting the PAT in the command line like that - I guess it&amp;rsquo;s in your bash history now. I&amp;rsquo;ll leave you to google how to do that more securely.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.02.37-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Note that you can be logged into several container registries at once. Logging into the ghcr.io won&amp;rsquo;t mean that you&amp;rsquo;ll need to re-logging to DockerHub later; that will still work fine.&lt;/p&gt;
&lt;h2 id="pushing"&gt;Pushing&lt;/h2&gt;
&lt;p&gt;Once that&amp;rsquo;s done, you can push images just as you are used to with DockerHub, with the exception that you need to specify the container registry as part of your image name. It&amp;rsquo;s possible to do that with &lt;code&gt;hub.docker.com&lt;/code&gt; as well, but Docker privileged themselves to make it a default. To use a different registry (in our case ghcr.io) it needs to be included in the image name, along with your GitHub use name.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker push ghcr.io/&amp;lt;github user name&amp;gt;/&amp;lt;container name&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.08.50-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you head to GitHub, and go into &amp;ldquo;Packages&amp;rdquo; instead of &amp;ldquo;Repositories&amp;rdquo;, your container will be there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.24.10-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.24.10-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="pulling"&gt;Pulling&lt;/h2&gt;
&lt;p&gt;Pulling the container is going to be even simpler, log in to the registry with the same command we used above, then just docker pull with the registry in the container name - exactly as suggested in the package page on GitHub above.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker pull ghcr.io/&amp;lt;github user name&amp;gt;/&amp;lt;container name&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.29.33-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.29.33-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Clear explanation of how transformer AI works</title><link>https://blog.iankulin.com/clear-explanation-of-how-transformer-ai-works/</link><pubDate>Mon, 28 Oct 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/clear-explanation-of-how-transformer-ai-works/</guid><description>&lt;p&gt;If you&amp;rsquo;re interested in how generative AI works, check out &lt;a href="https://ishananand.com/"&gt;Ishan Anand&lt;/a&gt;&amp;rsquo;s Youtube series &amp;ldquo;&lt;a href="https://www.youtube.com/@Spreadsheetsareallyouneed"&gt;Spreadsheets are all you need&lt;/a&gt;&amp;rdquo;. He steps through the basics using an Excel spreadsheet that encompasses most of GPT-2. Just doing that is an impressive (and hilarious) feat, but he also has a knack for teaching, so you&amp;rsquo;ll come away with a good understanding of AI and how some of it&amp;rsquo;s limitations manifest.&lt;/p&gt;
&lt;p&gt;Ishan is selling a course, which I guess these are the first three lessons of, and I got a lot out of them. It&amp;rsquo;s also possible to &lt;a href="https://github.com/ianand/spreadsheets-are-all-you-need/releases/tag/v0.6.1"&gt;download the spreadsheet&lt;/a&gt; he uses in the course to play with.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/FyeN5tXMnJ8?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>npm ERR! Exit handler never called!</title><link>https://blog.iankulin.com/npm-err-exit-handler-never-called/</link><pubDate>Mon, 21 Oct 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/npm-err-exit-handler-never-called/</guid><description>&lt;p&gt;I quite like GitHub scanning all my code and sending me security advisories. Here&amp;rsquo;s today&amp;rsquo;s:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-09-27-at-11.31.03-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-09-27-at-11.31.03-am.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With these, and my &lt;a href="https://github.com/dependabot"&gt;dependabot&lt;/a&gt; alerts, fixing them is usually just a matter of pulling down the project, running an &lt;code&gt;npm update&lt;/code&gt;, building any artifacts, then pushing it back up. But today, not so:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-09-27-at-11.36.57-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-09-27-at-11.36.57-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="package-lockjson"&gt;package-lock.json&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s probably worth revisiting what the &lt;code&gt;package-lock.json&lt;/code&gt; does. It contains all the versions of any packages you&amp;rsquo;ve imported, and their dependencies. The idea is that this will make the build reproducible. We don&amp;rsquo;t commit the node_modules folder (that actually contains all that package code), but npm can reproduce it exactly by using the version information in the package-lock.json file. Here&amp;rsquo;s a snippet where you can see all those versions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;node_modules/body-parser&amp;#34;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;version&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;1.20.2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;resolved&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;integrity&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;dependencies&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;bytes&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;3.1.2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;content-type&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;~1.0.5&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;debug&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2.6.9&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;depd&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2.0.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;destroy&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;1.2.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;http-errors&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2.0.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;iconv-lite&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;0.4.24&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;on-finished&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2.4.1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;qs&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;6.11.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;raw-body&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2.5.2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;type-is&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;~1.6.18&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;unpipe&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;engines&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;node&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;gt;= 0.8&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;npm&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;1.2.8000 || &amp;gt;= 1.4.16&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&lt;span style="color:#960050;background-color:#1e0010"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For me, I don&amp;rsquo;t really care that I&amp;rsquo;m using &amp;ldquo;iconv-lite&amp;rdquo; version 0.4.24, but if I&amp;rsquo;m working on a project with someone else, it might be important that we&amp;rsquo;re using the same version so we&amp;rsquo;re not chasing our tails trying to sort out a bug.&lt;/p&gt;
&lt;h3 id="npm-update"&gt;npm update&lt;/h3&gt;
&lt;p&gt;There are some rules about how the versions of packages are entered in &lt;code&gt;package.json&lt;/code&gt;; when we run &lt;code&gt;npm update&lt;/code&gt;, it uses those rules to look in the npm registry to find the most recent version of all the packages it&amp;rsquo;s allowed. Then it updates them in &lt;code&gt;package-lock.json&lt;/code&gt;, and downloads the code into the &lt;code&gt;node_modules&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;This is potentially a substantial change to your app, so you&amp;rsquo;d definitely want to be running your testing process again afterwards.&lt;/p&gt;
&lt;h3 id="the-error"&gt;The Error&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;npm ERR! Exit handler never called!

npm ERR! This is an error with npm itself. Please report this error at:
npm ERR! &amp;lt;https://github.com/npm/cli/issues&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This sounds quite serious, but before you head off to report it, try this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;npm install --no-package-lock
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This just runs the update ignoring the package-lock.json file - as if you&amp;rsquo;d just deleted it. If that works, it was a problem with the &lt;code&gt;package-lock.json&lt;/code&gt; file, which in this context of just wanting all the latest versions we don&amp;rsquo;t care about. We do want to rebuild the &lt;code&gt;package-lock.json&lt;/code&gt; file though, so go ahead and delete it and run &lt;code&gt;npm install&lt;/code&gt; to create a nice new one.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-09-27-at-12.03.23-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-09-27-at-12.03.23-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now your project will have a couple of version changes in those package files. You&amp;rsquo;ll need to redo all your testing and rebuild any Docker images etc, and then you&amp;rsquo;re all up to date and secure again!&lt;/p&gt;</description></item><item><title>Code reuse by publishing to NPM</title><link>https://blog.iankulin.com/code-reuse-by-publishing-to-npm/</link><pubDate>Mon, 14 Oct 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/code-reuse-by-publishing-to-npm/</guid><description>&lt;p&gt;If you find yourself copying over a source file from one Node project to another because it&amp;rsquo;s a handy utility you wrote and are used to using, you&amp;rsquo;re only doing it half right. A better way to do this is to publish your utility to the &lt;a href="https://www.npmjs.com"&gt;Node Package Manager&lt;/a&gt; (NPM). That way you can just import your utility where ever you need it, it will live in the &lt;code&gt;node_modules&lt;/code&gt; of any project that uses it, and most importantly, updates are sorted out automatically - because that&amp;rsquo;s what package managers are good at.&lt;/p&gt;
&lt;p&gt;By the time you are even thinking about this, you&amp;rsquo;ve already gotten used starting your Node projects with &lt;code&gt;npm init&lt;/code&gt; and installing packages with &lt;code&gt;npm install express&lt;/code&gt; when you have your own packages on npm they are handled exactly like that.&lt;/p&gt;
&lt;p&gt;So, how do we get our code up to npm?&lt;/p&gt;
&lt;h3 id="npm-account"&gt;NPM Account&lt;/h3&gt;
&lt;p&gt;You need an account on npm. It doesn&amp;rsquo;t cost anything to host your packages there, though they will be public on the free plan. If you don&amp;rsquo;t have an &lt;a href="https://www.npmjs.com/signup"&gt;account, create one&lt;/a&gt;. It&amp;rsquo;s 2024 so use 2FA.&lt;/p&gt;
&lt;h3 id="create-your-project"&gt;Create your project&lt;/h3&gt;
&lt;p&gt;Make a directory with the same name as your package. All lower case, no spaces, hyphens are allowed. While we&amp;rsquo;re at the command line, let&amp;rsquo;s sign into npm&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir is-even
cd is-even
npm login
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Almost every straightforward name you can think of for your utility code will have been used already on npm. I&amp;rsquo;m not trying to be twitter famous or to get 96,000 downloads of my package per week - I just want to reuse my code conveniently, so I&amp;rsquo;ll scope it to my user name. So my package won&amp;rsquo;t be called &lt;code&gt;is-even&lt;/code&gt; on npm (&lt;a href="https://www.npmjs.com/package/is-even"&gt;famously&lt;/a&gt;, that&amp;rsquo;s already taken), it will be called &lt;code&gt;@iankulin/is-even&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is called &lt;em&gt;scoping&lt;/em&gt; it to our username. When we do that, the project is initialised like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;npm init --scope=iankulin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You&amp;rsquo;ll get asked the questions in a similar way as init-ing a regular node project. &lt;code&gt;index.js&lt;/code&gt; is fine for your file name. You&amp;rsquo;ll end up with something that looks like this in your package.json. Note that I&amp;rsquo;ve added &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;&lt;/code&gt;, since I&amp;rsquo;m all about the ESM this week.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-31-at-5.07.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-31-at-5.07.11-pm.png" width="907" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now we&amp;rsquo;d better write some code. Here&amp;rsquo;s my &lt;code&gt;index.js&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function isEven(num) {
 if (typeof num === &amp;#39;number&amp;#39; &amp;amp;&amp;amp; Number.isInteger(num)) {
 return num % 2 === 0;
 }
 // we&amp;#39;re counting all non-integers and non-numbers as odd
 return false;
}

export { isEven };
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="publishing"&gt;Publishing&lt;/h3&gt;
&lt;p&gt;After extensive testing and refinement, you can push it up to npm:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;npm publish --access public
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And the exciting moment - we can see our package on npm, just like a real coder.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-31-at-5.22.48-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-31-at-5.22.48-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="use-your-package"&gt;Use your package&lt;/h3&gt;
&lt;p&gt;Using the package once it&amp;rsquo;s published is exactly the same as using any npm package: Start a new project, and install the package.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir test-is-even
cd test-is-even
npm init
npm install @iankulin/is-even
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Again, because I&amp;rsquo;m using ESM, I&amp;rsquo;ve added &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;,&lt;/code&gt; to my package.json. And some test code in my &lt;code&gt;index.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-31-at-5.38.38-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>rsync between Synology NAS</title><link>https://blog.iankulin.com/rsync-between-synology-nas/</link><pubDate>Mon, 30 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-between-synology-nas/</guid><description>&lt;p&gt;A while ago, I devised a complicated system where I could drop files in a web interface running on an LXD container and the files would then magically appear in a directory on a remote NAS in the morning. It turned out to not be very robust, and I gave up on it after a while.&lt;/p&gt;
&lt;p&gt;Also, really there should be no need for it - underneath, it was just using &lt;code&gt;rsync&lt;/code&gt; to move the files, so why not just do that direct from one NAS to another? Well, mainly because my NASs are all Synology - which I love, and they&amp;rsquo;ve been great, but in an effort to make them usable by muggles, Synology tend to somewhat complicate things for Linux command line wizards.&lt;/p&gt;
&lt;p&gt;It turns out to be totally possible to command line &lt;code&gt;rsync&lt;/code&gt;, including doing it over Tailscale, but there&amp;rsquo;s a couple of gotchas along the way.&lt;/p&gt;
&lt;h3 id="rsync-the-synology-way"&gt;rsync the Synology way&lt;/h3&gt;
&lt;p&gt;A reasonable question would be why didn&amp;rsquo;t I use the Synology rsync user interface to do all this - it&amp;rsquo;s right there in Control Panel / File Services? I did actually look at doing that, but after five minutes I couldn&amp;rsquo;t figure it out, so yeah na. It&amp;rsquo;s the command line for me.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;p&gt;The plan for the rest of this post is just to run through, in approximate order, the steps you&amp;rsquo;ll need to take to get &lt;code&gt;rsync&lt;/code&gt; working from the command line to sync files between two synology NASs. It&amp;rsquo;s probably also helpful between a real system and a Synology NAS. I&amp;rsquo;m going to talk about the &amp;rsquo;local&amp;rsquo; NAS (where we&amp;rsquo;ll be running the rsync command) and &amp;lsquo;remote&amp;rsquo; one. This is just for convenience - you might have two local or two remote NASs - I don&amp;rsquo;t judge. I&amp;rsquo;m just calling mine &amp;rsquo;local&amp;rsquo; and &amp;lsquo;remote&amp;rsquo; for this post that so you know which device I&amp;rsquo;m talking about.&lt;/p&gt;
&lt;h4 id="ssh"&gt;ssh&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;rsync&lt;/code&gt; works over an ssh connection, so you need to be able to ssh from one NAS to another without entering a password first. To test it, ssh into the local NAS, then without logging out, ssh into the remote NAS from the local one. If that works without asking for a password you&amp;rsquo;ve completed this step and can just ctrl-D to drop back to the local NAS.&lt;/p&gt;
&lt;p&gt;If the issue is that it asked for a password, that just means you need to install your public ssh keys on the remote. I usually do this with the &lt;code&gt;ssh-copy-id&lt;/code&gt; command on regular Linux, Mac and BSD systems, but that&amp;rsquo;s not available at the Synology command line so we&amp;rsquo;ll have to do it the old fashioned way.&lt;/p&gt;
&lt;p&gt;Anything to do with ssh is stored in a hidden directory, &lt;code&gt;.ssh&lt;/code&gt; in a user&amp;rsquo;s home directory. For example you can check you&amp;rsquo;ve got public keys with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ls -la ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These are the keys you want to add to the remote NASs authorised keys, so we&amp;rsquo;ll use ssh (with a password) to add them to the end of that file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh &amp;lt;user&amp;gt;@&amp;lt;remote NAS address&amp;gt; &amp;#39;cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&amp;#39; &amp;lt; ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You need to substitute your remote NASs username and address, so mayby it would look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh nas1_admin@83.78.2.105 &amp;#39;cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&amp;#39; &amp;lt; ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When you execute this, it will ask for the remote password, but once it&amp;rsquo;s worked you should be able to ssh in and it allows that without using a password.&lt;/p&gt;
&lt;h4 id="tailscale-out"&gt;Tailscale out&lt;/h4&gt;
&lt;p&gt;Perhaps you didn&amp;rsquo;t get as far as needing the ssh password, because when you tried to ssh to the remote, ssh didn&amp;rsquo;t even recognise the domain. If you are using Tailscale to connect your devices (which I recommend) then there are two tricks needed.&lt;/p&gt;
&lt;p&gt;Trick one is to get around the fact that since DSM 7, Synology have prevented (for good security reasons) external packages from making outbound connections. So you&amp;rsquo;ll be able to use Tailscale to access the Synology web interface, or even ssh &lt;em&gt;into&lt;/em&gt; it, but you won&amp;rsquo;t be able to ssh &lt;em&gt;out&lt;/em&gt; of it. When I first discovered this, I was running &lt;code&gt;ip a&lt;/code&gt; at the command line in the local NAS and noticed that the tailscale IP was not even listed - it was as if Tailscale wasn&amp;rsquo;t running, but I knew it was since I had ssh&amp;rsquo;d in with the Tailscale address.&lt;/p&gt;
&lt;p&gt;Tailscale have a fix for &lt;a href="https://tailscale.com/kb/1131/synology#enable-outbound-connections"&gt;enabling outbound connections via Tailscale on Synology&lt;/a&gt;, you need to run a thing on reboot to enable the TUN.&lt;/p&gt;
&lt;p&gt;Trick two is that even after you&amp;rsquo;ve done that and rebooted and can see the tailscale interface when you run &lt;code&gt;ip a&lt;/code&gt;, you still won&amp;rsquo;t be able to use the Tailscale &amp;lsquo;magic&amp;rsquo; DNS but will have to use the Tailscale IP address for the remote when you ssh (and later rsync) to it. So I can&amp;rsquo;t use:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ssh nas1_admin@NAS-01&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;as I would normally from my laptop, I have to use &lt;code&gt;ssh nas1_admin@104.43.22.181&lt;/code&gt; If you are not sure of the Tailscale IP for your remote, have a look at your &lt;a href="https://login.tailscale.com/admin/machines"&gt;machines list&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="turn-rsync-on"&gt;Turn rsync on&lt;/h4&gt;
&lt;p&gt;Via the web interface on both Synologys, you&amp;rsquo;ll need to enable rsync. The setting is in &lt;code&gt;Control Panel | File Services | rsync&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-25-at-1.56.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-25-at-1.56.57-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Leave the port as 22 and don&amp;rsquo;t bother with the other settings, but do hit &lt;code&gt;Apply&lt;/code&gt; at the bottom to save the change.&lt;/p&gt;
&lt;h4 id="give-it-a-try"&gt;Give it a try&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;re now at the stage where you should be able to ssh into the remote NAS from the local one without being asked for a password, and rsync is turned on both ends, so in theory, you should be able to do something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m not going to go into all the flags for rsync (the internet has plenty of good guides for that) except to say that the &amp;rsquo;n&amp;rsquo; on the end of the flags in the command above means that no files will actually be moved, it will do a &amp;lsquo;dry run&amp;rsquo; and tell you what it would have done if you let it.&lt;/p&gt;
&lt;p&gt;Note that if you have a jazillion files, this could take a while, you might be better to limit it to a smaller sub directory such as &lt;code&gt;/volume1/media/music/napster/Metallica&lt;/code&gt;/&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/yo-dawg-heard-you.jpg" width="497" alt=""&gt;
&lt;p&gt;The other bit of free rsync advice I&amp;rsquo;ll give you is to look carefully at the source and destination directories in the command above. The source sub directory has a trailing &amp;lsquo;/&amp;rsquo;, the destination does not. If you mess this up you&amp;rsquo;ll be making directories inside your directories dawg.&lt;/p&gt;
&lt;p&gt;In theory once you&amp;rsquo;re at this point, everything should work. But here&amp;rsquo;s a couple of other bumps / thoughts.&lt;/p&gt;
&lt;h4 id="eadir"&gt;@eaDir&lt;/h4&gt;
&lt;p&gt;Synology has a bunch of hidden directories with metadata stuff. My advice is don&amp;rsquo;t mess with them, but also don&amp;rsquo;t sync them over either. Tell rsync to ignore them. Same for the recycle bin.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="permissions"&gt;Permissions&lt;/h3&gt;
&lt;p&gt;The user that you&amp;rsquo;re ssh&amp;rsquo;ing with needs to have permissions to all the places you are rsync&amp;rsquo;ing files to. Even though I&amp;rsquo;ve only ever had one user for each of my Synology NAS&amp;rsquo;s, and everything has been done by that one user either through the web GUI or command line, the files and directories on my NAS have a mixture of owners (my user and root) and permissions. Someone smarter than me could probably figure out why - and if your NAS has to include files from multiple users etc, you are going to need to do that. Because I like sledgehammers, all I did was ssh into the remote and:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo chown -R nas1_admin:users /volume1/media
sudo chmod -R 775 /volume1/media
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="throttling-bandwidth"&gt;Throttling bandwidth&lt;/h4&gt;
&lt;p&gt;If I saturate the downlink at my remote site while I&amp;rsquo;m rsync-ing a bunch of files, the users there will be unhappy when they can&amp;rsquo;t stream video reliability or if they&amp;rsquo;re getting killed in online games due to lag.&lt;/p&gt;
&lt;p&gt;rsync has a flag for that. If we want to limit the transfer bandwidth to 500KB it could look like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="get-destructive"&gt;Get destructive&lt;/h4&gt;
&lt;p&gt;If you only want to sync the files one way from your local to remote, then we can add a flag that will delete any files on the remote machine that are not present on the local one. Obviously use with care, and run with the -n flag first to see what&amp;rsquo;s going to get chopped.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1 --del
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="do-something-else"&gt;Do something else&lt;/h4&gt;
&lt;p&gt;The first few times you do this, it will be exciting watching the terminal window as rsync carefully checks for each file and directory and copies them over as needed, and it will also be helpful to see what errors might pop up so you can sort them out.&lt;/p&gt;
&lt;p&gt;Eventually though, it will be so routine and error free you&amp;rsquo;d rather do something else, so you&amp;rsquo;ll wander off and leave it, then curse when you return to find your laptop turned itself off due to inactivity and wrecked the rsync. Don&amp;rsquo;t panic, rsync is robust and will pick right up next time you run it without damaging any files, but you might also consider doing it all on remote control.&lt;/p&gt;
&lt;p&gt;On the local NAS, create a file called &lt;code&gt;sync-media.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash

nohup rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1 &amp;gt; sync_media.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;chmod +x sync_media.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you run it &lt;code&gt;./sync-media.sh&lt;/code&gt; you can log off and let it do it&amp;rsquo;s thing.&lt;/p&gt;</description></item><item><title>Containerised NGINX Proxy Manager &amp; the 502 error</title><link>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</link><pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png" width="695" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re used to running NGINX Proxy Manager in front of your web apps, and switch to running it in a container, you&amp;rsquo;re going to need to learn a little about Docker networks to get everything connected. If you just do your regular setup, and direct the proxy for an address to &lt;code&gt;127.0.0.1:&amp;lt;some port&amp;gt;&lt;/code&gt;, it won&amp;rsquo;t exist, and you&amp;rsquo;ll visit your page to find the &amp;ldquo;502 Bad Gateway openresty&amp;rdquo; message.&lt;/p&gt;
&lt;p&gt;If you pause to think for a second it will be obvious why - with NGINX Proxy Manager (I&amp;rsquo;m going to start calling it NPM to save myself some typing) &lt;em&gt;inside&lt;/em&gt; a container, any addresses you&amp;rsquo;re entering into the web interface when setting up proxys are &lt;em&gt;inside&lt;/em&gt; the NPM container. &lt;code&gt;127.0.0.1&lt;/code&gt; from that point of view refers to the inside of the NPM container, and not the host, so you exposing a port from your container to the host is not going to work.&lt;/p&gt;
&lt;p&gt;The fix for this is pretty simple, but first let&amp;rsquo;s look at the exception.&lt;/p&gt;
&lt;h3 id="the-exception"&gt;The Exception&lt;/h3&gt;
&lt;p&gt;Usually the very first proxy I add in NPM is to it&amp;rsquo;s own admin interface on port 81. Since this &lt;em&gt;is&lt;/em&gt; inside the NPM container, the setup looks exactly the same as if you were running on the host, and may well be why you were lulled into a false sense of familiarity.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a little bit confusing, since we can also manually access NPM from &lt;code&gt;http://127.0.0.1:81&lt;/code&gt; on the host if we&amp;rsquo;ve exposed port 81 in our compose file, but it&amp;rsquo;s actually a different route. In fact, we could hide port 81 from the host, and the setting above will still work.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s my compose file, notice I haven&amp;rsquo;t exposed port 81&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 nginx-proxy-manager:
 image: &amp;#39;jc21/nginx-proxy-manager:latest&amp;#39;
 container_name: nginx-proxy-manager
 restart: unless-stopped
 ports:
 - &amp;#39;80:80&amp;#39;
 - &amp;#39;443:443&amp;#39;
 volumes:
 - ./data:/data
 - ./letsencrypt:/etc/letsencrypt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So if we try to access port 81 from the host, it won&amp;rsquo;t be answering.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But inside the container, 127.0.0.1 refers to itself, so if we open a shell into the container, the URL http://127.0.0.1:81 refers to something that exists, from there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="getting-out"&gt;Getting Out&lt;/h3&gt;
&lt;p&gt;So how can our NPM point to a service that&amp;rsquo;s running in another container? You are probably used to specifying ports: in your compose file to expose internal container ports to a host, but that doesn&amp;rsquo;t really help us since NPM in a container can not easily access the host&amp;rsquo;s ports. What we&amp;rsquo;d really like is for NPM to be able to access the second container&amp;rsquo;s ports. And in an ideal world, we&amp;rsquo;d like that to be the only way to access them - that way all the access to our second container service is forced through our proxy. Sounds like security no?&lt;/p&gt;
&lt;p&gt;Turns out this is simple thanks to the magic of Docker networks.&lt;/p&gt;
&lt;p&gt;You can get a fair way with Docker without really thinking or knowing about &lt;a href="https://docs.docker.com/engine/network/"&gt;Docker networks&lt;/a&gt;, and I&amp;rsquo;m really only covering the very basics here - you should probably invest some time in learning about this sometime. Meanwhile, lets run the &lt;code&gt;docker network ls&lt;/code&gt; command and see what networks we&amp;rsquo;ve got.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That network &lt;code&gt;nginx-proxy-manager_default&lt;/code&gt; is the one we&amp;rsquo;re interested in. Its name is just the container name with &amp;ldquo;_default&amp;rdquo; added on the end. What we need to do is just make sure the second container is included in that same network. That&amp;rsquo;s a matter of declaring the external network with that name, and including it in our service definition. I&amp;rsquo;m going to use an NGINX server in my example, but it could be anything. Here&amp;rsquo;s the compose for the second container.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 nginx-example.com:
 image: nginx
 container_name: nginx-example.com
 volumes:
 - ./www:/usr/share/nginx/html
 - ./conf/:/etc/nginx/conf.d/:ro
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that I haven&amp;rsquo;t exposed any ports to the host here; We&amp;rsquo;re not going to need them since we&amp;rsquo;ll access this container direct from the NPM container via the internal Docker network docker created for us. That little network even contains a DNS server, so we don&amp;rsquo;t even need to worry about the container&amp;rsquo;s IP addresses, we can just use their names.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.52.54-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So any web requests to &amp;ldquo;example.com&amp;rdquo; arriving at our host go to NPM (since I&amp;rsquo;ve exposed ports 80 &amp;amp; 443 - see the top compose file). Then using the proxy I&amp;rsquo;ve added above, they are forwarded to the container named &amp;ldquo;nginx-example.com&amp;rdquo; which is a DNS record inside the Docker network that Docker created for us, and which both the NPM, and my service, containers are members of.&lt;/p&gt;</description></item><item><title>Uploading files to a web app with Node</title><link>https://blog.iankulin.com/uploading-files-to-a-web-app-with-node/</link><pubDate>Mon, 02 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/uploading-files-to-a-web-app-with-node/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-3.09.38-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;My default approach to web apps at the moment is Node/Express SSR. I needed to have users be able to upload files this week, and as usual there&amp;rsquo;s an express middleware that makes it trivial. This post just steps through using &lt;a href="https://github.com/expressjs/multer"&gt;multer&lt;/a&gt; to make it simple to enable file uploads on your website.&lt;/p&gt;
&lt;h3 id="express--middleware"&gt;Express &amp;amp; middleware&lt;/h3&gt;
&lt;p&gt;Before we look at file uploading, it&amp;rsquo;s worth just explaining how it fits with the other tools we&amp;rsquo;re using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en"&gt;Node&lt;/a&gt; - A server runtime that executes javascript. It&amp;rsquo;s a good option for writing web apps if you already know JavaScript from frontend. It has an extensive ecosystem of packages that are installed managed with &lt;a href="https://www.npmjs.com/"&gt;NPM&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://expressjs.com/"&gt;Express&lt;/a&gt; - A node package that encapsulates a lot of functionality around handling web requests to make it much simpler for the developer. In particular it makes setting up routes easier and introduces the concept of middleware.&lt;/li&gt;
&lt;li&gt;Middleware - in Express we can install middleware - packages that intercept web requests and deal with them or pass them on. Commonly, they work to pull parts of requests out and expose them in developer friendly ways, but they can also do things like apply security rules to requests to allow or deny them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Multer is express middleware to handle data from a web request that includes &amp;ldquo;multipart/form-data&amp;rdquo; - which is what we use for file uploads.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;p&gt;Since this is quite a small topic, and I&amp;rsquo;ve started by saying what Node is, I&amp;rsquo;ll pitch these explanations for beginners. I am going to assume you&amp;rsquo;ve been able to install VSCode or some other IDE that you know how to use, that you&amp;rsquo;ve installed Node on the machine you&amp;rsquo;re working on, and you&amp;rsquo;ve got some familiarity with JavaScript &amp;amp; HTML.&lt;/p&gt;
&lt;h4 id="project-setup"&gt;Project Setup&lt;/h4&gt;
&lt;p&gt;Create a directory for your project - I&amp;rsquo;m calling mine &amp;lsquo;file-upload&amp;rsquo; which will be the name of this project. Open VSCode in that directory, and run:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;npm install express multer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After a few seconds NPM should have created a couple of files (&lt;code&gt;package.json&lt;/code&gt; &amp;amp; &lt;code&gt;package-lock.json&lt;/code&gt;) and a directory called &lt;code&gt;node_modules&lt;/code&gt;. &lt;code&gt;node_modules&lt;/code&gt; contains all the library code we&amp;rsquo;ll be using, and the package files have some versioning information used by NPM.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.00.01-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.00.01-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is going to be a server that responds to web requests, so we better write a skeleton for that. Create a file called &lt;code&gt;server.js&lt;/code&gt; and add this code.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#34;express&amp;#34;);
const app = express();

// handle the default route
app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 res.send(&amp;#34;hello world&amp;#34;);
});

// Start the server
app.listen(3000, () =&amp;gt; {
 console.log(&amp;#34;Listening on http://127.0.0.1:3000&amp;#34;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To start our server, we need to enter this in the terminal:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;node server.js
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.11.02-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now if you visit the web address &lt;a href="http://127.0.0.1:3000"&gt;http://127.0.0.1:3000&lt;/a&gt; in your web browser, you should see the message &amp;ldquo;hello world&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.15.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.15.28-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To stop the server hold down control and press &amp;lsquo;C&amp;rsquo; in the terminal window.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.18.49-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.18.49-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="serving-a-html-file"&gt;Serving a HTML file&lt;/h4&gt;
&lt;p&gt;Sending that &amp;lsquo;hello world&amp;rsquo; text is cool and all, but ideally, our web server would serve a web page. Let&amp;rsquo;s alter the default route of our server to do that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#34;express&amp;#34;);
const app = express();

// handle the default route
app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 res.sendFile(__dirname + &amp;#34;/index.html&amp;#34;);
});

// Start the server
app.listen(3000, () =&amp;gt; {
 console.log(&amp;#34;Listening on http://127.0.0.1:3000&amp;#34;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will send the file &lt;code&gt;index.html&lt;/code&gt; instead of just the &amp;lsquo;hello world&amp;rsquo; text from before. The &lt;code&gt;__dirname&lt;/code&gt; part is just saying the index.html file will be in the same directory as our app. We better also create an &lt;code&gt;index.html&lt;/code&gt; there so it can be sent.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
 &amp;lt;head&amp;gt;
 &amp;lt;title&amp;gt;Hello&amp;lt;/title&amp;gt;
 &amp;lt;/head&amp;gt;
 &amp;lt;body&amp;gt;
 Hello world
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ah yes. That&amp;rsquo;s much more professional.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.28.36-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-2.28.36-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="multer"&gt;Multer&lt;/h4&gt;
&lt;p&gt;Now it does get a bit more complicated. Multer can use several different types of storage. For example you might want to use an S3 bucket on AWS. We have simpler tastes and just want to store files as files on our host, but the point is the storage engines can be swapped in and out for Multer, so we need to create a Multer storage engine, then a Multer &lt;code&gt;upload&lt;/code&gt; that uses that storage.&lt;/p&gt;
&lt;p&gt;Then the Multer &lt;code&gt;upload&lt;/code&gt; is used in the route for the web request that contains our file. Possibly this explanation is more complicated than the code. Let&amp;rsquo;s have a look:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#34;express&amp;#34;);
const multer = require(&amp;#34;multer&amp;#34;);
const fs = require(&amp;#34;fs&amp;#34;);

const app = express();

// set up storage engine for Multer
const storage = multer.diskStorage({
 destination: function (req, file, cb) {
 cb(null, &amp;#34;data/&amp;#34;);
 },
 filename: function (req, file, cb) {
 cb(null, file.originalname);
 },
});

const upload = multer({ storage: storage });

// create the &amp;#39;data&amp;#39; directory if it doesn&amp;#39;t exist 
if (!fs.existsSync(&amp;#34;./data&amp;#34;)) {
 fs.mkdirSync(&amp;#34;./data&amp;#34;);
}

// handle the default route
app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 res.sendFile(__dirname + &amp;#34;/index.html&amp;#34;);
});

// handle the upload route
app.post(&amp;#34;/upload&amp;#34;, upload.single(&amp;#34;file&amp;#34;), (req, res) =&amp;gt; {
 if (!req.file) {
 return res.status(400).send(&amp;#34;No file uploaded.&amp;#34;);
 }
 res.send(&amp;#34;File uploaded successfully.&amp;#34;);
});

// start the server
app.listen(3000, () =&amp;gt; {
 console.log(&amp;#34;Listening on http://127.0.0.1:3000&amp;#34;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;ll look at each of these new fragments one at a time:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const multer = require(&amp;#34;multer&amp;#34;);
const fs = require(&amp;#34;fs&amp;#34;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This just pulls in two libraries - &lt;code&gt;multer&lt;/code&gt; for handling the uploads, and &lt;code&gt;fs&lt;/code&gt; which just has some file operations that we&amp;rsquo;ll use for creating the directory for our data.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// set up storage engine for Multer
const storage = multer.diskStorage({
 destination: function (req, file, cb) {
 cb(null, &amp;#34;data/&amp;#34;);
 },
 filename: function (req, file, cb) {
 cb(null, file.originalname);
 },
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As discussed before, we need to create a storage engine, and these can be of different types. This is the diskStorage type. We&amp;rsquo;ll save the file to the ./data directory and use the original filename it had on the user&amp;rsquo;s machine.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const upload = multer({ storage: storage });
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates the upload handler with that storage engine we created in the previous step.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// create the &amp;#39;data&amp;#39; directory if it doesn&amp;#39;t exist 
if (!fs.existsSync(&amp;#34;./data&amp;#34;)) {
 fs.mkdirSync(&amp;#34;./data&amp;#34;);
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;d get an error if multer tries to write to the directory we told it to when we created the storage engine and the directory did not exist. So we check for that here and create it if needed.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// handle the upload route
app.post(&amp;#34;/upload&amp;#34;, upload.single(&amp;#34;file&amp;#34;), (req, res) =&amp;gt; {
 if (!req.file) {
 return res.status(400).send(&amp;#34;No file uploaded.&amp;#34;);
 }
 res.send(&amp;#34;File uploaded successfully.&amp;#34;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&amp;rsquo;s our route handler. Any POST requests to hrrp://127.0.0.1:3000/upload will be sent here. It passes off the file contained in the request to our Multer upload, and if that all works, it sends a message back to the browser.&lt;/p&gt;
&lt;h4 id="html-form"&gt;HTML form&lt;/h4&gt;
&lt;p&gt;We need a way for the /upload route to be hit with the file data, and that&amp;rsquo;s done by submitting a form with the file data. Let&amp;rsquo;s edit out index.html to do that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
 &amp;lt;head&amp;gt;
 &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34; /&amp;gt;
 &amp;lt;title&amp;gt;File Upload&amp;lt;/title&amp;gt;
 &amp;lt;/head&amp;gt;
 &amp;lt;body&amp;gt;
 &amp;lt;form action=&amp;#34;/upload&amp;#34; method=&amp;#34;post&amp;#34; enctype=&amp;#34;multipart/form-data&amp;#34;&amp;gt;
 &amp;lt;input type=&amp;#34;file&amp;#34; name=&amp;#34;file&amp;#34; id=&amp;#34;file&amp;#34; /&amp;gt;
 &amp;lt;input type=&amp;#34;submit&amp;#34; value=&amp;#34;Upload&amp;#34; /&amp;gt;
 &amp;lt;/form&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That &lt;code&gt;enctype&lt;/code&gt; of &lt;code&gt;&amp;quot;multipart/form-data&amp;quot;&lt;/code&gt; is important. That&amp;rsquo;s what Multer wants to see. Apart from that, this is just a form with two buttons. The first one lets the user choose a file, and the second &amp;ldquo;Upload&amp;rdquo; button submits it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-3.00.01-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-3.00.01-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Clicking on the &amp;ldquo;Browse&amp;hellip;&amp;rdquo; button will open the file selection dialog for your operating system. Once you&amp;rsquo;ve selected a file, the name will be shown.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-3.03.39-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-3.03.39-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If we press the Upload button, that file will now be sent to the server, and should appear in the &lt;code&gt;data&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-3.07.20-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-18-at-3.07.20-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This is about the simplest I think we can make this. As always there are a heap of other considerations when implementing this in a live app. For example, I feel uncomfortable using the user submitted file name - perhaps they could manipulate this to be something like &lt;code&gt;./../server.js&lt;/code&gt; and overwrite our source code. We should probably sanitize that, or just replace it with a name we generate. We also should be thinking about restricting the size and or type of files the user can upload, and gracefully handle the errors if we run out of space or some other disaster befalls our system.&lt;/p&gt;</description></item><item><title>Authentication basics for Node apps</title><link>https://blog.iankulin.com/authentication-basics-for-node-apps/</link><pubDate>Mon, 19 Aug 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/authentication-basics-for-node-apps/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/calahorra-tower-torre-de-la-calahorra-in-cordoba-spain-a-fortified-gate-built-during-the-late-12th-century-by-the-almohads-to-protect-the-nearby-roman-bridge-in-the-historic-center-of-cordoba-andalusia-spain-ECsukeqrDoo"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-10-at-8.59.01-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Pretty much every serious web app needs to include a way for users to log in securely and to be served their content. Since there&amp;rsquo;s a lot of complexity in this, it&amp;rsquo;s highly advisable to use good libraries to support this. In a future post we&amp;rsquo;re going to use those libraries, but first I want to explain what&amp;rsquo;s happening at the lower level and tease out some of the concepts as we build a secure system from the ground up.&lt;/p&gt;
&lt;h3 id="http"&gt;HTTP&lt;/h3&gt;
&lt;p&gt;Before we dive into our authentication story, it&amp;rsquo;s worth thinking about how HTTP works and putting some names to things. We often don&amp;rsquo;t think too much about this level because the mechanics are most abstracted away for us by libraries such as express.js.&lt;/p&gt;
&lt;p&gt;A HTTP &lt;em&gt;request&lt;/em&gt; is just a bunch of lines of text arriving at TCP port 80. It&amp;rsquo;s an agreed on &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-example-message-exchange"&gt;Internet standard&lt;/a&gt; originally written by &lt;a href="https://en.wikipedia.org/wiki/Tim_Berners-Lee"&gt;Tim Berners-Lee&lt;/a&gt;. The request will include the type of request it is (GET, POST etc), the resource being requested (usually a web-page) - these make up the &lt;em&gt;request line&lt;/em&gt;. Then there will be some lines of data called the &lt;em&gt;header&lt;/em&gt; that might include things like the type of browser making the request, and optionally a &lt;em&gt;body&lt;/em&gt; of the request. The body might contain form data being submitted or a JSON description of an object. If there is a body, there will be a blank line separating it from the header.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;GET /hello.txt HTTP/1.1
User-Agent: curl/7.64.1
Host: www.example.com
Accept-Language: en, mi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Similarly, the HTTP &lt;em&gt;response&lt;/em&gt; is just some lines of text. A &lt;em&gt;status line&lt;/em&gt; (which includes the famous &lt;em&gt;status code&lt;/em&gt; such as 404), some &lt;em&gt;headers&lt;/em&gt; and the &lt;em&gt;body&lt;/em&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: &amp;#34;34aa387-d-1568eb00&amp;#34;
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

Hello World! My content includes a trailing CRLF.
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="sessions"&gt;Sessions&lt;/h3&gt;
&lt;p&gt;A web app might be serving thousands of users, so we need some way for the server to know which user it is talking to. If our app is a todo list, we don&amp;rsquo;t want to be showing Jane&amp;rsquo;s todo items to Fred - each user only wants to see their own items. A common way of doing this is that the browser making requests to the server could send a bit of text along with each request. These little bits of text are called &amp;lsquo;cookies&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;In a very simple example, the cookie could contain the name of our user - for example &amp;lsquo;Fred&amp;rsquo; or &amp;lsquo;Jane&amp;rsquo;. Then when the server received each request, it could read the cookie to know which user was making the request. Here&amp;rsquo;s our code:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#39;express&amp;#39;);

const app = express();

// Route to handle requests
app.get(&amp;#39;/&amp;#39;, (req, res) =&amp;gt; {
 if (req.headers.cookie &amp;amp;&amp;amp; req.headers.cookie.includes(&amp;#39;name=Fred&amp;#39;)) {
 res.send(&amp;#39;Hello Fred!&amp;#39;);
 } else if (req.headers.cookie &amp;amp;&amp;amp; req.headers.cookie.includes(&amp;#39;name=Jane&amp;#39;)) {
 res.send(&amp;#39;Hello Jane!&amp;#39;);
 } else {
 res.send(&amp;#39;Hello stranger!&amp;#39;);
 }
});

// Start the server
const PORT = 3000;
app.listen(PORT, () =&amp;gt; {
 console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The cookie is just a line of text included in the header of the request. Perhaps the request looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;GET / HTTP/1.1
Accept: application/json, text/plain, */*
Cookie: name=Fred
User-Agent: axios/1.5.1
Accept-Encoding: gzip, compress, deflate, br
Host: 127.0.0.1:3000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At the user&amp;rsquo;s end the cookie is probably stored in an sqlite database - this implementation detail is left up to the browser. When the users browser sends the request, it checks to see if it&amp;rsquo;s got a cookie for this host and encodes it into the header of the request.&lt;/p&gt;
&lt;h4 id="testing-this-code"&gt;Testing this code&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s no simple way to test the server code above since regular browsers don&amp;rsquo;t allow us to set the cookie values. There are however a number of tools that can send customised requests. Some examples of these API testing tools are &lt;a href="https://www.postman.com/"&gt;Postman&lt;/a&gt; and Insomnia. Since the &lt;a href="https://news.ycombinator.com/item?id=37680126"&gt;Insomnia rug-pull&lt;/a&gt;, I&amp;rsquo;ve been a big fan of &lt;a href="https://www.usebruno.com/"&gt;Bruno&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All of these tools allow you to specify the URL, the type of request, and any header or body to go with it. They can make the call to the server and show the results.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/brunoexample.png" width="1000" alt=""&gt;
&lt;h3 id="setting-a-cookie"&gt;Setting a Cookie&lt;/h3&gt;
&lt;p&gt;Our server as it stands at the moment is not very secure. Any hacker can just change the value of the cookie to see the content intended for Fred or Jane. We&amp;rsquo;ll get to authentication eventually, and when we do, we&amp;rsquo;ll need to be able to &lt;em&gt;set&lt;/em&gt; a cookie in the client. How does that work?&lt;/p&gt;
&lt;p&gt;Again, we&amp;rsquo;ll npm install a little library to assist us. &lt;a href="https://github.com/expressjs/cookie-parser#readme"&gt;cookie-parser&lt;/a&gt; is some middleware that lets us easily work with cookies. For the demonstration we&amp;rsquo;ll just add some routes to set the name to &amp;lsquo;Jane&amp;rsquo; or to clear it. Setting it to &amp;lsquo;Jane&amp;rsquo; will look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// Route to set a cookie for &amp;#39;Jane&amp;#39;
app.get(&amp;#34;/setuserjane&amp;#34;, (req, res) =&amp;gt; {
 res.cookie(&amp;#34;name&amp;#34;, &amp;#34;Jane&amp;#34;); // Set a cookie named &amp;#39;name&amp;#39; with value &amp;#39;Jane&amp;#39;
 res.send(&amp;#34;Cookie set for Jane&amp;#34;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And clearing it, like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// Route to clear the &amp;#39;name&amp;#39; cookie
app.get(&amp;#34;/clearuser&amp;#34;, (req, res) =&amp;gt; {
 res.clearCookie(&amp;#34;name&amp;#34;);
 res.send(&amp;#34;Cookie cleared&amp;#34;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And since we&amp;rsquo;re using cookie-parser, we may as well use it for reading the cookie to tidy things up a bit as well&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#34;express&amp;#34;);
const cookieParser = require(&amp;#34;cookie-parser&amp;#34;);

const app = express();

// cookie middleware
app.use(cookieParser());

app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 if (req.cookies.name === &amp;#34;Fred&amp;#34;) {
 res.send(&amp;#34;Hello Fred!&amp;#34;);
 } else if (req.cookies.name === &amp;#34;Jane&amp;#34;) {
 res.send(&amp;#34;Hello Jane!&amp;#34;);
 } else {
 res.send(&amp;#34;Hello stranger!&amp;#34;);
 }
});

// Route to set a cookie for &amp;#39;Jane&amp;#39;
app.get(&amp;#34;/setuserjane&amp;#34;, (req, res) =&amp;gt; {
 res.cookie(&amp;#34;name&amp;#34;, &amp;#34;Jane&amp;#34;);
 res.send(&amp;#34;Cookie set for Jane&amp;#34;);
});

// Route to clear the &amp;#39;name&amp;#39; cookie
app.get(&amp;#34;/clearuser&amp;#34;, (req, res) =&amp;gt; {
 res.clearCookie(&amp;#34;name&amp;#34;);
 res.send(&amp;#34;Cookie cleared&amp;#34;);
});

// Start the server
const PORT = 3000;
app.listen(PORT, () =&amp;gt; {
 console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With this code, we can just use a regular browser for testing. Visiting &lt;code&gt;127.0.0.1:3000/clearuser&lt;/code&gt; will delete the &lt;code&gt;name&lt;/code&gt; cookie, which we could test by visiting &lt;code&gt;127.0.0.1:3000&lt;/code&gt; and getting the &amp;ldquo;Hello stranger!&amp;rdquo; message. If we then go to &lt;code&gt;127.0.0.1:3000/setuserjane&lt;/code&gt; and back to &lt;code&gt;127.0.0.1:3000&lt;/code&gt; we&amp;rsquo;ll see &amp;ldquo;Hello Jane!&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="session-id"&gt;Session ID&lt;/h3&gt;
&lt;p&gt;Clearly this setup is still insecure since a hacker can easily just include a name cookie to pretend to be any particular user. A better system would be to store a unique ID in the cookie, then match that internally to a particular user. This means we&amp;rsquo;d have to maintain the links between each GUID and user on the server, but it would massively reduce the chance of a hacker being able to pretend to be a particular user since the chance of correctly guessing a GUID would be very low.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s think about what we&amp;rsquo;d need to do to make this work for /setuserjane.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generate a unique ID&lt;/li&gt;
&lt;li&gt;save that ID along with &amp;lsquo;Jane&amp;rsquo; in the local store&lt;/li&gt;
&lt;li&gt;save the UID to the cookie to go back to the browser&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/setuserjane&amp;#34;, (req, res) =&amp;gt; {
 const sessionId = uuidv4(); // Generate a new GUID
 sessions.push({ sessionId, name: &amp;#34;Jane&amp;#34; });
 res.cookie(&amp;#34;sessionId&amp;#34;, sessionId);
 res.send(&amp;#34;Session set for Jane&amp;#34;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then when we needed to check who the user was at a route, we&amp;rsquo;d need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;extract the session ID from the cookie if there is one&lt;/li&gt;
&lt;li&gt;look it up in the server&amp;rsquo;s session store&lt;/li&gt;
&lt;li&gt;use that to identify the name&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 const sessionId = req.cookies.sessionId;
 const session = sessions.find(s =&amp;gt; s.sessionId === sessionId);

 if (session) {
 res.send(`Hello ${session.name}!`);
 } else {
 res.send(&amp;#34;Hello stranger!&amp;#34;);
 }
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&amp;rsquo;s the whole thing. The store of session id:name keypairs is just an array of objects (so it will be wiped on every server restart), and we&amp;rsquo;re using the uuid library to generate globally unique ids.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#34;express&amp;#34;);
const cookieParser = require(&amp;#34;cookie-parser&amp;#34;);
const { v4: uuidv4 } = require(&amp;#34;uuid&amp;#34;);

const app = express();

// cookie middleware
app.use(cookieParser());

// Array to store session objects
const sessions = [];

app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 const sessionId = req.cookies.sessionId;
 const session = sessions.find(s =&amp;gt; s.sessionId === sessionId);

 if (session) {
 res.send(`Hello ${session.name}!`);
 } else {
 res.send(&amp;#34;Hello stranger!&amp;#34;);
 }
});

// Route to set a session for &amp;#39;Jane&amp;#39;
app.get(&amp;#34;/setuserjane&amp;#34;, (req, res) =&amp;gt; {
 const sessionId = uuidv4(); // Generate a new GUID
 sessions.push({ sessionId, name: &amp;#34;Jane&amp;#34; });
 res.cookie(&amp;#34;sessionId&amp;#34;, sessionId);
 res.send(&amp;#34;Session set for Jane&amp;#34;);
});

// Route to clear the session
app.get(&amp;#34;/clearuser&amp;#34;, (req, res) =&amp;gt; {
 const sessionId = req.cookies.sessionId;
 const index = sessions.findIndex(s =&amp;gt; s.sessionId === sessionId);
 if (index !== -1) {
 sessions.splice(index, 1);
 }
 res.clearCookie(&amp;#34;sessionId&amp;#34;);
 res.send(&amp;#34;Session cleared&amp;#34;);
});

// Start the server
const PORT = 3000;
app.listen(PORT, () =&amp;gt; {
 console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="express-session"&gt;express-session&lt;/h2&gt;
&lt;p&gt;The code above is a great improvement, however in practice, instead of managing session ids ourselves, we&amp;rsquo;d make use of express-session. Although general good practice is to avoid dependencies, when we&amp;rsquo;re working with security related code, it&amp;rsquo;s often advisable to use a trusted library since they will have already dealt with a lot of the edge cases and potential weaknesses.&lt;/p&gt;
&lt;p&gt;This is the case with &lt;code&gt;express-session&lt;/code&gt; which does basically what we have above, but also deals with potential cross-site scripting, regenerates session id&amp;rsquo;s to avoid fixation attacks, and signs the cookies to reduce the chance of session data being tampered with. express-session will also handle the storage for the key value pairs for us.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#34;express&amp;#34;);
const cookieParser = require(&amp;#34;cookie-parser&amp;#34;);
const session = require(&amp;#34;express-session&amp;#34;);

const app = express();

// cookie middleware
app.use(cookieParser());

// session middleware
app.use(
 session({
 secret: &amp;#34;REtKU9xyvahuHGd3&amp;#34;, // Replace with a strong secret key
 resave: false,
 saveUninitialized: true,
 cookie: { secure: false }, // Set to true if using HTTPS
 })
);

app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 if (req.session.name) {
 res.send(`Hello ${req.session.name}!`);
 } else {
 res.send(&amp;#34;Hello stranger!&amp;#34;);
 }
});

// Route to set a session for &amp;#39;Jane&amp;#39;
app.get(&amp;#34;/setuserjane&amp;#34;, (req, res) =&amp;gt; {
 req.session.name = &amp;#34;Jane&amp;#34;;
 res.send(&amp;#34;Session set for Jane&amp;#34;);
});

// Route to clear the session
app.get(&amp;#34;/clearuser&amp;#34;, (req, res) =&amp;gt; {
 req.session.destroy((err) =&amp;gt; {
 if (err) {
 res.send(&amp;#34;Error clearing session&amp;#34;);
 } else {
 res.send(&amp;#34;Session cleared&amp;#34;);
 }
 });
});

// Start the server
const PORT = 3000;
app.listen(PORT, () =&amp;gt; {
 console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="authentication-flow"&gt;Authentication flow&lt;/h3&gt;
&lt;p&gt;Everyone in the world by now is familiar with having to use a username and password to sign into a web app and use it. If we think about how that is going to work with the session ID, it would be something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a user tries to access a route that needs authorisation, we check the session object to see if there&amp;rsquo;s a logged in user attached to it.&lt;/li&gt;
&lt;li&gt;If there is, then the route is served, if not they are redirected to a log in page&lt;/li&gt;
&lt;li&gt;At the log in page, we take a username and password, and check it against an internal store. If they match, we update the session to identify the user&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 if (req.session.name) {
 res.send(`Hello ${req.session.name}!`);
 } else {
 res.render(&amp;#34;login.ejs&amp;#34;);
 }
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m using the EJS templating system for this app because it will be handy for later. I&amp;rsquo;m not going to explain it more here other than to say you can just imagine the above is loading the login form HTML. In fact, it just looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
 &amp;lt;head&amp;gt;
 &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34; /&amp;gt;
 &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34; /&amp;gt;
 &amp;lt;title&amp;gt;Login&amp;lt;/title&amp;gt;
 &amp;lt;/head&amp;gt;
 &amp;lt;body&amp;gt;
 &amp;lt;h1&amp;gt;Login&amp;lt;/h1&amp;gt;
 &amp;lt;form action=&amp;#34;/login&amp;#34; method=&amp;#34;post&amp;#34;&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;label for=&amp;#34;username&amp;#34;&amp;gt;Username:&amp;lt;/label&amp;gt;
 &amp;lt;input type=&amp;#34;text&amp;#34; id=&amp;#34;username&amp;#34; name=&amp;#34;username&amp;#34; /&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;label for=&amp;#34;password&amp;#34;&amp;gt;Password:&amp;lt;/label&amp;gt;
 &amp;lt;input type=&amp;#34;password&amp;#34; id=&amp;#34;password&amp;#34; name=&amp;#34;password&amp;#34; /&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Login&amp;lt;/button&amp;gt;
 &amp;lt;/form&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This form posts to the /login route, which looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#34;/login&amp;#34;, (req, res) =&amp;gt; {
 const { username, password } = req.body;
 if (username === &amp;#34;demo&amp;#34; &amp;amp;&amp;amp; password === &amp;#34;password&amp;#34;) {
 req.session.name = username;
 res.send(&amp;#34;Logged in&amp;#34;);
 } else {
 res.send(&amp;#34;Invalid username or password&amp;#34;);
 }
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It extracts the user name and password from the body of the request (ie from the form). If they are a match, then it sets &amp;ldquo;name&amp;rdquo; in the session which signifies to the rest of the app that we are validly logged in.&lt;/p&gt;
&lt;p&gt;To log out, we just tell express-session to destroy the session:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/logout&amp;#34;, (req, res) =&amp;gt; {
 req.session.destroy((err) =&amp;gt; {
 if (err) {
 res.send(&amp;#34;Error clearing session&amp;#34;);
 } else {
 res.send(&amp;#34;Session cleared&amp;#34;);
 }
 });
});
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="tidy-up"&gt;Tidy up&lt;/h3&gt;
&lt;p&gt;We just need a bit of refactoring before we move on. Currently our &lt;code&gt;/login&lt;/code&gt; route only allows a single user, and is not great to read, let&amp;rsquo;s change it to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#34;/login&amp;#34;, (req, res) =&amp;gt; {
 const { username, password } = req.body;
 if (isValidCredentials(username, password)) {
 req.session.name = username;
 res.send(&amp;#34;Logged in&amp;#34;);
 } else {
 res.send(&amp;#34;Invalid username or password&amp;#34;);
 }
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s better, and for the isValidCredentials() we&amp;rsquo;ll check against an array of objects like so:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const validCredentials = [
 { username: &amp;#34;demo&amp;#34;, password: &amp;#34;password&amp;#34; },
 { username: &amp;#34;Jane&amp;#34;, password: &amp;#34;password&amp;#34; },
 { username: &amp;#34;Fred&amp;#34;, password: &amp;#34;password&amp;#34; },
];

function isValidCredentials(username, password) {
 return validCredentials.some(
 (cred) =&amp;gt; cred.username === username &amp;amp;&amp;amp; cred.password === password
 );
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you haven&amp;rsquo;t met the JavaScript &lt;code&gt;.some()&lt;/code&gt; method, it&amp;rsquo;s used to run a callback function against the elements in an array until it returns true or comes to the end of an array.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve made a few changes, lets revisit the complete server.js code:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// npm install cookie-parser express express-session

const express = require(&amp;#34;express&amp;#34;);
const cookieParser = require(&amp;#34;cookie-parser&amp;#34;);
const session = require(&amp;#34;express-session&amp;#34;);
const bodyParser = require(&amp;#34;body-parser&amp;#34;);

const app = express();

// Set up view engine
app.set(&amp;#34;views&amp;#34;, &amp;#34;views&amp;#34;);
app.set(&amp;#34;view engine&amp;#34;, &amp;#34;ejs&amp;#34;);

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));

// session middleware
app.use(
 session({
 secret: &amp;#34;REtKU9xyvahuHGd3&amp;#34;, // Replace with a strong secret key
 resave: false,
 saveUninitialized: true,
 cookie: { secure: false }, // Set to true if using HTTPS
 })
);

app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 if (req.session.name) {
 res.send(`Hello ${req.session.name}!`);
 } else {
 res.render(&amp;#34;login.ejs&amp;#34;);
 }
});

// Route to clear the session
app.get(&amp;#34;/logout&amp;#34;, (req, res) =&amp;gt; {
 req.session.destroy((err) =&amp;gt; {
 if (err) {
 res.send(&amp;#34;Error clearing session&amp;#34;);
 } else {
 res.send(&amp;#34;Session cleared&amp;#34;);
 }
 });
});

const validCredentials = [
 { username: &amp;#34;demo&amp;#34;, password: &amp;#34;password&amp;#34; },
 { username: &amp;#34;Jane&amp;#34;, password: &amp;#34;password&amp;#34; },
 { username: &amp;#34;Fred&amp;#34;, password: &amp;#34;password&amp;#34; },
];

function isValidCredentials(username, password) {
 return validCredentials.some(
 (cred) =&amp;gt; cred.username === username &amp;amp;&amp;amp; cred.password === password
 );
}

app.post(&amp;#34;/login&amp;#34;, (req, res) =&amp;gt; {
 const { username, password } = req.body;
 if (isValidCredentials(username, password)) {
 req.session.name = username;
 res.send(&amp;#34;Logged in&amp;#34;);
 } else {
 res.send(&amp;#34;Invalid username or password&amp;#34;);
 }
});

// Start the server
const PORT = 3000;
app.listen(PORT, () =&amp;gt; {
 console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="plaintext-passwords"&gt;Plaintext passwords&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s a bad idea to ever store passwords in plaintext anywhere. A solution for this is to hash the password before storing it, then when we need to test a password a user has entered, we test the hash of the password the user has entered against the hashes we have stored. I&amp;rsquo;m being very casual in my language here - I should probably be saying &lt;em&gt;salting&lt;/em&gt; and &lt;em&gt;hashing&lt;/em&gt;. For the purposes of this discussion the idea is to turn each password into gobbledygook in such a way it&amp;rsquo;s not possible to turn it back into the password.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to use the &lt;a href="https://www.npmjs.com/package/bcrypt"&gt;bcrypt&lt;/a&gt; to do the heavy lifting for us since it&amp;rsquo;s going to be more cryptographically sound than anything we could write.&lt;/p&gt;
&lt;p&gt;The encryption process is resource intensive, so these are going to be async operations.It&amp;rsquo;s a small trade-off for the security we&amp;rsquo;re adding.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const bcrypt = require(&amp;#34;bcrypt&amp;#34;);

const validCredentials = [
 {
 username: &amp;#34;demo&amp;#34;,
 hashedPassword:
 &amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;,
 },
 {
 username: &amp;#34;Jane&amp;#34;,
 hashedPassword:
 &amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;,
 },
 {
 username: &amp;#34;Fred&amp;#34;,
 hashedPassword:
 &amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;,
 },
];

async function isValidCredentials(username, password) {
 const user = validCredentials.find((cred) =&amp;gt; cred.username === username);
 if (!user) return false;
 return await bcrypt.compare(password, user.hashedPassword);
}

app.post(&amp;#34;/login&amp;#34;, async (req, res) =&amp;gt; {
 const { username, password } = req.body;
 if (await isValidCredentials(username, password)) {
 req.session.name = username;
 res.send(&amp;#34;Logged in&amp;#34;);
 } else {
 res.send(&amp;#34;Invalid username or password&amp;#34;);
 }
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Okay, now we have a login system, with safeish password storage and session management so the user doesn&amp;rsquo;t have to log in on every page.&lt;/p&gt;
&lt;h3 id="persisting-sessions"&gt;Persisting sessions&lt;/h3&gt;
&lt;p&gt;One last thing before we wrap up this overly long post. Currently, if Jane is logged in, and the server is rebooted, when she returns, her session will have been eliminated. That&amp;rsquo;s to say, her browser will pass the session id in it&amp;rsquo;s cookie, but the server won&amp;rsquo;t recognise it and will force her to log in again. That&amp;rsquo;s not the end of the world (in fact a future improvement should probably be to expire sessions every now and then) but it would be nicer if the session information survived server reboots.&lt;/p&gt;
&lt;p&gt;By default, &lt;code&gt;express-session&lt;/code&gt; uses a memory store, but this can be swapped out for other types of stores. Frequently, production apps will use a database of some kind to keep the session data, but for a single instance app with a hundred or so users a simpler system is just to use the host file system. Such a thing is built into express-session in the form of &lt;code&gt;session-file-store&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Implementing this is simple, we just need to declare a variable for the class, then include it in our initialisation of the session middleware.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const FileStore = require(&amp;#34;session-file-store&amp;#34;)(session);

const app = express();

// Set up view engine
app.set(&amp;#34;views&amp;#34;, &amp;#34;views&amp;#34;);
app.set(&amp;#34;view engine&amp;#34;, &amp;#34;ejs&amp;#34;);

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));

// session middleware
app.use(
 session({
 secret: &amp;#34;REtKU9xyvahuHGd3&amp;#34;, // Replace with a strong secret key
 resave: false,
 saveUninitialized: true,
 cookie: { secure: false }, 
 store: new FileStore({logFn: function(){}})
 })
);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You don&amp;rsquo;t need the business with &lt;code&gt;logFn&lt;/code&gt;, that&amp;rsquo;s just a hack to subdue the logs. Without it, express-session logs an error each time a session id arrives in a cookie and there&amp;rsquo;s no corresponding file for it. That happens all the time when I&amp;rsquo;m developing so I foolishly turn it off.&lt;/p&gt;
&lt;p&gt;Now every time a session is created, it will be stored as a text file of JSON in the sessions directory. When a browser makes a request, the express-session will check for a file matching the session id from the cookie, and load the session data from it if needed.&lt;/p&gt;
&lt;p&gt;Since express-session is now dealing with our cookies, we can eliminate cookie-parser.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s where we&amp;rsquo;re up to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#34;express&amp;#34;);
const session = require(&amp;#34;express-session&amp;#34;);
const bodyParser = require(&amp;#34;body-parser&amp;#34;);
const bcrypt = require(&amp;#34;bcrypt&amp;#34;);
const FileStore = require(&amp;#34;session-file-store&amp;#34;)(session);

const app = express();

// Set up view engine
app.set(&amp;#34;views&amp;#34;, &amp;#34;views&amp;#34;);
app.set(&amp;#34;view engine&amp;#34;, &amp;#34;ejs&amp;#34;);

app.use(bodyParser.urlencoded({ extended: false }));

// session middleware
app.use(
 session({
 secret: &amp;#34;REtKU9xyvahuHGd3&amp;#34;, // Replace with a strong secret key
 resave: false,
 saveUninitialized: true,
 cookie: { secure: false }, 
 store: new FileStore({logFn: function(){}})
 })
);

app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
 if (req.session.name) {
 res.send(`Hello ${req.session.name}!`);
 } else {
 res.render(&amp;#34;login.ejs&amp;#34;);
 }
});

// Route to clear the session
app.get(&amp;#34;/logout&amp;#34;, (req, res) =&amp;gt; {
 req.session.destroy((err) =&amp;gt; {
 if (err) {
 res.send(&amp;#34;Error clearing session&amp;#34;);
 } else {
 res.send(&amp;#34;Session cleared&amp;#34;);
 }
 });
});

const validCredentials = [
 {
 username: &amp;#34;demo&amp;#34;,
 hashedPassword:
 &amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;,
 },
 {
 username: &amp;#34;Jane&amp;#34;,
 hashedPassword:
 &amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;,
 },
 {
 username: &amp;#34;Fred&amp;#34;,
 hashedPassword:
 &amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;,
 },
];

async function isValidCredentials(username, password) {
 const user = validCredentials.find((cred) =&amp;gt; cred.username === username);
 if (!user) return false;
 return await bcrypt.compare(password, user.hashedPassword);
}

app.post(&amp;#34;/login&amp;#34;, async (req, res) =&amp;gt; {
 const { username, password } = req.body;
 if (await isValidCredentials(username, password)) {
 req.session.name = username;
 res.send(&amp;#34;Logged in&amp;#34;);
 } else {
 res.send(&amp;#34;Invalid username or password&amp;#34;);
 }
});

// Start the server
const PORT = 3000;
app.listen(PORT, () =&amp;gt; {
 console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="where-next"&gt;Where next?&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s been a bit of a trek to get to this point, so I&amp;rsquo;m winding this up here, and we&amp;rsquo;ll take it to the next level in a future post. Some of the next steps to explore are to move our secrets out of the source file, and to use &lt;a href="https://www.npmjs.com/package/passport"&gt;Passport.js&lt;/a&gt; like the two million other projects who downloaded it this week.&lt;/p&gt;</description></item><item><title>Moving from Docker volumes to bind mounts</title><link>https://blog.iankulin.com/moving-from-docker-volumes-to-bind-mounts/</link><pubDate>Mon, 05 Aug 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-from-docker-volumes-to-bind-mounts/</guid><description>&lt;p&gt;&lt;a href="https://placesjournal.org/article/all-is-lost-notes-on-broken-world-design/"&gt;&lt;img src="https://blog.iankulin.com/images/friedman-moe-lost-6.jpg" width="600" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I started with Docker, the docs seemed to suggest that using Docker volumes was a good thing. With a Docker volume, you just create the volume and Docker manages the rest. You don&amp;rsquo;t have to worry about where it is, or really ever think about it.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a docker-compose for &lt;a href="https://github.com/louislam/uptime-kuma/wiki"&gt;Uptime Kuma&lt;/a&gt; using a volume.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 uptime-kuma:
 image: louislam/uptime-kuma:1
 container_name: uptime-kuma
 volumes:
 - kuma_data:/app/data
 ports:
 - 80:3001
 restart: unless-stopped

volumes:
 kuma_data:
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is telling Docker we want to create a volume called &amp;ldquo;kuma_data&amp;rdquo; and then map it into the container file system at &lt;code&gt;/app/data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t love this for a couple of reasons. The first is that I don&amp;rsquo;t know how to back that volume up - although this is a commonly quoted reason for using them. And the second is that, in order to make moving containers around easier, I have settled on a sort of standard setup. All Docker containers are in a subdirectory in my home directory where the subdirectory is their name. In that subdirectory is a docker-compose file to manage them, and a further subdirectory named &amp;lsquo;data&amp;rsquo; that holds all their data.&lt;/p&gt;
&lt;p&gt;For example, here&amp;rsquo;s my Jellyfin directory. The Jellyfin docker container needs two &amp;lsquo;volumes&amp;rsquo; - config and cache:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-2.57.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-2.57.34-pm.png" width="907" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So if I ever needed to move this somewhere, I could &lt;code&gt;docker compose down&lt;/code&gt;, then &lt;code&gt;rsync&lt;/code&gt; the whole directory somewhere, then &lt;code&gt;docker compose up&lt;/code&gt; &lt;code&gt;-d&lt;/code&gt;, and I&amp;rsquo;d be in business.&lt;/p&gt;
&lt;p&gt;Instead of using Docker volumes, I&amp;rsquo;m using &amp;lsquo;bind mounts&amp;rsquo; to those sub-directories. The docker-compose is not more complicated, if anything, it&amp;rsquo;s simpler since we don&amp;rsquo;t need the &lt;code&gt;volumes&lt;/code&gt; part at the bottom.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 jellyfin:
 image: jellyfin/jellyfin
 container_name: jellyfin
 network_mode: host
 ports:
 - &amp;#34;8096:8096&amp;#34;
 volumes:
 - ./data/config:/config
 - ./data/cache:/cache
 - /mnt/media:/media
 restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="pros-and-cons"&gt;Pros and Cons&lt;/h3&gt;
&lt;p&gt;Many people and projects prefer Docker volumes. My system of just using bind mounts might increase the likelihood of me breaking something by monkeying around with the files - and volumes reduce that possibility since they&amp;rsquo;d be hidden away somewhere and I guess owned by root. There are also some performance considerations if running under Mac or Windows since Docker Desktop in those cases is really running your container on a VM, and if you let it manage the volumes it can do so in the native file system.&lt;/p&gt;
&lt;p&gt;Another downside is that bind mount locations are not automatically initialised in the same way as volumes.&lt;/p&gt;
&lt;p&gt;Nevertheless, for my use, I like to use bind mounts and keep everything together.&lt;/p&gt;
&lt;h3 id="moving-advice"&gt;Moving advice&lt;/h3&gt;
&lt;p&gt;Since I started with volumes, I now needed to be able to change over to using bind mounts, and the first advice I googled up (such as &lt;a href="https://forums.docker.com/t/move-docker-volume-to-bind-mount/140843"&gt;here&lt;/a&gt; and &lt;a href="https://www.synoforum.com/threads/move-from-docker-volume-to-bind-mount.12990/"&gt;here&lt;/a&gt;) made it sound like it was going to be complicated. The advice was that the hidden file location of the volumes in the host system had some sort of magic about it and we needed to use a &lt;code&gt;docker cp&lt;/code&gt; command to access it or create a temporary container with the named volume and the bind mount you want to move it to, then copy the data across from inside that container.&lt;/p&gt;
&lt;h3 id="ignoring-advice"&gt;Ignoring advice&lt;/h3&gt;
&lt;p&gt;As long as the container is stopped when you access the files, I can&amp;rsquo;t see why we can&amp;rsquo;t just copy them as normal. Of course, I could be totally incorrect about this, but I&amp;rsquo;ve done it a number of times now, and not had any disasters. Of course, I always snapshot the VM before I start in case it doesn&amp;rsquo;t work, and you should as well. If you like to yolo your production data, here&amp;rsquo;s how.&lt;/p&gt;
&lt;h3 id="where-is-it"&gt;Where is it?&lt;/h3&gt;
&lt;p&gt;To find the actual location of the data in the host system, use the docker inspect command on the running container. You&amp;rsquo;ll get a bunch of JSON. About a third up from the bottom of that there&amp;rsquo;ll be a section called &amp;ldquo;Mounts&amp;rdquo; that will have the real location for each volume. (if you&amp;rsquo;re looking at a &amp;ldquo;Mounts&amp;rdquo; section that doesn&amp;rsquo;t have the &amp;ldquo;Source&amp;rdquo; line, scroll down a bit).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-3.27.33-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-3.27.33-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="copying"&gt;Copying&lt;/h3&gt;
&lt;p&gt;Next thing, I&amp;rsquo;ll make a data sub-directory for it. &lt;code&gt;mkdir ~/uptimekuma/data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The (with the container stopped), copy the data over.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo cp -a /var/lib/docker/volumes/uptimekuma_kuma_data/_data/. ~/uptimekuma/data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After checking that&amp;rsquo;s actually worked, I&amp;rsquo;ll kill the volume. We need to get the volume name first if you haven&amp;rsquo;t figured it out from the location path.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@ct390-test:~/uptimekuma$ sudo docker volume ls
DRIVER VOLUME NAME
local uptimekuma_kuma_data
ian@ct390-test:~/uptimekuma$ sudo docker volume rm uptimekuma_kuma_data 
uptimekuma_kuma_data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we&amp;rsquo;ll need to update the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file from the top of this post so it bind mounts the sub-directory that we&amp;rsquo;ve copied to instead of the named volume.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 uptime-kuma:
 image: louislam/uptime-kuma:1
 container_name: uptime-kuma
 volumes:
 - ./data:/app/data
 ports:
 - 80:3001
 restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once that&amp;rsquo;s done, &lt;code&gt;docker compose up -d&lt;/code&gt; should get things running. And now we have a nice situation with the docker compose and all the data for the container together in one spot.&lt;/p&gt;</description></item><item><title>LLM coding question comparison using Ollama</title><link>https://blog.iankulin.com/llm-coding-question-comparison-using-ollama/</link><pubDate>Mon, 29 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/llm-coding-question-comparison-using-ollama/</guid><description>&lt;p&gt;Now Ollama has made it simple enough for anyone who can use a terminal to run large language models locally, naturally I&amp;rsquo;ve gone overboard downloading too many to play with. I&amp;rsquo;m increasingly feeling they definitely have a place in the devops/coding arsenal of tools, but which model is best?&lt;/p&gt;
&lt;p&gt;If you go on HuggingFace to look at a new model you&amp;rsquo;re interested, they often have great comparisons like this.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"&gt;&lt;img src="https://blog.iankulin.com/images/performance.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There has been a lot of work in crafting these and other benchmarks which are often comprehensive and well thought out. I&amp;rsquo;ve also seen people doing fun things, like &lt;a href="https://youtu.be/B0uMFWAGUzI?t=145"&gt;this guy&lt;/a&gt;, who is just pasting coding challenges off a web page into an LLM and seeing if it can solve them (spoiler - mostly it can solve &lt;a href="https://www.w3resource.com/python-exercises/basic/python-basic-1-exercise-141.php"&gt;coding problems that are probably part of it&amp;rsquo;s training set&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;A factor to keep in mind when looking at these charts is that they are probably running unquantised (uncompressed is a close enough analogy) models on fleets of &lt;a href="https://www.nvidia.com/en-au/data-center/h100/"&gt;$60K graphics cards&lt;/a&gt;. I can use that if I pay them $20 a month and have an internet connection, but I want to pay $0 and run it on my M1 MacBook - that&amp;rsquo;s why I downloaded &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So what follows is my completely unscientific testing of the models I&amp;rsquo;ve downloaded. Basically, I&amp;rsquo;ll ask them the same question (that I think I know the answer to) and time their response, and subjectively judge their output. For the question I&amp;rsquo;ve chosen:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Thinking about Docker, what&amp;rsquo;s the difference between [CMD] and [EntryPoint]?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This seems like a fairly specific bit of knowledge someone might want to know about, I know the answer, and the first page of google results are mostly good so there should be sufficient training data. I&amp;rsquo;ve put both terms in square brackets as a red herring, and same with the camelcase for ENTRYPOINT. I also didn&amp;rsquo;t specify that these are both usually defined in the dockerfile. I&amp;rsquo;ve had a go at the same question, and &lt;a href="https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/"&gt;published it here last week&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="the-results"&gt;The results&lt;/h3&gt;
&lt;p&gt;According to me, I am the winner.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/2309065-t2sarahconnor2.jpg" width="640" alt=""&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-2.37.43-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The word count for my answer would be a bit higher if we counted the text in my images, which we probably should. I made up my times by guessing what they&amp;rsquo;d be if you asked me this question.&lt;/p&gt;
&lt;h3 id="the-contestants"&gt;The contestants&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;codeqwen&lt;/em&gt; and &lt;a href="https://chat.deepseek.com/coder"&gt;&lt;em&gt;deepseek-coder&lt;/em&gt;&lt;/a&gt; are both optimised for chatting about code, which I&amp;rsquo;m claiming Docker skills are a legitimate part of. They both also do autocomplete, and I&amp;rsquo;m using &lt;em&gt;codeqwen&lt;/em&gt; for that in VSCode. &lt;em&gt;deepseek-coder&lt;/em&gt; is about twice as big, and you&amp;rsquo;d think better, which it was, but in my opinion, only a little. codeqwen had a clear error and &lt;em&gt;deepseek-coder&lt;/em&gt; was a bit muddled in some parts but did a great job of wrapping it up with an explanation of where you&amp;rsquo;d use both.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;phi3&lt;/em&gt;&amp;rsquo;s is small (half the size of most of the others here) and great for chatting. For general questions it&amp;rsquo;s very impressive for it&amp;rsquo;s size, but was useless for this task. It&amp;rsquo;s interesting to me that the smartest and the stupidest AI&amp;rsquo;s had the most to say, and that my explanation was almost the exact size of all the others.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/d11c1d71-92aa-43dd-9b44-39e7ac1b2727_1600x900.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;dolphin-mistral&lt;/em&gt;&amp;rsquo;s claim to fame is that it&amp;rsquo;s uncensored. So if you ask it how to build an improvised explosive, overturn an election, or trick a co-worker into falling in love with you, it will happily tell you - something the other models here cannot. Basically, it laughs at the first law of robotics. Even though launching a Docker container is not illegal or unethical, it had a reasonable, usable answer for our question.&lt;/p&gt;
&lt;p&gt;I tried two versions of &lt;em&gt;llama3&lt;/em&gt;. To explain the difference, we need to go into an explanation of how large language models work, which I don&amp;rsquo;t know, so I&amp;rsquo;m just going to hallucinate it for you:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/input.jpg" width="800" alt=""&gt;
&lt;p&gt;If you vacuum up heaps of input (the training data), then filter out all the cruft (&amp;rsquo;the&amp;rsquo;, &amp;lsquo;a&amp;rsquo;, &amp;rsquo;not)&amp;rsquo;, then put it into a special multidimensional database so that similar things are near each other (eg &amp;lsquo;rose&amp;rsquo; is near &amp;lsquo;flower&amp;rsquo;, &amp;lsquo;red&amp;rsquo; and &amp;rsquo;titanic&amp;rsquo; and a long way from &amp;lsquo;bulldozer&amp;rsquo; and &amp;lsquo;antidisestablishmentarianism&amp;rsquo;) and the database also includes how far away from each other those things are, then it is very, very, big. Too big to put on my MacBook.&lt;/p&gt;
&lt;p&gt;We can reduce the size of it by &amp;lsquo;quantising&amp;rsquo; it which is a word I&amp;rsquo;ve heard on a podcast and might mean reducing the resolution of the numbers representing the distances between concepts in the database. This is the &amp;lsquo;q8&amp;rsquo; and &amp;lsquo;q4&amp;rsquo; you can see in the tags in the table. &amp;lsquo;q4&amp;rsquo; is going to be smaller, but less accurate than a &amp;lsquo;q8&amp;rsquo; of the same data.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the situation with these two versions of &lt;em&gt;llama3&lt;/em&gt; - one is more &amp;lsquo;compressed&amp;rsquo;. The relationship between the quantitation and the usefulness of the model is not linear for many applications, and that seems to be the case here. The bigger model did produce some more detail, but I actually preferred the output of the smaller one.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;It would be foolish to put much weight on a conclusion from a single run of a dubious test analyzed by a subjective carbon based lifeform, but anyway&amp;hellip;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All of these models produced a useful starting point except &lt;em&gt;phi3&lt;/em&gt;. You probably could have just used what the others produced and gone on working with your dockerfile and things would have worked out fine.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;llama3&lt;/em&gt;&amp;rsquo;s performance matches my experience of other times I&amp;rsquo;ve been using it. It&amp;rsquo;s just pretty great for what it is.&lt;/li&gt;
&lt;li&gt;Most of the explanations over-complicated things - this probably could have been fixed with a better prompt.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s sort of magic when you think this is most of the world&amp;rsquo;s knowledge squashed into 4GB on my laptop in a form I can just ask questions of&lt;/li&gt;
&lt;li&gt;There is room for improvement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s easy to imagine that if these models were able to reach out to the internet and check what they&amp;rsquo;d come up with then generate a response by combining their first guess and their new knowledge, they&amp;rsquo;d be a lot better. That&amp;rsquo;s basically what &lt;a href="https://www.perplexity.ai/search/thinking-about-docker-what-s-t-9wJXPl_iTv2BLE60.QXgGA"&gt;Perplexity&lt;/a&gt; does, and it&amp;rsquo;s output is better than any of my local models, and it includes some of the links it used which would probably clear up any further questions. That sort of functionality is not far away for local models, and something like it is running in &lt;a href="https://useanything.com/"&gt;AnythingLLM&lt;/a&gt;, so I expect these will be indispensable tools in a year.&lt;/p&gt;</description></item><item><title>dockerfile - CMD vs ENTRYPOINT</title><link>https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/</link><pubDate>Mon, 22 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/</guid><description>&lt;p&gt;There are two entries we often have at the end of a &lt;code&gt;dockerfile&lt;/code&gt; (which is the file that tells Docker how an image is to be built).&lt;/p&gt;
&lt;p&gt;They are similar in that when the container is launched from an image, these commands will be executed. For example, both of the dockerfiles below will print &amp;ldquo;Hello World&amp;rdquo; when run.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doc-&lt;/code&gt;entry:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM debian:stable-slim
ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;doc-cmd&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM debian:stable-slim
CMD [&amp;#34;echo&amp;#34;, &amp;#34;Hello World&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.45.26-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The key difference between them is that CMD can be overridden:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.47.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see from this that the ENTRYPOINT command is just added on to by the extra command line argument, but the CMD one is replaced entirely.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to have an ENTRYPOINT and a CMD in your dockerfile:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doc-both&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM debian:stable-slim
ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
CMD [&amp;#34;&amp;amp; Hello World from CMD&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.55.45-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Naturally, only the CMD is overridden if we pass in extra values.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.58.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h4 id="other-things-of-note"&gt;Other things of note&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Although these demos are at the command line, we&amp;rsquo;d see the same behaviour if we&amp;rsquo;d added a CMD to a docker compose file and started the container that way.&lt;/li&gt;
&lt;li&gt;You can have multiple ENTRYPOINTs or CMDs in a file, they are all ignored except the last one.&lt;/li&gt;
&lt;li&gt;The best place to learn more about &lt;a href="https://docs.docker.com/reference/dockerfile/#entrypoint"&gt;ENTRYPOINT&lt;/a&gt; and &lt;a href="https://docs.docker.com/reference/dockerfile/#cmd"&gt;CMD&lt;/a&gt; is in the official &lt;a href="https://docs.docker.com/reference/dockerfile/"&gt;Docker docs for dockerfile&lt;/a&gt;, not from an AI.&lt;/li&gt;
&lt;li&gt;Most times, what you&amp;rsquo;re looking at the end of your dockerfile is ENTRYPOINT. Just use CMD to add a default behaviour that you&amp;rsquo;re happy for your image users to overide.&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t confuse either of these with RUN - that happens during the image build, ENTRYPOINT and CMD are used when the container is launched/run.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>User environment variables are not available in cron</title><link>https://blog.iankulin.com/user-environment-variables-are-not-available-in-cron/</link><pubDate>Mon, 15 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/user-environment-variables-are-not-available-in-cron/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-4.13.13-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m used to using the &lt;code&gt;docker-compose.yaml&lt;/code&gt; or &lt;code&gt;dockerfile&lt;/code&gt; to set environment variables for containers running my apps, but ran into an issue recently where the variable seemed to be set some of the time, but at others it didn&amp;rsquo;t appear to exist.&lt;/p&gt;
&lt;p&gt;I had a script set to run by &lt;code&gt;cron&lt;/code&gt; inside the container, and it turns out that the environment variables set for the container are available in the user space, but not in &lt;code&gt;cron&lt;/code&gt;, even if running with that user&amp;rsquo;s permissions. This is probably old news to established Linux users but it threw me for a while. I&amp;rsquo;d &lt;code&gt;exec&lt;/code&gt; into the container and the script would work perfectly, then wait another minute for &lt;code&gt;cron&lt;/code&gt; to run it and it would fail 🤦‍♀️ It was exasperated by my discovery that I didn&amp;rsquo;t know how to console.log debug from inside a container cron job as well - the subject of an earlier post.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d narrowed it down to this issue and googleconfirmed it, I didn&amp;rsquo;t really come up with an elegant solution either. You may think &amp;ldquo;buy hey, everything in Linux is a file, it must be in proc somewhere&amp;rdquo;, and you&amp;rsquo;d be right, sort of.&lt;/p&gt;
&lt;p&gt;In Linux, to see your own environment variables, you type in &lt;code&gt;env&lt;/code&gt;. I think this probably works by grabbing them from &lt;code&gt;proc&lt;/code&gt; under &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/environ&lt;/code&gt; where &lt;PID&gt; is the process id of the shell. We can get our own process id with the &lt;code&gt;ps&lt;/code&gt; command. If we&amp;rsquo;re the root user (which I am in my container) we can see &lt;em&gt;someone else&amp;rsquo;s&lt;/em&gt; process ids with &lt;code&gt;ps -u &amp;lt;username&amp;gt;&lt;/code&gt;, get the process id for the shell and look in proc. So I tried that from the cron script - it turns out no.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-6.51.16-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I could see them, but it didn&amp;rsquo;t include the environment variable passed in from Docker that was available at the entry point. This is all a bit weird to me - I&amp;rsquo;m not sure why we&amp;rsquo;re the same user, with the same permissions but with a new seperate environment. I guess there is a reason, it&amp;rsquo;s just not apparent to me.&lt;/p&gt;
&lt;h3 id="the-hack"&gt;The Hack&lt;/h3&gt;
&lt;p&gt;To get around this, I now save the environment variable I&amp;rsquo;m interested in to a file in the script that runs at the entry point:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Save the environment variable IMAGE_URL into
# a file for later use in the cron script

env | grep IMAGE_URL &amp;gt; image_url.txt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in my script that is run by cron, I reconstitute it from the file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Read the file saved by the entry point script
# and extract the environment variable

while IFS=&amp;#39;=&amp;#39; read -r key value; do
 if [[ $key == &amp;#34;IMAGE_URL&amp;#34; ]]; then
 export &amp;#34;$key=$value&amp;#34;
 fi
done &amp;lt; &amp;#34;/image_url.txt&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That works fine. Software testers will be looking at this solution thinking &amp;ldquo;What about the case where the environment variable isn&amp;rsquo;t set, but the file from the last run is still there?&amp;rdquo; Worry not, bug finding person. It&amp;rsquo;s a container so everything&amp;rsquo;s ephemeral. The file with the environment variable can only be there on runs when that environment variable has been set.&lt;/p&gt;</description></item><item><title>Outputting to the console, in Docker, from a cron job</title><link>https://blog.iankulin.com/outputting-to-the-console-in-docker-from-a-cron-job/</link><pubDate>Mon, 08 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/outputting-to-the-console-in-docker-from-a-cron-job/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-3.48.02-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re googling this exact title, you&amp;rsquo;re probably bumping your head against the same things I was today. I was debugging a completely different project, and needed to print to the console, from a &lt;code&gt;cron&lt;/code&gt; job, in a Docker container. Turns out this isn&amp;rsquo;t as straightforward as I thought.&lt;/p&gt;
&lt;h3 id="foreground-cron"&gt;Foreground cron&lt;/h3&gt;
&lt;p&gt;Before you even get to the problem space, here&amp;rsquo;s a tip. If you want to have a cron job running in a container, start &lt;code&gt;cron&lt;/code&gt; in the foreground. If you do not, Docker realises nothing is going on, and exits. If you want to keep the container active so your &lt;code&gt;cron&lt;/code&gt; jobs get a chance to execute, then start it in the foreground.&lt;/p&gt;
&lt;h3 id="stdout"&gt;stdout&lt;/h3&gt;
&lt;p&gt;I always think about &lt;code&gt;stdout&lt;/code&gt; as being the console, but I guess at one time it was probably a teletype printer, and for &lt;code&gt;cron&lt;/code&gt;, &lt;a href="https://askubuntu.com/questions/1454389/where-does-the-users-cron-output-go-to-by-default-on-ubuntu"&gt;apparently it&amp;rsquo;s an email to the user&lt;/a&gt;. So me directing the output of the command I&amp;rsquo;m running in crontab to stdout is not helpful. It turns out you need something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;* * * * * /script.sh &amp;gt; /proc/1/fd/1 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://github.com/moby/moby/issues/19616#issuecomment-174492543"&gt;There&amp;rsquo;s some good Linuxy reason for this&lt;/a&gt; - &lt;code&gt;/proc/1/fd/1&lt;/code&gt; is the standard output of a particular process which happens to be the process for the entry point of the container or some such thing,&lt;/p&gt;
&lt;p&gt;If you are not familiar with &lt;code&gt;cron&lt;/code&gt;, there are going to be much better explanations than this, but it&amp;rsquo;s a mechanism for running jobs at various times. It uses a file, called the &lt;code&gt;crontab&lt;/code&gt; to define these. Each of the asterisks above are spots where we can define the minute, hour, day, etc that the job runs by entering a number. If they are all asterisks then we are saying &amp;lsquo;run this every minute&amp;rsquo;. Following that is just the command, which in this case is run &lt;code&gt;/script.sh&lt;/code&gt; and send the output and error output to this proc file which happens to be the console.&lt;/p&gt;
&lt;p&gt;Other gotchas when working with cron is which user it&amp;rsquo;s running as (so permission problems) and where it&amp;rsquo;s running from (don&amp;rsquo;t use relative file paths).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/cron-docker-output"&gt;Example project on GutHub&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Using LLMs for coding</title><link>https://blog.iankulin.com/using-llms-for-coding/</link><pubDate>Mon, 01 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-llms-for-coding/</guid><description>&lt;p&gt;&lt;a href="https://madmuseum.org/events/ghost-shell"&gt;&lt;img src="https://blog.iankulin.com/images/ghost-in-the-shell_07.jpg" alt="Ghost in the Shell
© Manga Entertainment 1996
"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This post looks at the context for some of my thinking about AI for supporting software development, and where I&amp;rsquo;ve landed on it for the time being.&lt;/p&gt;
&lt;h3 id="the-landscape"&gt;The landscape&lt;/h3&gt;
&lt;p&gt;I &lt;a href="https://blog.iankulin.com/chatgpts-code-writing/"&gt;briefly wrote about ChatGPT&amp;rsquo;s&lt;/a&gt; coding ability at the end of 2022. The wide availability of this tool marked the beginning of what I think can fairly be described as a revolution. The controversies that have crystalised since have not dampened my amazement of this step forward in what compute can do, especially around natural language processing.&lt;/p&gt;
&lt;p&gt;The next big news in this story was Microsoft&amp;rsquo;s launch of Github Copilot. In business terms this was a brilliant move - owning the most popular code editor, and leveraging the world&amp;rsquo;s biggest collection of public code to create a product that &lt;a href="https://visualstudiomagazine.com/Articles/2024/02/05/copilot-numbers.aspx"&gt;millions of people&lt;/a&gt; are prepared to pay $10 a month for can only be regarded as a success.&lt;/p&gt;
&lt;p&gt;At the same time as Microsoft established a new revenue stream, LLMs have been an exciting area of open source growth, especially the excellent Python libraries and the tools in the LangChain ecosystem.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not all rainbows and unicorns though - there&amp;rsquo;s a few valid points that AI skeptics have coalesced around.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Training data - although this is a bigger issue for general models (where masses of web content has been vacuumed up) than it is for code, it is still an issue. If a model is trained on some non-permissively licensed code, and the generative AI I&amp;rsquo;m using includes that code in a commit, then a license, or at least some ethics have been breached.&lt;/li&gt;
&lt;li&gt;Quality (1) - You can see from the feature images in many of the posts in this blog during my MidJourney enthusiasm that generative AI is not perfect. Before I abandoned them I started to prefer the mangled writing and fingers of the engines, but no one wants the software equivalent of mangled fingers in their codebases. I suspect this particular aspect of the quality of the code will probably have a technological solution - we&amp;rsquo;re in the very early days after all.&lt;/li&gt;
&lt;li&gt;Quality (2) - A trickier quality problem is people writing code using AI where they do not fully understand the code they are committing. I imagine this is going to be a growing issue for projects, especially anything with a profit motive such as bug bonuses. Projects have mechanisms like code reviews and pull requests, but if submissions can be low-effort and checking them is high-effort, that asymmetry is going to be painful.&lt;/li&gt;
&lt;li&gt;Poisoned well - As the amount of AI code in codebases increases, then AI is trained on those codebases this will quickly become a snake eating it&amp;rsquo;s tail as AI is training itself on it&amp;rsquo;s own code. If allowed, this would tend to slowly evolve future codebases to use techniques favoured by early coding LLMs. The current amount of machine influenced code on &lt;a href="https://decrypt.co/147191/no-human-programmers-five-years-ai-stability-ceo"&gt;GitHub is definitely not 41%&lt;/a&gt; but it must be some, and is likely to increase, so this is a factor that will need some thought.&lt;/li&gt;
&lt;li&gt;Exfiltrating code - if you use an external LLM, such as GitHub Copilot to write commercial code, who can see your code? Since it&amp;rsquo;s being transmitted to the AI in order to make autocomplete suggestions, the answer is Microsoft, or some other company. How does that intersect with your company&amp;rsquo;s policies? I assume, based on the questions I&amp;rsquo;ve asked Copilot over the last year, that I&amp;rsquo;d never be considered for a coding job at Microsoft :-)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="i-for-one-welcome-our-new-robot-overlords"&gt;I, for one, welcome our new robot overlords&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/hailants-1.jpg" width="512" alt=""&gt;
&lt;p&gt;In an industry particularly known for excessive hype-cycles, it&amp;rsquo;s important to critically examine what we&amp;rsquo;re doing, but for the moment, I&amp;rsquo;ve landed on the position that these are good tools for me to use. Here&amp;rsquo;s my thinking.&lt;/p&gt;
&lt;p&gt;My situation is that I&amp;rsquo;m a very experienced developer, with solid expertise in several languages and programing paradigms, and with a degree that was strong in looking at the meta level of languages and software development processes, but, I&amp;rsquo;ve got no professional experience in modern languages. Because of this, a lot of my process has been knowing what I wanted to do, using google or stack overflow to figure out the mechanics of that in whatever language I&amp;rsquo;m using, then translating that into the context of the code I&amp;rsquo;m working on. Generative AI fits extremely well into that need - instead of jumping into a browser window to look something up, I&amp;rsquo;m just writing a descriptive comment of my intentions, then tabbing through the suggestions to chose an approach.&lt;/p&gt;
&lt;p&gt;My particular style is also well suited to these tools - I like clear, simple to reason about code. If I can write a pure function for something, I do. I like to break my code up into separated concerns with clear interfaces, I don&amp;rsquo;t prematurely optimise. I use descriptive variable, function and object names. I like to work with established, well documented languages and popular libraries, and I prefer to reduce external dependencies. All of these habits make it easier for an AI assistant to access the context of what I&amp;rsquo;m doing, and therefore to make better quality suggestions.&lt;/p&gt;
&lt;h3 id="my-journey"&gt;My journey&lt;/h3&gt;
&lt;p&gt;I started out using ChatGPT 3 then 3.5 as a sort of super-google/stack-overflow eliminator.&lt;/p&gt;
&lt;p&gt;Then with the public launch of &lt;a href="https://github.com/features/copilot"&gt;GitHub Copilot&lt;/a&gt;, I trialed that in VSCode and it was a great experience. I guess they didn&amp;rsquo;t invent the idea for the greyed out auto-complete suggestion you can tab to accept, but it feels like a natural way to work with this stuff.&lt;/p&gt;
&lt;p&gt;I paid for Copilot for a couple of months. But then heard about &lt;a href="https://codeium.com/"&gt;Codium&lt;/a&gt;, probably on &lt;a href="https://syntax.fm/show/728/ai-superpowers-with-kevin-hou-and-codeium"&gt;Syntax&lt;/a&gt;, which is free for individual developers (for now - thank you VC funding). I haven&amp;rsquo;t done any careful comparisons, but its definitely of the same order. I suspect Copilot is doing something better with the local context. For example I use a plain text accounting system called &lt;a href="https://beancount.github.io/docs/beancount_language_syntax.html#transactions"&gt;Bean Count&lt;/a&gt; in VSCode. Copilot is able to understand these transactions and make much useful suggestions than Codium. I assume this is just inferred from my local files since there would not be much training data for them, and it suggests the correct accounts based on the payees which must be from local context.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve probably done more work with Codium, 80% of it on Javascript, than with Copilot. It&amp;rsquo;s definitely a workable solution and a great choice if you want a Copilot type experience without paying for it, or have questions about Microsoft&amp;rsquo;s training data.&lt;/p&gt;
&lt;p&gt;More recently I&amp;rsquo;ve started playing with local models to avoid the problem of exfiltrating my code - I strongly feel I can&amp;rsquo;t use AI assisted coding with client code if I don&amp;rsquo;t know what&amp;rsquo;s happening it. If I can run a local model, that problem is avoided.&lt;/p&gt;
&lt;p&gt;I code on an early M1 MacBook, so &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; is an easy to use choice. I&amp;rsquo;ve tried &lt;a href="https://ai.meta.com/blog/meta-llama-3/"&gt;llama3&lt;/a&gt; and &lt;a href="https://qwenlm.github.io/blog/codeqwen1.5/"&gt;codeqwen1.5&lt;/a&gt; in the terminal for a bit, but missed the ChatGPT web experience. To get that back, I&amp;rsquo;ve been running &lt;a href="https://openwebui.com/"&gt;Open WebUI&lt;/a&gt; in a docker container.&lt;/p&gt;
&lt;p&gt;More recently, I&amp;rsquo;ve installed the &lt;a href="https://docs.continue.dev/intro"&gt;Continue&lt;/a&gt; VSCode extension that allows those Ollama managed models to work in VSCode, including the auto-suggestions (following &lt;a href="https://www.davegray.codes/posts/bye-copilot-how-to-create-a-local-ai-coding-assistant-for-free"&gt;Dave Gray&amp;rsquo;s blog post&lt;/a&gt;). I&amp;rsquo;ve got a few long flights coming up over the next week, so it will be good to be able to work offline with that help.&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t really done more than play with CodeQwen in VSCode via Continue so far, but my initial impression is that it&amp;rsquo;s comparable to Copilot, although the extra second of waiting for auto-suggestions did make me look up M3max MacBook pricing. Logic tells you that a 4GB model on a MacBook is going to be less capable than the giant GPT4 powered Copilot, but &lt;a href="https://qwenlm.github.io/blog/codeqwen1.5/"&gt;this comparison&lt;/a&gt; suggests the difference is not an order of magnitude (although the model size is). From limited playing around in small JavaScript codebases, they seem similar, with the local model just being a bit slower.&lt;/p&gt;
&lt;p&gt;If this is a revolution, it&amp;rsquo;s one we&amp;rsquo;re at the start of, and I certainly reserve the right to change my mind about AI assistance in coding, but I suspect it&amp;rsquo;s our future and I&amp;rsquo;m excited at the productivity boost it currently gives me working in languages I&amp;rsquo;m new to.&lt;/p&gt;</description></item><item><title>SSH login notification</title><link>https://blog.iankulin.com/ssh-login-notification/</link><pubDate>Mon, 13 May 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-login-notification/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/brown-bell-on-white-concrete-wall-4VRzuA4UxSY?utm_content=creditShareLink&amp;utm_medium=referral&amp;utm_source=unsplash"&gt;&lt;img src="https://blog.iankulin.com/images/nick-fewings-4vrzua4uxsy-unsplash.jpg" width="400" alt="Photo by Nick Fewings Unsplash
"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My VPS&amp;rsquo;s are usually locked down so just ports 80 &amp;amp; 443 (for web server) and 22 (for ssh) are open. That&amp;rsquo;s great for reducing the attack surface, but having ssh open is a potentially disastrous vulnerability. For this reason I often close that at the cloud firewall level as well, but it has to be open when I&amp;rsquo;m making changes or running the weekly ansible update/cleanup playbooks.&lt;/p&gt;
&lt;p&gt;To make things a bit safer, I run &lt;a href="https://blog.iankulin.com/beginning-node-app-security/"&gt;Fail2Ban&lt;/a&gt; on the ssh logs, and also have notifications turned on via &lt;a href="https://ntfy.sh/"&gt;Ntfy&lt;/a&gt;. Ntfy is so useful I make an annual donation to support it&amp;rsquo;s development and help with Phil&amp;rsquo;s server costs. I recommend you do to. In fact, my setup for getting a notification on my watch everytime someone ssh&amp;rsquo;s into one of my VPS&amp;rsquo;s is just copied directly from &lt;a href="https://docs.ntfy.sh/examples/#__tabbed_1_1"&gt;Phil&amp;rsquo;s examples&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="changes"&gt;Changes&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re not logged in as root (that should be turned off) you&amp;rsquo;ll need to run all these as sudo.&lt;/p&gt;
&lt;p&gt;Edit &lt;code&gt;/etc/pam.d/sshd&lt;/code&gt; to add these lines to the bottom:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# at the end of the file
session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then create the file &lt;code&gt;/usr/bin/ntfy-ssh-login.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash
if [ &amp;#34;${PAM_TYPE}&amp;#34; = &amp;#34;open_session&amp;#34; ]; then
 curl \
 -H prio:high \
 -H tags:warning \
 -d &amp;#34;SSH login: ${PAM_USER} from ${PAM_RHOST}&amp;#34; \
 ntfy.sh/your-unique-notification-string
fi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;but replace &lt;code&gt;your-unique-notification-string&lt;/code&gt; with whatever string you&amp;rsquo;re monitoring with the app. Note that if you&amp;rsquo;re using the shared (rather than self hosted) service, these are public. If I&amp;rsquo;d used mine here, you&amp;rsquo;d be able to use it to spam my phone with alerts. For this reason, many people use GUIDs.&lt;/p&gt;
&lt;p&gt;We need to make this executable:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;chmod +x /usr/bin/ntfy-ssh-login.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And restart the ssh daemon&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then log out, and ssh back in, and you should get the notification.&lt;/p&gt;</description></item><item><title>Upgrading to Forgejo 7.0.1</title><link>https://blog.iankulin.com/upgrading-to-forgejo-7-0-1/</link><pubDate>Mon, 06 May 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/upgrading-to-forgejo-7-0-1/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-28-at-1.08.21-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-28-at-1.08.21-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not that long ago that &lt;a href="https://blog.iankulin.com/my-web-app-update-process/"&gt;I wrote about&lt;/a&gt; doing routine upgrades on containerised web apps using Forgejo as an example as I upgraded Forgejo (my git repository manager) between patch versions of 1.21, then a few days later, they dropped 7.0.0&lt;/p&gt;
&lt;p&gt;&lt;a href="https://forgejo.org/2024-04-release-v7-0/"&gt;They say&lt;/a&gt; the major version jump is due to it being an LTS (long term support) release, and changing to &lt;a href="https://semver.org/spec/v2.0.0.html"&gt;semantic versioning 2.0.0&lt;/a&gt; , but that doesn&amp;rsquo;t quite explain it to me, and I assume this is partly signifying the fork&amp;rsquo;s drift away from the gitea codebase. In any case, the upgrade to 7.0.0 it does involve some breaking changes, and signifies to me that a lot has been on, which makes me keen to wait for a patch release (I&amp;rsquo;m always keen for other people to debug these things) which has now landed.&lt;/p&gt;
&lt;p&gt;The reason I think the upgrade process is worth mentioning, is that the steps we went through to move from 1.21.0 to 1.21.8:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker pull codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker compose up&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;will not work this time, and gives me the excuse to talk about container tags.&lt;/p&gt;
&lt;h3 id="container-tags"&gt;Container Tags&lt;/h3&gt;
&lt;p&gt;When the developers had built their release for 1.21.8, they would have pushed the exact same image to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21.8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:latest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;that way, people like me who had specified &lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt; in their docker-compose.yml files just had to down/pull/up to be in business.&lt;/p&gt;
&lt;p&gt;If they had released another patch version, say 1.21.10, they they would have pushed the new image to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21.10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:latest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;i.e. the old 1.21.8 image would have stayed the same, so anyone who had depended on that not changing will still be fine, but people like me who want all the patch versions updated (but not a minor version change) would get the new one.&lt;/p&gt;
&lt;p&gt;Normally you can just click on &amp;rsquo;tags&amp;rsquo; for an image on Docker Hub, but since this one is hosted on Codeburg&amp;rsquo;s Forgejo instance, you need to go &lt;a href="https://codeberg.org/forgejo/-/packages/container/forgejo/versions"&gt;https://codeberg.org/forgejo/-/packages/container/forgejo/versions&lt;/a&gt; to see all the tags they&amp;rsquo;ve pushed to.&lt;/p&gt;
&lt;h3 id="upgrade-steps"&gt;Upgrade steps&lt;/h3&gt;
&lt;p&gt;The extra step we&amp;rsquo;ll need to go through this time is to decide what level of version we want to specify in our docker-compose. I&amp;rsquo;ll stick to specifying to the minor version so my new &lt;code&gt;docker-compose.yml&lt;/code&gt; will be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;

networks:
 forgejo:
 external: false

services:
 server:
 image: codeberg.org/forgejo/forgejo:7.0
 container_name: forgejo
 environment:
 - USER_UID=112
 - USER_GID=103
 restart: always
 networks:
 - forgejo
 volumes:
 - ./forgejo:/data
 - /etc/timezone:/etc/timezone:ro
 - /etc/localtime:/etc/localtime:ro
 ports:
 - &amp;#39;80:3000&amp;#39;
 - &amp;#39;2200:22&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once that decision is made, it&amp;rsquo;s just the same:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;backup the LXC&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker pull codeberg.org/forgejo/forgejo:7.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker compose up&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Then some testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We could probably skip that pull step - when you compose up the system would notice the version change and pull it for us.&lt;/p&gt;</description></item><item><title>Peek inside a Docker image</title><link>https://blog.iankulin.com/peek-inside-a-docker-image/</link><pubDate>Mon, 29 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/peek-inside-a-docker-image/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.20.28-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.20.28-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A &amp;lsquo;dockerfile&amp;rsquo; contains all the instructions to build a Docker image. Here&amp;rsquo;s my first draft for a project I&amp;rsquo;m working on:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM node:20
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD [&amp;#34;node&amp;#34;, &amp;#34;server.js&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;COPY . .&lt;/code&gt; is copying all of the files in my project into the working directory of the image so they can be run. Of course we don&amp;rsquo;t need them all for the app - for example the &lt;code&gt;node_modules&lt;/code&gt; directory will be created when we &lt;code&gt;npm install&lt;/code&gt; so no need to copy that, and I don&amp;rsquo;t need all my dot files in the container.&lt;/p&gt;
&lt;p&gt;Docker has an easy fix for this, we can just add these files to a &lt;code&gt;.dockerignore&lt;/code&gt; file in the project, again, here a first draft.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;data
db
node_modules
.vscode
.dockerignore
.gitignore
.env
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I build an image, it doesn&amp;rsquo;t list the files it&amp;rsquo;s copying in, so I often like to sneak inside the image to have a look. This is easy, the trick is just to launch bash inside there. When I built this particular image, I tagged it &lt;code&gt;iankulin/tick&lt;/code&gt;, so the command to run bash inside it is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run -it iankulin/tick /bin/bash
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those flags, &lt;code&gt;-it&lt;/code&gt; are saying we want an interactive terminal. To get back out of it, just use &lt;code&gt;ctrl-D&lt;/code&gt; the sames as if you where logging out of an ssh session.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.27.22-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.27.22-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well well, there are a few files there I can add to the &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a couple of reasons to only keep necessary files in our containers. The first is that it just seems like good programming craft to keep things neat and clean, and a second is that it could become a security issue if we leak things into our containers. An obvious one would be a .&lt;code&gt;env&lt;/code&gt; that contained API keys or similarly sensitive stuff, but also, I have no idea what&amp;rsquo;s in a &lt;code&gt;.DS_Store&lt;/code&gt;. Mostly likely nothing important, but it&amp;rsquo;s not needed by my app so lets eliminate it by adding it to &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You might think I could have avoided all this by explicitly copying the files I know I need in the &lt;code&gt;dockerfile&lt;/code&gt; instead of using the broadbrush &lt;code&gt;COPY . .&lt;/code&gt; and that&amp;rsquo;s true. But I&amp;rsquo;ve found that if I do that, I end up wasting time debugging things that turn out to be a missing file, whereas if I copy everything, I just need to inspect the container at the start of the project and again as part of the shipping checks and we&amp;rsquo;re golden.&lt;/p&gt;
&lt;p&gt;Actually, I generally don&amp;rsquo;t want any dot files in my containers, so we&amp;rsquo;ll add that as a wildcard in the .dockerignore&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;data
db
node_modules
.*
dockerfile
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Much neater:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.42.56-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.42.56-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Virtual Hosts on "Static Web Server"</title><link>https://blog.iankulin.com/virtual-hosts-on-static-web-server/</link><pubDate>Mon, 22 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/virtual-hosts-on-static-web-server/</guid><description>&lt;p&gt;I&amp;rsquo;ve been running &lt;a href="https://blog.iankulin.com/nginx-proxy-manager/"&gt;NGINX Proxy Manager&lt;/a&gt; (NPM) in my homelab for a bit, and I&amp;rsquo;ve been meaning to clean up the VPS that runs most of my websites and public facing servers, so I&amp;rsquo;m considering running NGINX Proxy Manager on that VPS. While NGINX Proxy Manager wraps up the configs in a beautiful GUI, in the process you lose some of NGINXs capabilities. In particular there&amp;rsquo;s no GUI way to serve static virtual hosts from NGINX Proxy Manager.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a pity, since it seems like it wouldn&amp;rsquo;t be a lot of work, but in any case we can easily just run another web server and proxy to it. I guess I could run another instance of NGINX, but on $5 VPSs memory is a bit scarce, and since my sites are extremely low traffic, perhaps something a bit lighter is in order.&lt;/p&gt;
&lt;p&gt;An option mentioned in several posts for this exact situation is the very well named &lt;a href="https://static-web-server.net/"&gt;Static Web Server&lt;/a&gt;. It&amp;rsquo;s written in Rust, the docker image is less that 10MB, and it claims to be able to serve for virtual hosts. Let&amp;rsquo;s give it a try.&lt;/p&gt;
&lt;h3 id="directory-structure"&gt;Directory Structure&lt;/h3&gt;
&lt;p&gt;My routine setup now is that everything runs in docker. There&amp;rsquo;s a directory under the home directory of my user for named for the container which holds the &lt;code&gt;docker-compose.yml&lt;/code&gt;. Underneath that there&amp;rsquo;s two sub-directories: &lt;code&gt;data&lt;/code&gt; - which holds all the app data, and &lt;code&gt;config&lt;/code&gt; - which holds the app settings files.&lt;/p&gt;
&lt;p&gt;In the data directory, I want to have sub-directories for each virtual host, then inside them a &lt;code&gt;public&lt;/code&gt; directory which will hold the files to be served. This will allow me to keep config or other files for each virtual host in the directory above &lt;code&gt;public&lt;/code&gt; which should not be accessible. That seems like a good arrangement so I can manage each virtual host in git and pull the sites down from a repository into their directory without things like the &lt;code&gt;.gitignore&lt;/code&gt; being exposed to the world.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-8.15.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-8.15.14-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running Tailscale on this VM, so it can be referred to by any of the addresses &lt;code&gt;100.124.218.26&lt;/code&gt;, &lt;code&gt;192.168.100.35&lt;/code&gt; or &lt;code&gt;ct357-sws&lt;/code&gt;. These are going to be stand- ins for the different virtual host names. The index.html files you can see are all different; I&amp;rsquo;ve edited them so each one just outputs the name of the directory it is being served from. eg:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-9.20.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-9.20.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="docker"&gt;Docker&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#34;3.3&amp;#34;

services:
 website:
 image: joseluisq/static-web-server:2
 container_name: &amp;#34;sws&amp;#34;
 ports:
 - 80:80
 restart: unless-stopped
 environment:
 - SERVER_CONFIG_FILE=/etc/config.toml
 volumes:
 - ./data:/var
 - ./config/config.toml:/etc/config.toml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You&amp;rsquo;re probably familiar with docker-compose by now, but if not, the volumes lines probably need explaining. They map &amp;lsquo;real-world&amp;rsquo; directories on our server, to the internal directories &lt;em&gt;inside&lt;/em&gt; the container. This is a useful thing - we could copy our files into the container but those changes would be ephemeral, they&amp;rsquo;d be gone next time we restart the container. By creating these links, we can store the web server configs and data on our server, but from the point of view of the app, they appear inside.&lt;/p&gt;
&lt;p&gt;An example will make this more concrete, lets look at the first line:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; - ./data:/var
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is saying that the directory inside the container named &lt;code&gt;/var&lt;/code&gt; is mapped onto the external directory &lt;code&gt;./data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Consider our file in the tree listing above &lt;code&gt;~/sws/data/100.124.218.26/public/index.html&lt;/code&gt; the web server running inside the container will see that file at &lt;code&gt;/var/100.124.218.26/public/index.html&lt;/code&gt; It seems weird at first, but you soon get used to this sort of container directory mapping maths.&lt;/p&gt;
&lt;h3 id="config"&gt;Config&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s excellent documentation about Static Web Server on their &lt;a href="https://static-web-server.net/"&gt;web site&lt;/a&gt;, so I&amp;rsquo;m not going to go through the whole &lt;code&gt;config.toml&lt;/code&gt; file, in any case, I&amp;rsquo;ve left nearly everything on the defaults. Instead, lets scroll down to the bottom and look at the virtual hosts settings.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[[advanced.virtual-hosts]]
host = &amp;#34;100.124.218.26&amp;#34;
root = &amp;#34;/var/100.124.218.26/public&amp;#34;

[[advanced.virtual-hosts]]
host = &amp;#34;ct357-sws&amp;#34;
root = &amp;#34;/var/ct357-sws/public&amp;#34;

[[advanced.virtual-hosts]]
host = &amp;#34;192.168.100.35&amp;#34;
root = &amp;#34;/var/192.168.100.35/public&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="it-works"&gt;It works&lt;/h3&gt;
&lt;p&gt;A quick &lt;code&gt;docker compose up -d&lt;/code&gt;, and we&amp;rsquo;re in business.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-21-at-8.52.14-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-21-at-8.57.27-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>NGINX Proxy Manager</title><link>https://blog.iankulin.com/nginx-proxy-manager/</link><pubDate>Mon, 15 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-proxy-manager/</guid><description>&lt;p&gt;I&amp;rsquo;ve mentioned using NGINX as an &lt;a href="https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/"&gt;interface between the internet and a service&lt;/a&gt; a while ago. This works by all incoming traffic coming to NGINX, and NGINX determining which service that traffic should go (from the NGINX config files) then acting as a middleman. This functionality is generally referred to as a &amp;lsquo;reverse proxy&amp;rsquo;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/nginx.png" width="959" alt="Terrible drawing of NGINX proxying requests off to different services."&gt;
&lt;p&gt;This is nice for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can have a single point of entry to the services, easier to lock down and secure, with access centrally logged&lt;/li&gt;
&lt;li&gt;The services can be running on all sorts of odd addresses and ports (for example 192.168.101.23:4002) but they can be addressed with sensible names by the user (such as todo.example.com)&lt;/li&gt;
&lt;li&gt;We can add &lt;a href="https://blog.iankulin.com/quick-dirty-auth-with-nginx-node/"&gt;basic auth&lt;/a&gt; to any services that need it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All this stuff is managed through the &lt;a href="https://blog.iankulin.com/nginx-config-on-debian-ubuntu/"&gt;NGINX config&lt;/a&gt; files. Perhaps one might look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
 listen 80;
 server_name example.com;

 location / {
 root /var/www;
 index index.html;
 }

 location /app2/ {
 proxy_pass http://192.168.101.65:8096/;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 }

 location /secure_area/ {
 auth_basic &amp;#34;Restricted&amp;#34;;
 auth_basic_user_file /etc/nginx/.htpasswd;
 }
}

server {
 listen 80;
 server_name app1.example.com;

 location / {
 proxy_pass http://192.168.101.23:4000;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These config files are powerful, and in the usual trade-off somewhat complicated and I&amp;rsquo;ve certainly made problems for myself in the past by making errors in them.&lt;/p&gt;
&lt;p&gt;There is a great project, &lt;a href="https://nginxproxymanager.com/"&gt;NGINX Proxy Manager&lt;/a&gt; that throws a nice web GUI on this process. On top of that, it makes the process of obtaining &lt;a href="https://blog.iankulin.com/certbot-lets-encrypt-are-great/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; SSL certificates even easier than CertBot.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/npm.png" width="946" alt="Terrible drawing of NGINX Proxy Manager proxying requests off to different service, and obtaining SSL certificates for them."&gt;
&lt;p&gt;NGINX Proxy Manager is available as a docker image, and is trivial to set up if you&amp;rsquo;re used to docker. Once that&amp;rsquo;s done, the process of adding the proxies is simple enough that you probably don&amp;rsquo;t need any instructions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-31-at-8.59.56-am.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="alternatives"&gt;Alternatives&lt;/h3&gt;
&lt;p&gt;Rolling your own, or using NGINX Proxy Manager are not your only options. There&amp;rsquo;s also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.haproxy.org/#desc"&gt;HAProxy&lt;/a&gt; - an industrial strength proxy/load balancer&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/quick-starts/reverse-proxy"&gt;Caddy&lt;/a&gt; - same as NGINX but different. Has a great plugin architecture. A particular plugin &lt;a href="https://github.com/lucaslorentz/caddy-docker-proxy"&gt;Caddy-docker-proxy&lt;/a&gt; enables configuration of each service with tables inside the service&amp;rsquo;s docker-compose file which is a particularly neat trick.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://traefik.io/traefik/"&gt;Traefik&lt;/a&gt; - does a similar trick to Caddy-docker-proxy of figuring out it&amp;rsquo;s config from the services it&amp;rsquo;s proxying. It&amp;rsquo;s a serious bit of kit valuable for putting in front of huge Kubernetes swams, and is therefore probably a bit more complex to manage than Caddy-docker-proxy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I haven&amp;rsquo;t used any of these (except NGINX Proxy Manager) so take these descriptions as a starting point only.&lt;/p&gt;
&lt;p&gt;For any self-hosted (at home or on a VPS) services, you are going to need some of this functionality, and NGINX Proxy Manager is a simple, robust approach that should definitely be considered.&lt;/p&gt;</description></item><item><title>Due Diligence on a Docker Image</title><link>https://blog.iankulin.com/due-diligence-on-a-docker-image/</link><pubDate>Mon, 08 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/due-diligence-on-a-docker-image/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/gray-figure-ELLDKLrXMoA"&gt;&lt;img src="https://blog.iankulin.com/images/brett-jordan-elldklrxmoa-unsplash.jpg" width="640" alt=""&gt;&lt;/a&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@brett_jordan?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Brett Jordan&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/gray-figure-ELLDKLrXMoA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I need a survey tool, and a quick search turned up &lt;a href="https://www.limesurvey.org/"&gt;LimeSurvey&lt;/a&gt;, there&amp;rsquo;s a &amp;lsquo;community edition&amp;rsquo; so naturally I plan to self-host it. I scrolled down to the &amp;lsquo;installation&amp;rsquo; section of the &lt;a href="https://manual.limesurvey.org/Installation_-_LimeSurvey_CE/en"&gt;manual&lt;/a&gt; which has a big list of PHP dependencies.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-7.20.31-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Ain&amp;rsquo;t nobody got the time for that in 2024, I scroll further looking for the docker-compose but there isn&amp;rsquo;t one. Huh. No official Docker image.&lt;/p&gt;
&lt;p&gt;Making my own docker image will be a little bit more work than just the pain of installing it manually and writing the Ansible playbook, but almost certainly someone else will have done that for us, so pop over to Docker Hub to have a look.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-7.38.06-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to want the &amp;rsquo;trusted content&amp;rsquo; right?&lt;/p&gt;
&lt;h3 id="who-do-you-trust"&gt;Who do you trust?&lt;/h3&gt;
&lt;p&gt;At some stage when you run software, you are going to need to decide who you trust for this application. You might say &amp;ldquo;no, as an Open Source advocate, I&amp;rsquo;m going to read through the code and compile it myself&amp;rdquo; - and that&amp;rsquo;s going to be a legit approach in some circumstances. But of course you&amp;rsquo;re still choosing to trust all the libraries it uses, the compiler, the operating system, and the computer chipset (or worse - your hosting provider).&lt;/p&gt;
&lt;p&gt;If you want to go hardcore secure, you&amp;rsquo;ll eventually find yourself constructing your own &lt;code&gt;[fab](https://en.wikipedia.org/wiki/Semiconductor_fabrication_plant)&lt;/code&gt;. This is the concept of the Security-Convenience trade-off. More security generally equals less convenience and vice versa.&lt;/p&gt;
&lt;p&gt;To make a sensible decision about this when choosing software I think about the threat profile, and have some rough metrics about what makes me more comfortable or less comfortable about a software artifact.&lt;/p&gt;
&lt;p&gt;The reason this is a front-of-mind issue for me is that software packaging is an easy stage for bad actors to insert their code. We&amp;rsquo;ve seen a &lt;a href="https://popey.com/blog/2024/02/exodus-bitcoin-wallet-490k-swindle/"&gt;bit of that over the past month&lt;/a&gt; in the Canonical Snap store. Nefarious bitcoin stealers were packaging their software as legit, users were downloading them and installing them and one cryptobro lost a heap of money. It&amp;rsquo;s easy to imagine lots of related exploits - for example just altering the original code and packaging it. In the case of LimeSurvey (which is used by many researchers at leading universities perhaps a bad actor might want to be able to run bots from inside verifiable uni IP addresses.&lt;/p&gt;
&lt;h3 id="trust-factors"&gt;Trust Factors&lt;/h3&gt;
&lt;p&gt;What are the things that might reduce our anxiety about the software bill of materials we&amp;rsquo;re dealing with? These are just mine - a complete non-expert:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trusted organisation - If the Ubuntu team have signed off on something, I&amp;rsquo;m going to trust it more that some other group I never heard of till today.&lt;/li&gt;
&lt;li&gt;Popular - it&amp;rsquo;s not just it&amp;rsquo;s reassuring that other people have decided it&amp;rsquo;s trustworthy, it&amp;rsquo;s also a comforting idea that if there is an exploit, it&amp;rsquo;s more likely to be discovered, and to hurt someone else first. If there&amp;rsquo;s a zero day exploit in MacOS I&amp;rsquo;ll hear about it on Mastodon and be able to get some advice about a work-around or fix.&lt;/li&gt;
&lt;li&gt;Updated - a project under current development is more likely to be applying updates to any compromised libraries, and there&amp;rsquo;s probably a community of some sort where security concerns are considered.&lt;/li&gt;
&lt;li&gt;Verifiable - One of the big strengths of Open Source. Even if I don&amp;rsquo;t have the skills or time to read the code, there&amp;rsquo;s a good chance that other people are. And in the case of a container image, I actually do sort of have the skills to read the dockerfile and know what&amp;rsquo;s gone into it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="choosing-an-image"&gt;Choosing an Image&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s apply some of these rules-of-thumb to my LimeSurvey container needs.&lt;/p&gt;
&lt;p&gt;When Docker Hub says something is &amp;ldquo;trusted content&amp;rdquo; they&amp;rsquo;ve already done some of my work for me. And when I click through to &lt;a href="https://hub.docker.com/r/eucm/limesurvey"&gt;eucm/limesurvey&lt;/a&gt; it says that the developer (eucm) is a &amp;ldquo;&lt;a href="https://docs.docker.com/trusted-content/dsos-program/#:~:text=The%20Docker-Sponsored%20Open%20Source,Insights%20and%20analytics"&gt;Sponsored OSS&lt;/a&gt;&amp;rdquo; which means a human at Docker thinks this developer group are legit. These are all encouraging factors.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-9.47.41-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-9.47.41-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s less encouraging is &amp;ldquo;Pulls 635&amp;rdquo; which does not seem a lot, and &amp;ldquo;Updated over 3 years ago&amp;rdquo;. Also I&amp;rsquo;ve got no reason to think this is an official image - eucm doesn&amp;rsquo;t seem to have any connection to the LimeSurvey developers. Let&amp;rsquo;s look at the other images.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-10.25.21-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-10.25.21-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s more further down but all with about 100K pulls or less. Of these four, we&amp;rsquo;ve already eliminated the top one, and we can probably eliminate the bottom one based on it&amp;rsquo;s age - although it&amp;rsquo;s always interesting to see an image with so many pulls and no updates since that sometimes happens when a project changes hands or gets relisted under a different owner.&lt;/p&gt;
&lt;p&gt;Either of the other two (acspri/limesurvey &amp;amp; martialblog/limesurvey) are worth investigating - there&amp;rsquo;s nothing much to tell them apart in this view since they both have about the same number of stars (the number next to the pulls number). I&amp;rsquo;ll start at the top.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-11.34.52-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-11.34.52-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s promising to have a good readme, including a docker-compose example, but there&amp;rsquo;s no github link, and I really want a peek at the dockerfile. If I click through to the developer, they seem to be the &lt;a href="https://www.acspri.org.au"&gt;Australia Consortium for Social and Political Research Inc&lt;/a&gt; - which it makes sense why they would be interested in a survey tool. Additionally, they seem to have an &lt;a href="https://www.acspri.org.au/limesurvey"&gt;official link to the project&lt;/a&gt;. They do have an active GitHub account, but it doesn&amp;rsquo;t include a repository for this which seems, odd.&lt;/p&gt;
&lt;p&gt;However, there is this repo &lt;a href="https://github.com/adamzammit/limesurvey-docker"&gt;adamzammit / limesurvey-docker&lt;/a&gt; which appears to get pushed to it&amp;rsquo;s own &lt;a href="https://hub.docker.com/r/adamzammit/limesurvey"&gt;dockerhub&lt;/a&gt; as well as the ACSPRI one. There is a note on the adamzamit dockerhub saying this used to be the acspri but has been moved here - that would be more convincing if the note was on the acspri dockerhub. The github looks legit, and there was nothing suspicious (to my inexpert view) in the dockerfile.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-30-at-11.33.52-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-30-at-11.33.52-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://hub.docker.com/u/martialblog"&gt;martialblog/limesurvey&lt;/a&gt; one also looks good - recently updated, lots of pulls, a comprehensive readme, and a github link right at the top. The only criteria it doesn&amp;rsquo;t meet is &amp;ldquo;trusted organisation&amp;rdquo; but the link to an active github goes part of the way. I&amp;rsquo;d be happy to use this container for the purpose I&amp;rsquo;ve got in mind.&lt;/p&gt;</description></item><item><title>My Web App Update Process</title><link>https://blog.iankulin.com/my-web-app-update-process/</link><pubDate>Mon, 01 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/my-web-app-update-process/</guid><description>&lt;p&gt;I&amp;rsquo;ve settled on a very standard, reproducible setup for services in my homelab. This post looks at that, then runs through the update I did today to Forgejo which only took a few minutes and felt relatively risk free.&lt;/p&gt;
&lt;h3 id="standard-setups"&gt;Standard Setups&lt;/h3&gt;
&lt;p&gt;My system is based around Proxmox. I have three physical machines - one for production apps, a production spare, and a development/testbed machine. A Synology NAS serves for backups. Moving a VM or LXC between the machines is trivial; but it&amp;rsquo;s done manually - the machines are not clustered for high availability.&lt;/p&gt;
&lt;p&gt;Most workloads are Docker containers &lt;em&gt;inside&lt;/em&gt; an LXC. This works fine with a couple of caveats. I have an LXC template saved with Docker and Tailscale installed, my non-root user added, the mount for the NAS, and SSH keys. Setting up a new app starts with a full clone of this, a &lt;code&gt;dpkg-reconfigure openssh-server&lt;/code&gt; and &lt;code&gt;tailscale up&lt;/code&gt; and changing the root &amp;amp; non-root users&amp;rsquo; passwords.&lt;/p&gt;
&lt;p&gt;Next I create a sub directory for the app and write the &lt;code&gt;docker-compose.yaml&lt;/code&gt; in there. Then it&amp;rsquo;s just a matter of &lt;code&gt;docker compose up -d&lt;/code&gt;. If there&amp;rsquo;s any data, it goes in a another sub directory off this one.&lt;/p&gt;
&lt;p&gt;Unless I need something else, nightly backups to the NAS happen automatically for all the VMs and containers handled by a setting in Proxmox.&lt;/p&gt;
&lt;h3 id="upgrading-an-app"&gt;Upgrading an App&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve noticed a couple of posts about a new release of &lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt; on Mastodon in the past few days, so I figure I should look at that. My version is 1.21.1 and the new one is 1.21.8&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.44.36-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Because of &lt;a href="https://semver.org/"&gt;semantic versioning&lt;/a&gt;, I&amp;rsquo;m confident this is not going to break anything, but I check the release notes anyway. It looks good.&lt;/p&gt;
&lt;h4 id="backup"&gt;Backup&lt;/h4&gt;
&lt;p&gt;I jump into the Proxmox web gui and make a backup of the container.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.47.06-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;h4 id="docker-compose"&gt;Docker Compose&lt;/h4&gt;
&lt;p&gt;I ssh in to look at the image tag in the docker-compose.yml file. The reason I&amp;rsquo;m interested in this is that if the compose is set to &lt;code&gt;codeberg.org/forgejo/forgejo:1.21.1&lt;/code&gt; then it will be locked into that patch version, but it says &lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt; so we&amp;rsquo;re good.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.48.38-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now I take the service down from the CLI with &lt;code&gt;sudo docker compose down&lt;/code&gt;, then pull the new image with &lt;code&gt;sudo docker pull codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-9.24.21-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The to start it again, it&amp;rsquo;s just a &lt;code&gt;docker compose up -d&lt;/code&gt; and we&amp;rsquo;re live again.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.52.45-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.52.45-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="testing"&gt;Testing&lt;/h4&gt;
&lt;p&gt;My testing of this was pretty brief since (a) I&amp;rsquo;ve got high confidence in the developers at &lt;a href="https://blog.iankulin.com/gogs-gitea-forgejo/"&gt;gitea and forgejo&lt;/a&gt; and (b) this app gets pretty much daily use so if there are issues I&amp;rsquo;ll surface them pretty quickly, (c) anything I&amp;rsquo;m actively working on had full git histories on my laptop, and (d) the releases since my last update are pretty much just bug fixes.&lt;/p&gt;
&lt;p&gt;Nevertheless, I clicked around the web gui, and tried some pushes, pulls and clones and everything seemed fine.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m very comfortable with the way I&amp;rsquo;ve put all this together now. It&amp;rsquo;s a reliable, easily managed setup that makes maintenance like this simple and safe.&lt;/p&gt;</description></item><item><title>Deploying a Node app in Docker</title><link>https://blog.iankulin.com/deploying-a-node-app-in-docker/</link><pubDate>Sun, 31 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/deploying-a-node-app-in-docker/</guid><description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Cargo_ship#/media/File:Cargo_Ship_Puerto_Cortes.jpg"&gt;&lt;img src="https://blog.iankulin.com/images/cargo_ship_puerto_cortes.jpg" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I wrote the install instructions for mdserver (little Markdown server Node app) on it&amp;rsquo;s &lt;a href="https://github.com/IanKulin/mdserver"&gt;github page&lt;/a&gt; it was something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have node.js installed and working&lt;/li&gt;
&lt;li&gt;Clone the repo&lt;/li&gt;
&lt;li&gt;Start with &lt;code&gt;npm start&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which is great if you know &lt;a href="https://blog.iankulin.com/installing-a-node-app-on-a-server/"&gt;how to do those things&lt;/a&gt; (they are bread and butter to a web dev) but not if you&amp;rsquo;re a self-hoster who just wants a web server that converts markdown to HTML on the fly. For any situation where you just want to use the app, what you probably want is a Docker image of the app.&lt;/p&gt;
&lt;h3 id="docker"&gt;Docker&lt;/h3&gt;
&lt;p&gt;Docker &lt;em&gt;containers&lt;/em&gt; are similar to a virtual machine in the sense that they need to be hosted, and are relatively isolated from other processes except is some explicitly defined ways. Docker images are stored in repositories (the default one is &lt;a href="https://hub.docker.com/"&gt;DockerHub&lt;/a&gt;). It probably sounds like a wasteful process to ship an entire operating system with every little app - this is somewhat overcome by the images being built up in layers, and duplicated layers don&amp;rsquo;t need to be shlipped around since they are cached.&lt;/p&gt;
&lt;p&gt;So to deploy our Node app as a Docker container, we need to build an image, and store it on Docker Hub. From there, users can deploy it from their command lines by calling it directly or declaratively with a &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;h3 id="dockerfile"&gt;Dockerfile&lt;/h3&gt;
&lt;p&gt;To create a Docker image of our app that can be distributed, we run the &lt;code&gt;docker build&lt;/code&gt; command which reads a file named &lt;code&gt;Dockerfile&lt;/code&gt; to create the image. Here&amp;rsquo;s the &lt;code&gt;Dockerfile&lt;/code&gt; for the mdserver app.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Use an official Node.js runtime as the base image
FROM node:20-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json to the working directory
COPY package*.json .

RUN npm install

# Copy the rest of the application source code to the container
COPY ./server.js .
COPY ./LICENSE .
COPY ./readme.md .

# Expose the port that the Node.js app will listen on
EXPOSE 3000

# Define the command to start your Node.js app
CMD [ &amp;#34;node&amp;#34;, &amp;#34;server.js&amp;#34; ]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;FROM node:20-alpine&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FROM&lt;/code&gt; keyword specifies the base image we&amp;rsquo;re starting with. It could just be something like a Debian base, then in the following commands in the Dockerfile we&amp;rsquo;d install Node, but Node (and lots of other web dev tool builders) have provided &lt;a href="https://hub.docker.com/_/node/"&gt;official Docker images&lt;/a&gt; that they have crafted to make it easier for us. In this case I&amp;rsquo;m specifying that I want the image based on the lightweight Alpine Linux distro, with version 20 of Node installed on it.&lt;/p&gt;
&lt;p&gt;Note that when Node created the &lt;code&gt;node:20-alpine&lt;/code&gt; image, their Dockerfile probably started with &lt;code&gt;FROM [alpine:3.18](https://hub.docker.com/_/alpine)&lt;/code&gt; - you see? Layers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WORKDIR /usr/src/app&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So now we&amp;rsquo;ve got a container with a fully working install of Linux (or close enough to that so we can think of it like that - I&amp;rsquo;m pretty sure there&amp;rsquo;s no kernel). This command is saying that all the next commands are going to refer to the working directory &lt;em&gt;inside&lt;/em&gt; the container as &lt;code&gt;/usr/src/app&lt;/code&gt;. In effect its as if you&amp;rsquo;d ssh&amp;rsquo;d in and run &lt;code&gt;mkdir /usr/scr/app &amp;amp;&amp;amp; cd mkdir /usr/scr/app&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY package*.json .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve written before about the &lt;a href="https://blog.iankulin.com/sorting-out-node-package-dependencies-when-cloning-old-repos/"&gt;intricacies of the package files in Node&lt;/a&gt;. Basically these files (&lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt;) specify the dependencies for out project. The dependencies are all sitting in the node_modules folder, but having a listing of them in the package files means we can just check them into source control and not worry about that bloated folder.&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;COPY&lt;/code&gt; command, just copies them both into out container image - the &lt;code&gt;.&lt;/code&gt; at the end just means the current working directory inside the container - ie &lt;code&gt;/usr/src/app&lt;/code&gt; in our case.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RUN npm install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now the package files are inside the container, we just run &lt;code&gt;npm install&lt;/code&gt;, exactly the same as we would on a server, in order to download all of the dependencies for our app into the container. If that looks like you could just say &lt;code&gt;RUN&lt;/code&gt; then run any old Linux command then you&amp;rsquo;re getting the hang of it. You can &lt;code&gt;apt install&lt;/code&gt; stuff, &lt;code&gt;echo&lt;/code&gt; a line into a config file - whatever you need.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./server.js .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./LICENSE .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./readme.md .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For this trivial app, we only need the one source file, but I like to copy the license and readme in as well. It&amp;rsquo;s possible for future users of the container to run commands in their copy of the container, so it&amp;rsquo;s conceivable someone might look in here to read them. Once again, the second parameter specifies where in the container the files are copied to, and once again we&amp;rsquo;ve said the current work dir.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll very commonly see &lt;code&gt;COPY . .&lt;/code&gt; in Dockerfiles. This is saying copy all the files in the current directory to the working directory inside the container image. I guess that way you don&amp;rsquo;t miss anything, but do I really need a copy of my &lt;code&gt;Dockerfile&lt;/code&gt;, my vscode settings, my &lt;code&gt;node_modules&lt;/code&gt; folder in the image? No. There is a way to avoid copying that stuff in - add a &lt;code&gt;.dockerignore&lt;/code&gt; file to your project. This works exactly like a &lt;code&gt;.gitignore&lt;/code&gt; - you just list one file or directory per line, and then the &lt;code&gt;COPY&lt;/code&gt; command will know not to bother with it,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EXPOSE 3000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My node app is set to use port 3000, so we need to tell Docker to open that port for us since by default everything&amp;rsquo;s locked down. Note that the user of this container won&amp;rsquo;t be stuck with this decision, when they start the container, they can specify where in the outside world this internal container is going to be mapped to. That could be port &lt;code&gt;8080&lt;/code&gt;, &lt;code&gt;80&lt;/code&gt; or whatever.&lt;/p&gt;
&lt;p&gt;CMD [ &amp;ldquo;node&amp;rdquo;, &amp;ldquo;server.js&amp;rdquo; ]&lt;/p&gt;
&lt;p&gt;Finally, Docker needs to know how to start our app. This command is not being run now (when we&amp;rsquo;re building the image) it&amp;rsquo;s used by Docker when it launches the containerised app. I&amp;rsquo;m not sure why it is an array of strings instead of just a string, but it is. Just break it at each space in your command to run the app.&lt;/p&gt;
&lt;p&gt;If you look back at the list of manual steps I started this post with, you&amp;rsquo;ll see that we&amp;rsquo;ve pretty much just re-implemented them in the Dockerfile:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set up a node environment&lt;/li&gt;
&lt;li&gt;copy the files in&lt;/li&gt;
&lt;li&gt;run server.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously there&amp;rsquo;s lots more you can do with Dockerfiles, but the underlying concept is pretty straightforward - you&amp;rsquo;re setting up the whole environment for your app to run in so it can be mostly independent from its host OS.&lt;/p&gt;
&lt;h3 id="build-step"&gt;Build Step&lt;/h3&gt;
&lt;p&gt;To create the image from the Dockerfile, you are going to need Docker. I&amp;rsquo;m working on a Mac so I&amp;rsquo;ve got &lt;a href="https://www.docker.com/products/docker-desktop/"&gt;Docker Desktop&lt;/a&gt; installed. When it&amp;rsquo;s running there&amp;rsquo;s the little whale up in the toolbar.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t need a &lt;a href="https://hub.docker.com/"&gt;DockerHub&lt;/a&gt; account to build the image, but you&amp;rsquo;ll need one to upload it, and for naming your build, so head there now and create one. It is possible to use other registries for storing your images, but by default docker looks at it&amp;rsquo;s own registry, so that&amp;rsquo;s the best place to start when you&amp;rsquo;re figuring things out.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re working with Docker images and registries, to uniquely identify an image, it usually has a name format like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;username&amp;gt;/&amp;lt;imagename&amp;gt;:&amp;lt;tag&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Usually the tag will be a version number, or perhaps &lt;code&gt;:latest&lt;/code&gt;. The build command for our image could be this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build -t iankulin/mdserver:latest&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.27.51-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This will load the .dockerignore then step through the Dockerfile to build our image. The image is stored away by Docker - we don&amp;rsquo;t need to worry about where. You can get the list at the command line with &lt;code&gt;docker images&lt;/code&gt;, or if you&amp;rsquo;re running Docker Desktop, on the &amp;lsquo;images&amp;rsquo; tab.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.36.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.36.22-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have skipped quite a bit of detail about the build step and options. For example I sometimes use the &lt;code&gt;--platform&lt;/code&gt; flag to specify &lt;code&gt;linux/amd64&lt;/code&gt; if I&amp;rsquo;m testing on one of my homelab VMs rather than &lt;code&gt;linux/arm64&lt;/code&gt; if I&amp;rsquo;m running the container on the mac. Also, we don&amp;rsquo;t have to just build from the local machine, it&amp;rsquo;s just as straightforward to build from your GitHub repo as part of a CI/CD system. I&amp;rsquo;m not planning to go into any of that today, except I will force it to build for x86 since it is my plan to test on the homelab VM&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build --platform linux/amd64 -t iankulin/mdserver:latest .&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="the-registry"&gt;The Registry&lt;/h3&gt;
&lt;p&gt;So the images can be available to anyone, we need to make it available in a Docker Registry. The most famous one of these, and the one set up as the default for all the docker commands, is &lt;a href="https://hub.docker.com/"&gt;Docker Hub&lt;/a&gt;. Despite some &lt;a href="https://www.docker.com/blog/no-longer-sunsetting-the-free-team-plan/"&gt;missteps&lt;/a&gt;, it&amp;rsquo;s still the main place people and organisations store docker images.&lt;/p&gt;
&lt;p&gt;In order to push an image to a registry, we need to be signed in to it. As I&amp;rsquo;m using Docker Desktop, and I&amp;rsquo;m signed in to Docker Hub on that. I&amp;rsquo;ve skipped that step, but if you needed to, you&amp;rsquo;d use the &lt;a href="https://docs.docker.com/engine/reference/commandline/login/"&gt;docker login&lt;/a&gt; command. Once that&amp;rsquo;s sorted, the push is easy:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker push iankulin/mdserver:latest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.12.15-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.12.15-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this output, you can see some of the efficiencies of the layers - docker recognises (from the UUIDs) that the Alpine and Node layers are ones that I pulled down from it when I was creating the image locally, so it doesn&amp;rsquo;t send them back to Docker Hub.&lt;/p&gt;
&lt;p&gt;If we go to Docker Hub and search for mdserver, we should be able to find it now available to the public.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.10.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.10.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="using-the-image"&gt;Using the image&lt;/h3&gt;
&lt;p&gt;Now it&amp;rsquo;s in the registry, anyone can use it as easily as any of the Docker images - NGINX, Jellyfin - whatever. I provide a docker-compose file in the repo, it looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;
services:
 mdserver:
 image: iankulin/mdserver:latest
 ports:
 - &amp;#34;3000:3000&amp;#34;
 volumes:
 - ./public:/usr/src/app/public 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So any user can just drop that into a directory, and enter &lt;code&gt;docker compose up -d&lt;/code&gt; then the image will be pulled down and run, and they&amp;rsquo;ll have their server live.&lt;/p&gt;</description></item><item><title>Hosting Your Own Docker Registry</title><link>https://blog.iankulin.com/hosting-your-own-docker-registry/</link><pubDate>Mon, 25 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hosting-your-own-docker-registry/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/architectural-photography-of-cargo-containers-stack-hP4ZiN1_kdk?utm_content=creditShareLink&amp;utm_medium=referral&amp;utm_source=unsplash"&gt;&lt;img src="https://blog.iankulin.com/images/tri-eptaroka-mardiana-hp4zin1_kdk-unsplash.jpg" width="640" alt="Photo by Tri Eptaroka Mardianam on Unsplash
"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Docker &lt;a href="https://docs.docker.com/subscription/core-subscription/details/"&gt;Personal (ie free tier) plan&lt;/a&gt; currently allows one private repository, but even if you want to pay for the next level where you can have unlimited repositories, you may still want to host your own private registry - it&amp;rsquo;s going to be quicker inside your network, and you won&amp;rsquo;t run up against Docker&amp;rsquo;s pull/push limits if you are hammering it with your CI/CD system.&lt;/p&gt;
&lt;p&gt;There are fancier tools, but in this post we&amp;rsquo;ll look at the basics of how to use the official registry app from Docker.&lt;/p&gt;
&lt;h3 id="initial-setup"&gt;Initial Setup&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://hub.docker.com/_/registry"&gt;registry app&lt;/a&gt; is (unsurprisingly) dockerised. So I&amp;rsquo;ve created a directory for the &lt;code&gt;docker-compose.yml&lt;/code&gt; file, and a &lt;code&gt;data&lt;/code&gt; sub directory.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-7.50.43-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And the yaml.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services: registry: image: registry:2 container_name: registry restart: unless-stopped ports: - &amp;#34;5000:5000&amp;#34; environment: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data volumes: - ./data:/data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;docker compose up&lt;/code&gt;, and bingo. Our registry is live.&lt;/p&gt;
&lt;h3 id="creating-an-image"&gt;Creating an image&lt;/h3&gt;
&lt;p&gt;Now our registry is up, let&amp;rsquo;s jump over to another machine, and create an image to store in it. I&amp;rsquo;m only going to minimally explain this, since if you&amp;rsquo;re interested in your own registry, you&amp;rsquo;ve probably been down this path.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.24.50-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busyboxRUN mkdir /appCOPY script.sh /app/script.shWORKDIR /appRUN chmod +x script.shCMD [&amp;#34;./script.sh&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;script.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/shecho &amp;#34;Hello from Docker!&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So basically, this image contains a small Linux distro, and all it does is run a script that outputs &amp;ldquo;Hello from Docker!&amp;rdquo; to the console. We can build our image by switching into the directory with the &lt;code&gt;dockerfile&lt;/code&gt; and running:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker build -t hello-docker .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.37.15-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you want to run it to check my docker skills, use&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker run hello-docker
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="pushing--insecure"&gt;Pushing &amp;amp; Insecure&lt;/h3&gt;
&lt;p&gt;Now I want to push the image we&amp;rsquo;ve created to the new registry we set up earlier, but we&amp;rsquo;re going to run into a problem.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using two Debian virtual machines (LXCs actually) both on my homelab network. They&amp;rsquo;ve been named with Tailscale to make things clearer in the screenshots. (If you&amp;rsquo;re following along you&amp;rsquo;ll probably be using IP addresses). Importantly, there are no TLS certificates, self-signed or otherwise.&lt;/p&gt;
&lt;p&gt;First we need to tag our image to include the registry name:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker tag hello-docker:latest ct390-docker-reg:5000/hello-docker
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.53.18-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And we&amp;rsquo;ll try to push it up to our registry with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker push ct390-docker-reg:5000/hello-docker
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.35.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s happening is that Docker would (quite reasonably) prefer to only work over secure connections. We can override this on this machine for today&amp;rsquo;s demo purposes by adding an exception for our self-hosted registry. You&amp;rsquo;ll need to create the file &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; and add the registry that&amp;rsquo;s going to be allowed like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we restart docker and retry the push now, it should work:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.43.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That looks like it worked. If we wanted to check, we can just hit an endpoint on the registry:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl http://ct390-docker-reg:5000/v2/_catalog
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.49.36-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="pulling--insecure"&gt;Pulling &amp;amp; Insecure&lt;/h3&gt;
&lt;p&gt;Of course the ultimate test is going to be to use this image from a third machine, so let&amp;rsquo;s spin one up with a clean docker install with no images and try to run the image we&amp;rsquo;ve just added to our registry.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to have the same challenge pulling from a non-TLS registry as we had pushing to it, and the workaround is going to be exactly the same - add the registry to the insecure list in the &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;echo &amp;#39;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}&amp;#39; | sudo tee /etc/docker/daemon.jsonsudo systemctl daemon-reloadsudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we can run it. Since we don&amp;rsquo;t have the image locally yet, docker will pull it down for us from the registry before running it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-3.19.03-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. Our own private Docker registry to store our images.&lt;/p&gt;
&lt;h4 id="references"&gt;References&lt;/h4&gt;
&lt;p&gt;In writing this post, I relied on some these resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Digital Ocean - &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-private-docker-registry-on-ubuntu-20-04"&gt;How To Set Up a Private Docker Registry on Ubuntu 20.04&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Baeldung - &lt;a href="https://www.baeldung.com/ops/docker-private-registry"&gt;Configure a Private Docker Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;O&amp;rsquo;Reilly - &lt;a href="https://www.oreilly.com/library/view/kubernetes-in-the/9781492043270/app03.html"&gt;Configuring Docker to Push or Pull from an Insecure Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Certbot - removing a domain</title><link>https://blog.iankulin.com/certbot-removing-a-domain/</link><pubDate>Mon, 18 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/certbot-removing-a-domain/</guid><description>&lt;p&gt;I had a number of domains all running on one host when I first set them up with certbot. One started to be serious, so I moved it to another host and ran certbot there. That all worked perfectly, but of course, the old domain is still part of the original certificate, so when I went to renew it, it came up with some errors.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a few commands that are going to help navigate this situation if you&amp;rsquo;ve found yourself in the same spot:&lt;/p&gt;
&lt;h4 id="show-all-certificates-and-which-domains"&gt;Show all certificates and which domains&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo certbot certificates
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="renew-just-some-domains"&gt;Renew just some domains&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s no way to delete a domain from a certificate, the process is to renew it, but just for the domains you want to keep. Certbot will notice you&amp;rsquo;ve missed some and warn you that you&amp;rsquo;re effectively deleting them.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo certbot --cert-name &amp;lt;certifcate-name&amp;gt; -d &amp;lt;domain1&amp;gt; -d &amp;lt;domain-2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Quick &amp;&amp; Dirty auth with nginx &amp; Node</title><link>https://blog.iankulin.com/quick-dirty-auth-with-nginx-node/</link><pubDate>Fri, 23 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/quick-dirty-auth-with-nginx-node/</guid><description>&lt;p&gt;One of the basic requirements for any serious web app is a proper users/roles/authentication system - but if you&amp;rsquo;re just throwing up a utility of some kind on a public IP for testing, and you don&amp;rsquo;t want it to be abused, then this could be an option. There&amp;rsquo;s a few components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your app. In this demo it&amp;rsquo;s going to be Node, but it could be Go or whatever your server-side poison is. The app is listening for connections on a non-web port (ie not on 80 or 443), I&amp;rsquo;m going to use the traditional 3000.&lt;/li&gt;
&lt;li&gt;A firewall. That port (in my example 3000) must not be accessible from the internet. It has to be blocked by a firewall.&lt;/li&gt;
&lt;li&gt;A web server (I&amp;rsquo;m using nginx) that enforces basic auth.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I briefly discussed web server basic auth earlier - it&amp;rsquo;s a system built into the web server that requires a log in for a route, and authenticates it against the credentials in a password file (usually named &lt;code&gt;.htpasswrd&lt;/code&gt;) and only serves the content if authenticated.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to complicate that a bit by then inserting the authenticated user name into a header, so that we can access it in our node app. The web server does this as it passes the incoming request to our app in a process called proxy-ing.&lt;/p&gt;
&lt;h3 id="prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;You&amp;rsquo;re going to need a server, separate to the machine you&amp;rsquo;re using. I&amp;rsquo;m going to use an LXC container on one of my Proxmox servers, but perhaps you&amp;rsquo;re on windows and have a WSL to play with, or you&amp;rsquo;ve perhaps you&amp;rsquo;ve spun up a baby server on Hetzner, Linode or Digital Ocean. What ever floats your boat. You need to be able to set it up and &lt;code&gt;ssh&lt;/code&gt; into it to follow along.&lt;/p&gt;
&lt;p&gt;All my examples are assuming Debian, so that or a Debian based distro like Ubuntu is going to be simplest, but if you&amp;rsquo;re on something with a different package management system, you&amp;rsquo;re probably able to translate things to that.&lt;/p&gt;
&lt;h3 id="install-nginx"&gt;Install nginx&lt;/h3&gt;
&lt;p&gt;To install nginx, we just&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt install nginx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now if we open the server ip address, we should see the nginx test page:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-3.23.32-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-3.23.32-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re wondering where this page comes from, it&amp;rsquo;s &lt;code&gt;/var/www/html/index.nginx-debian.html&lt;/code&gt;. There&amp;rsquo;s a default nginx site config at &lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt; that points to it. We&amp;rsquo;ll be playing in there later.&lt;/p&gt;
&lt;h2 id="installing-node"&gt;Installing Node&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt install nodejssudo apt install npm
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is going to install the version of node and npm that are provided by Debian or the Debian related distro you&amp;rsquo;re using, so they won&amp;rsquo;t be the latest and greatest, but they will be stable and bug patched to whatever level your distro maintainers think they should be. You could check with &lt;code&gt;node -v&lt;/code&gt; and &lt;code&gt;npm -v&lt;/code&gt; if you were interested, but we&amp;rsquo;re not using any bleeding edge features here, so whatever you&amp;rsquo;ve got it should be fine. For reverence, I have node v18.19.0, and npm 9.2.0&lt;/p&gt;
&lt;h3 id="the-app"&gt;The App&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;re going to create a very basic node/Express server app to run on our server. I&amp;rsquo;m going to remote in with VS Code because that&amp;rsquo;s how I roll this week, but do this however you want. Nano is fine, or maybe you&amp;rsquo;re a vim person. Perhaps for these examples we&amp;rsquo;ll assume you&amp;rsquo;re a sane person near the start of their dev journey and use nano. &lt;code&gt;ssh&lt;/code&gt; to the server, then:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir appcd appnpm initnpm install expressnano app.js
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, our app code in app.js&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#39;express&amp;#39;);const app = express();const port = 3000;app.get(&amp;#39;/&amp;#39;, (req, res) =&amp;gt; { res.send(&amp;#39;Hello World&amp;#39;);});app.listen(port, () =&amp;gt; { console.log(`Server is listening at http://localhost:${port}`);});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we&amp;rsquo;ve done everything right, once you&amp;rsquo;ve saved that (ctl-O, ctl-X) if we run &lt;code&gt;node app.js&lt;/code&gt; we&amp;rsquo;ll get the message &lt;code&gt;Server is listening at http://localhost:3000&lt;/code&gt; and visiting the IP address of our server with &lt;code&gt;:3000&lt;/code&gt; on the end should get this result:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-3.56.39-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-3.56.39-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="the-firewall"&gt;The Firewall&lt;/h3&gt;
&lt;p&gt;Firewalls are their own big thing that I should write about another time. Suffice to say we&amp;rsquo;re going to make it so outside traffic can&amp;rsquo;t access our app on port 3000 (so we can force them to go through nginx where we authenticate them).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt-get install netfilter-persistentsudo iptables -A INPUT -p tcp --dport 3000 -j DROPsudo netfilter-persistent savesudo netfilter-persistent reload
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now if you start the app again with &lt;code&gt;node app.js&lt;/code&gt; and visit :3000 in the browser, it should eventually just time out because the request is never making it to our app.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-4.17.42-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-4.17.42-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="proxy-pass"&gt;Proxy Pass&lt;/h3&gt;
&lt;p&gt;So now that raw access from the network to our app is blocked off, we want to configure nginx to pass any requests to our app. There&amp;rsquo;s a number of good reasons why you should put a web server in front of you apps, but today we&amp;rsquo;re doing it so we can authenticate the users. We&amp;rsquo;ll get to that, but for the moment, we need to edit &lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Scroll down till you see the &lt;code&gt;location / {&lt;/code&gt; block. Delete out the contents and replace it with&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;proxy_pass http://localhost:3000;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-4.49.27-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-4.49.27-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then we&amp;rsquo;ll check the configuration is okay, and restart the nginx server.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo nginx -tsudo service nginx restart
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now if our app is running (&lt;code&gt;node app.js&lt;/code&gt;) you should be able to go to the server address (without the :3000) and see the app working again.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-4.55.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-4.55.54-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="credentials"&gt;Credentials&lt;/h3&gt;
&lt;p&gt;Now we need to create a file with our credentials, so nginx can have something to check against. The first web server that I ever used that did this was &lt;a href="https://httpd.apache.org/"&gt;Apache&lt;/a&gt;, and that format has carried forward to be used by nginx. I&amp;rsquo;m mentioning this to explain why I&amp;rsquo;m about to tell you to install some Apache tools.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt install apache2-utilssudo htpasswd -c /etc/nginx/.htpasswd user1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This second command is creating (that&amp;rsquo;s the &lt;code&gt;-c&lt;/code&gt; flag) a text file called &lt;code&gt;.htpasswd&lt;/code&gt; in the &lt;code&gt;/etc/nginx&lt;/code&gt; directory. It doesn&amp;rsquo;t matter that much what it&amp;rsquo;s called or where it is - we&amp;rsquo;re going to specify that later in the nginx conf, but I like to put it somewhere I&amp;rsquo;d probably guess later.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;user1&lt;/code&gt; is just what I&amp;rsquo;ve called this user - it could of course be just about anything. htpasswd will ask you to enter a password for this user, and confirm it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-5.55.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-5.55.14-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re curious about how that looks in the file, you can just &lt;code&gt;cat&lt;/code&gt; it out. You won&amp;rsquo;t see the plaintext password, it&amp;rsquo;s been hashed into gooblygook.&lt;/p&gt;
&lt;p&gt;If you want to add more users, go ahead; it&amp;rsquo;s the same command without the &lt;code&gt;-c&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo htpasswd /etc/nginx/.htpasswd ian
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we need to tell nginx to use this. We need to go back to the same spot in the &lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt; where we added the proxy pass statement. Just &lt;em&gt;above&lt;/em&gt; the proxy statement, add:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;auth_basic &amp;#34;Protected app&amp;#34;;auth_basic_user_file /etc/nginx/.htpasswd;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;ldquo;Protected app&amp;rdquo; is the explanation that should pop up in the modal, and the other directive just tells nginx where to look for the credentials.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-6.09.17-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-6.09.17-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m pretty sure nginx processes these in order, so put the auth_basic directives before the proxy_pass.&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s saved, we&amp;rsquo;ll check the configuration and restart nginx to load it.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@ct372-authplay:~$ sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulian@ct372-authplay:~$ sudo service nginx restart
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we go back to the page, it should pop up and ask for the credentials. If you input your credentials it will direct you to the &amp;ldquo;hello world&amp;rdquo; message from our app.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-6.15.55-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-6.15.55-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="accessing-the-user-in-node"&gt;Accessing the user in node&lt;/h3&gt;
&lt;p&gt;That&amp;rsquo;s all great, but how do we access the authenticated user in our app so we know what content to serve? Nginx knows the username, but our node app does not. To fix that, nginx needs to put it in the header passed to the app. To do this, we need to edit the nginx conf file again to add:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy_set_header X-Username $remote_user;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This takes the user name (in remote_user) and inserts it to the request header.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-7.39.41-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-7.39.41-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After making this change, we need to restart nginx to pick up the config change again - &lt;code&gt;sudo service nginx restart&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Back in our node app, we need to recover the username from the request header.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#39;/&amp;#39;, (req, res) =&amp;gt; { const username = req.get(&amp;#39;X-Username&amp;#39;); res.send(&amp;#39;Hello &amp;#39;+username);});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-7.51.33-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-02-17-at-7.51.33-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In the example above I&amp;rsquo;ve extracted the username in the route - often in my apps I do that in middleware and use it to set some request variables with allowed roles and so on.&lt;/p&gt;
&lt;h3 id="limitations"&gt;Limitations&lt;/h3&gt;
&lt;p&gt;This is not a sophisticated system, here are some shortcomings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The most dangerous thing (although I guess this applies to any auth) is that if you&amp;rsquo;re not securing the web traffic with SSL, the password is transmitted in plaintext across the internet.&lt;/li&gt;
&lt;li&gt;There&amp;rsquo;s no simple way to logout or change the user.&lt;/li&gt;
&lt;li&gt;I entered wrong credentials about twenty times as fast as I could and it never stopped me trying, so a brute force is possible. There are ways of addressing this that I haven&amp;rsquo;t covered here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All in all, this is a handy tool that doesn&amp;rsquo;t require a lot of libraries or setup. It is very simple and doesn&amp;rsquo;t provide any fancy functionality like password resets, but sometimes it&amp;rsquo;s all you need.&lt;/p&gt;
&lt;h4 id="links"&gt;Links&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/"&gt;NGINX basic auth&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Beginning Node App Security</title><link>https://blog.iankulin.com/beginning-node-app-security/</link><pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/beginning-node-app-security/</guid><description>&lt;p&gt;Since I&amp;rsquo;m using Tailscale to painlessly manage all my networking on the homeserver here and my remotes, I&amp;rsquo;ve had the luxury of being a bit casual about the security of my internal apps and self hosted dev tools. I&amp;rsquo;m currently iterating on a web app that requires public access, and is therefore up on a VPS and exposed to all the evils of the open internet.&lt;/p&gt;
&lt;p&gt;I am in no way a security expert, but here&amp;rsquo;s a few of the (reasonably simple) steps I&amp;rsquo;ve taken to secure my node app.&lt;/p&gt;
&lt;h3 id="put-it-behind-nginx"&gt;Put it behind Nginx&lt;/h3&gt;
&lt;p&gt;I could just change the port number of the Node app to listen to port 80 and connect it directly to the world, but Nginx is battle hardened for outward facing tasks so that seems safer. Additionally, it opens up a lot of functionality such as putting my app on a subdomain and some other security options we&amp;rsquo;ll come to. Putting Nginx in front of your app like this is called &amp;lsquo;reverse proxying&amp;rsquo;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;	server_name sub.example.com;	location / { 		proxy_pass http://localhost:3000;			proxy_http_version 1.1;		proxy_set_header Upgrade $http_upgrade;		proxy_set_header Connection &amp;#39;upgrade&amp;#39;;		proxy_set_header Host $host;		proxy_cache_bypass $http_upgrade;	}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="basic-auth"&gt;Basic Auth&lt;/h3&gt;
&lt;p&gt;One of the Nginx options is the ability to turn on &amp;lsquo;&lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/"&gt;basic auth&lt;/a&gt;&amp;rsquo;. This can be enabled for a route, subdomain, or a whole domain. It forces a user to authenticate before being able to access resources from that area. It&amp;rsquo;s basic in the sense that the password is manually set on the server in a file that the nginx conf points to. Ideally your app will have comprehensive auth built in, but (especially when you are still developing it) basic auth is a quick and easy way to prevent all of the internet from accessing your app.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;	server_name sub.example.com;	location / {		auth_basic &amp;#34;Secure app&amp;#34;;	 auth_basic_user_file /etc/nginx/.htpasswd;	}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="https"&gt;HTTPS&lt;/h3&gt;
&lt;p&gt;Enforcing SSL connections is much easier than it used to be (thank you L&lt;a href="https://blog.iankulin.com/certbot-lets-encrypt-are-great/"&gt;et&amp;rsquo;s Encrypt and Certbot&lt;/a&gt;) and it keeps all the data being sent between your app and the user&amp;rsquo;s browser encrypted - including the username and password you are using for your auth.&lt;/p&gt;
&lt;h3 id="logging"&gt;Logging&lt;/h3&gt;
&lt;p&gt;Ensuring that logs are turned on means that you&amp;rsquo;ve got some chance of detecting problems and possible attacks. In fact, you&amp;rsquo;ll probably be aghast at the number of bots that start accessing your server as soon as it&amp;rsquo;s live. For the most part, they are probing for the existence of known vulnerabilities in well known packages - may of the php. When I look up the IP addresses for these, they almost always come from China or Eastern Europe.&lt;/p&gt;
&lt;p&gt;Note that logging does involve some future maintenance. Logs need rotated and deleted or we&amp;rsquo;ll soon be running out of disk space.&lt;/p&gt;
&lt;h3 id="fail2ban"&gt;Fail2ban&lt;/h3&gt;
&lt;p&gt;Manually checking the logs is not effective, we need to automate this a bit. The things I&amp;rsquo;m looking for in my system is brute force attempts at breaking the basic auth, and the same with SSH.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/fail2ban/fail2ban"&gt;Fail2Ban&lt;/a&gt; can automate this (and many other things, but I&amp;rsquo;m just using these two) by scanning the logs for failed attempts. There are various settings to determine the thresholds - say check for 5 failed attempts in 10 minutes then ban the IP address for 30 minutes. Once the threshold is met, Fail2Ban updates iptables (the internal firewall) to block them.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@novel-ironic:~$ sudo fail2ban-client status sshdStatus for the jail: sshd|- Filter| |- Currently failed:	1| |- Total failed:	2960| `- File list:	/var/log/auth.log`- Actions |- Currently banned:	6 |- Total banned:	483 `- Banned IP list:	218.92.0.27 61.177.172.136 61.177.172.140 218.92.0.113 218.92.0.31 218.92.0.76ian@novel-ironic:~$ sudo fail2ban-client status nginx-http-authStatus for the jail: nginx-http-auth|- Filter| |- Currently failed:	1| |- Total failed:	6| `- File list:	/var/log/nginx/error.log`- Actions |- Currently banned:	0 |- Total banned:	1 `- Banned IP list:	ian@novel-ironic:~$ 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the output above, you can see there are 6 ip addresses currently blocked for trying to crack SSH, and there&amp;rsquo;s been 483 banned in the couple of days since I turned it on - so this is a very common attack vector. The basic auth one just has a single ban (from when I tested it). I&amp;rsquo;m not sure why I like looking at the list of bans so much, but I do!&lt;/p&gt;
&lt;h3 id="cloud-firewall"&gt;Cloud Firewall&lt;/h3&gt;
&lt;p&gt;Many VPS providers will have a cloud firewall (although they may call it something else). We can use this to lock down all the ports we are not using to massively reduce the attack surface for this machine. One of the very appealing things about this firewall which is external to the VPS is that it&amp;rsquo;s accessed via the VPS provider web interface - so it&amp;rsquo;s not possible to lock yourself out if you make a mistake - as opposed to when you&amp;rsquo;re SSHd in and fiddling with iptables.&lt;/p&gt;
&lt;p&gt;Since this VPS is just running web apps, I just have ports 80, 443 and 22 open.&lt;/p&gt;
&lt;h3 id="no-root-login"&gt;No root login&lt;/h3&gt;
&lt;p&gt;By default, Ubuntu does not allow root login by password, but once I&amp;rsquo;ve added a new user and added them to the sudo group, I turn it off entirely. Most of those SSH attempts that failed would have been trying common user names including root, so may as well take it out of the possibilities.&lt;/p&gt;
&lt;h3 id="ssh-keys"&gt;SSH keys&lt;/h3&gt;
&lt;p&gt;Apart from being more convenient, well managed SSH keys are much safer than using passwords. So my new user copies up their keys and I set that user to no login with password as well.&lt;/p&gt;
&lt;h3 id="updates"&gt;Updates&lt;/h3&gt;
&lt;p&gt;One of the wonderful things about open source and the modern web, is that as new vulnerabilities are being discovered, they are being patched. We only get them if we run updates though. It&amp;rsquo;s possible (and recommended) to use &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-keep-ubuntu-20-04-servers-updated"&gt;automatic updates&lt;/a&gt;, but I have a weekend ansible routine to do them that I like to look at the output of to check everything&amp;rsquo;s healthy.&lt;/p&gt;
&lt;h3 id="monitoring"&gt;Monitoring&lt;/h3&gt;
&lt;p&gt;I use a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;very simple monitoring system&lt;/a&gt; for all the VM&amp;rsquo;s and containers in my Tailnet - just checking the root disk space and available memory. This is exposed as an http endpoint, then checked by &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;. That&amp;rsquo;s better than nothing, but for a production system not really enough. This is one of the areas I&amp;rsquo;ll revisit in the future.&lt;/p&gt;</description></item><item><title>User Sessions &amp; Cookies in Node</title><link>https://blog.iankulin.com/user-sessions-cookies-in-node/</link><pubDate>Fri, 09 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/user-sessions-cookies-in-node/</guid><description>&lt;p&gt;When you are learning app development, you can create all sorts of apps that work for you, but for any serious app, it&amp;rsquo;s going to need to authenticate users and persist sessions across visits. So much so, that as a professional developer, you&amp;rsquo;ll probably build that out first - it becomes a sort of boiler plate you always drop in.&lt;/p&gt;
&lt;p&gt;In this post, focusing on the server side, using node, express, and particularly express-session, I&amp;rsquo;ll try and build up from nothing to a reasonable usable user login system explaining the increasing complexity and reasons for it. To follow along you&amp;rsquo;ll need basic familiarity with node and express.&lt;/p&gt;
&lt;h3 id="the-problem-were-addressing"&gt;The problem we&amp;rsquo;re addressing&lt;/h3&gt;
&lt;p&gt;For most web applications, we need to persist state &lt;em&gt;per user&lt;/em&gt;. For example, if you go to a drawing app and start a drawing, you want it to be there when you come back to the app. Additionally, you don&amp;rsquo;t want to come back to someone else&amp;rsquo;s half-drawn app, or, have them drawing over your picture. What we really want is something like this:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/20240126-sessions-1.drawio-1.png" width="283" alt=""&gt;
&lt;p&gt;User 1 sees their picture of the star, and User 2 sees their picture of a heart.&lt;/p&gt;
&lt;p&gt;Since HTTP is &lt;a href="https://en.wikipedia.org/wiki/Stateless_protocol"&gt;stateless&lt;/a&gt; - a request to &lt;code&gt;/picture&lt;/code&gt; from one user is indistinguishable from another users request to &lt;code&gt;/picture&lt;/code&gt; - so we need to add something to allow the server to distinguish between the two. The something would be a bit of &lt;em&gt;state&lt;/em&gt; that the server passes back to the user, then the user sends it in with their actions so the server can identify them.&lt;/p&gt;
&lt;p&gt;There are a few of ways to do this. The first (which is not the subject of this post) is to store that in the URL. For example, when a user in the above app requests to create a picture, we could generate a &lt;a href="https://www.techtarget.com/searchwindowsserver/definition/GUID-global-unique-identifier"&gt;GUID&lt;/a&gt; for them, then redirect them to a URL based on that - perhaps /picture/cbe34f. Thereafter, all their requests could include that GUID. This can be a useful way of managing session state and has some affordances that the other method does not, but it&amp;rsquo;s not the most common.&lt;/p&gt;
&lt;p&gt;Another system that was extensively used in the early days of the web was to embed a hidden input with the GUID in the HTML returned to the user. When the user submitted the form later, the GUID was available&lt;/p&gt;
&lt;p&gt;The most common method is for the server to create a bit of state (our GUID), send it to the user and have the user&amp;rsquo;s browser store it, and return it with every request. You will know this bit of state as a &lt;em&gt;cookie&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="code-example"&gt;Code example&lt;/h3&gt;
&lt;p&gt;Enough theory, lets look at some code. If you google &amp;lsquo;simple node express session&amp;rsquo; you&amp;rsquo;ll find this, or something almost identical. Instead of the state we&amp;rsquo;re trying to persist being a picture of a heart or a star, we&amp;rsquo;re trying to remember how many times each user has visited our web site. Note these are separate counts for each user - a new visitor will start at zero, then count up each time they come back or refresh their page.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re using a couple of packages, so to run this example, you&amp;rsquo;ll first need to install them with &lt;code&gt;npm i express express-session&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express-session&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;your-secret-key&amp;#39;&lt;/span&gt;, 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Access session data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Views: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Server is running on port 3000&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we load this page from &lt;code&gt;http://localhost:3000&lt;/code&gt; it says &amp;ldquo;Views: 1&amp;rdquo;. Reloading the page it will say &amp;ldquo;Views: 2&amp;rdquo;. If I open a different browser and visit the server, we&amp;rsquo;ll be back to &amp;ldquo;Views: 1&amp;rdquo;. So what&amp;rsquo;s the magic that&amp;rsquo;s happening here?&lt;/p&gt;
&lt;p&gt;On the first request from a browser it hasn&amp;rsquo;t seen before, express-session is generating a session ID, and sending that back to the browser (along with &amp;lsquo;Views: 1&amp;rsquo;) and asking the browser to store it as a cookie. Thereafter, every request to this website &lt;code&gt;http://localhost:3000&lt;/code&gt; will include the cookie containing the session ID. The number of views is being stored on the server in memory. Express-session does the work of looking up the number of views for a particular session ID returned in a cookie so we have the luxury of just grabbing that from &lt;code&gt;rec.session.views&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Thanks to the magic of browser development tools, we can have a look in the request header from the browser and see the cookie with it&amp;rsquo;s session ID contents:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-9.42.43-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also possible to go and find the cookie your browser has stored. Again, this is easiest in the developer tools - look under &amp;lsquo;Storage&amp;rsquo;:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-10.19.16-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-10.19.16-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Currently, we&amp;rsquo;re only storing the links between session_ids and the number of views in memory in the server, so if the server restarts, we&amp;rsquo;ll lose them, and everyone will go back to &amp;lsquo;Views: 1&amp;rsquo;.&lt;/p&gt;
&lt;h3 id="persisting-across-server-restarts"&gt;Persisting across server restarts&lt;/h3&gt;
&lt;p&gt;If we want our view counts to not be reset to zero each time the server restarts, we&amp;rsquo;ll need to save them somehow. express-session doesn&amp;rsquo;t let us access its internal array of sessions, but it does provide a mechanism for that access with the concept of &lt;em&gt;stores&lt;/em&gt;. Usually we&amp;rsquo;d keep the sessions in a database, but as files is a simpler solution for a blog post. There is a package called &lt;code&gt;session-file-store&lt;/code&gt; that will slot into &lt;code&gt;expression-session&lt;/code&gt; that will just use the file system to persist the session-view count key pairs for us. After installing it with &lt;code&gt;npm i session-file-store&lt;/code&gt;, we just declare a const for it, and pass it in when initialising the session middleware.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;)(&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;your-secret-key&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// This should be a secret, used to sign the session ID cookie
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;store&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;session_cookie&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Access session data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Views: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Server is running on port 3000&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now view counts for each browser are persisted even when the server is re-started. If we look inside one of the files we should see a view_count field:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-12.22.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-12.22.45-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The name of each file is the session id, but these don&amp;rsquo;t match the session id&amp;rsquo;s you see in the browser cookies - presumably because they&amp;rsquo;ve been encrypted by the secret we used when establishing the express-session.&lt;/p&gt;
&lt;p&gt;So this is a better experience - our users see their own view counts increasing on each refresh, and the view counts don&amp;rsquo;t get lost when you take the server down for maintenance. But what happens if the user wants to pick up their phone and check their view count. It&amp;rsquo;s a different browser without the cookie containing the session id, so they&amp;rsquo;ll get &amp;lsquo;View: 1&amp;rsquo; again.&lt;/p&gt;
&lt;p&gt;Or, what if your partner sits down at your laptop to check &lt;em&gt;their&lt;/em&gt; view count, expecting it to be &amp;lsquo;1&amp;rsquo; because they&amp;rsquo;ve never visited this page, and they see your view count instead?&lt;/p&gt;
&lt;h3 id="users"&gt;Users&lt;/h3&gt;
&lt;p&gt;Our current system is really based around browser instances, but we want it to be about people. So a better system, and one that you&amp;rsquo;ll be used to is the concept of &lt;em&gt;logging in&lt;/em&gt; as a &lt;em&gt;user&lt;/em&gt;. This can solve both the problems we described above - users will be able to log in from any browser and see only their own data.&lt;/p&gt;
&lt;p&gt;This is going to take things up a substantial step in complexity because now instead of two bits of data (the cookie with the session_id, and the session store linking the session_id and the view count) there is going to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The cookie with the &lt;em&gt;session_id&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The session store linking the &lt;em&gt;session_id&lt;/em&gt; and the &lt;em&gt;user&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;A new store we will have to manage that links the &lt;em&gt;user&lt;/em&gt; and the &lt;em&gt;view_count&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, we&amp;rsquo;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some mechanism to create a user&lt;/li&gt;
&lt;li&gt;Some mechanism to log in&lt;/li&gt;
&lt;li&gt;And log out&lt;/li&gt;
&lt;li&gt;If there&amp;rsquo;s no user logged in, redirect to the log in page&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="log-out"&gt;Log out&lt;/h4&gt;
&lt;p&gt;Logging the user out is reasonably simple - we just delete the session information. In a real app we&amp;rsquo;d have a &amp;rsquo;log out&amp;rsquo; button of some sort, but for simplicity here, I&amp;rsquo;m just going to add a &amp;rsquo;log out&amp;rsquo; route.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/logout&amp;#34;, (req, res) =&amp;gt; { req.session.destroy((err) =&amp;gt; { if (err) { return console.log(err); } res.clearCookie(&amp;#34;session_cookie&amp;#34;); res.redirect(&amp;#34;/&amp;#34;); });});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;clearCookie()&lt;/code&gt; tells the browser to delete the cookie - which just saves us from console messages later when the browser sends it, but express-session can&amp;rsquo;t find the matching session.&lt;/p&gt;
&lt;p&gt;If you add this to the code from earlier and run it, view will count up as before, but when you visit the &lt;code&gt;/logout&lt;/code&gt; endpoint views will be set back to 1.&lt;/p&gt;
&lt;h4 id="users--view-counts"&gt;Users &amp;amp; view counts&lt;/h4&gt;
&lt;p&gt;In a real application we&amp;rsquo;d be keeping this in a database, but again for simplicity of this demo, I&amp;rsquo;m just going to keep an array of objects and persist them as a text file. The array will be user_views, and somewhere at the top of our code we&amp;rsquo;ll add:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let user_views = [];// if the user_views.json file exists, read it and parse it to user_views arrayif (fs.existsSync(&amp;#34;./user_views.json&amp;#34;)) { user_views = JSON.parse(fs.readFileSync(&amp;#34;./user_views.json&amp;#34;));}
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="creating-a-user"&gt;Creating a user&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ll use a route parameter to create a new user (like &lt;code&gt;/create/jane&lt;/code&gt; or /&lt;code&gt;create/robert&lt;/code&gt; where the second part is accessible in &lt;code&gt;req.params&lt;/code&gt;). That user will be set as the session user, then we&amp;rsquo;ll add it to our array and save the array to disk.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user&amp;#34;, (req, res) =&amp;gt; { const user = req.params.user; const views = 0; req.session.user = user; user_views.push({ user, views }); fs.writeFile(&amp;#34;./user_views.json&amp;#34;, JSON.stringify(user_views), (err) =&amp;gt; { if (err) { res.send(err); return; } }); res.send(`User ${user} created &amp;amp; logged in`);});
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="logging-a-user-in"&gt;Logging a user in&lt;/h4&gt;
&lt;p&gt;If the user is in our array, we&amp;rsquo;ll set the session user to it, otherwise redirect to the create endpoint:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/login/:user&amp;#34;, (req, res) =&amp;gt; { // see if this user is in the user_views array if (user_views.find((u) =&amp;gt; u.user === req.params.user)) { req.session.user = req.params.user; res.send(`User ${req.params.user} logged in`); return; } else { res.redirect(`/create/${req.params.user}`); }});
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="showing-the-view-count"&gt;Showing the view count&lt;/h4&gt;
&lt;p&gt;The default route that shows our view count has a few jobs to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check we&amp;rsquo;re logged in, if not, tell the user to log in&lt;/li&gt;
&lt;li&gt;If we are logged in, fetch the view count for that user&lt;/li&gt;
&lt;li&gt;Increment the view count&lt;/li&gt;
&lt;li&gt;Show it to the user&lt;/li&gt;
&lt;li&gt;Re-save the user_views data since we&amp;rsquo;ve mutated it&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; { if (req.session.user) { const user_view = user_views.find((u) =&amp;gt; u.user === req.session.user); user_view.views++; res.send(`User &amp;#34;${req.session.user}&amp;#34; has ${user_view.views} views`); writeUserViewFile(); } else { res.send(&amp;#34;Please log in&amp;#34;); }});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So that&amp;rsquo;s our system for associating the view counts with a user done. Here&amp;rsquo;s the whole thing where we&amp;rsquo;re up to (I&amp;rsquo;ve done a little refactoring).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;)(&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;./user_views.json&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;session_cookie&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// if the user_views.json file exists, read it and parse it to user_views array
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;existsSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;parse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;readFileSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;writeFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;), (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;find&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;) =&amp;gt; &lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;your-secret-keyz&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// This should be a secret, used to sign the session ID cookie
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;store&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; has &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; views`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Please log in&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;destroy&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;clearCookie&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/create/:user&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;push&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; created &amp;amp; logged in`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login/:user&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// see if this user is in the user_views array
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; logged in`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`/create/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Server is running on port 3000&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="authentication"&gt;Authentication&lt;/h3&gt;
&lt;p&gt;It won&amp;rsquo;t have escaped your attention that our view counting app isn&amp;rsquo;t at all secure. At the moment, any one can log in as &amp;lsquo;jane&amp;rsquo; and see her view count. The common way to address this is to also require a password.&lt;/p&gt;
&lt;p&gt;I will eventually have to start serving some actual HTML, but for this next step I&amp;rsquo;ll stick to just using the routes. So our user interface will be this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Logging in &lt;code&gt;/login/&amp;lt;user&amp;gt;/&amp;lt;password&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Check the user exists, and that this is the correct password&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Creating a new user &lt;code&gt;/create/&amp;lt;user&amp;gt;/&amp;lt;password&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Save the new user and password to the file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;rsquo;ll do create first:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user/:password&amp;#34;, (req, res) =&amp;gt; { const user = req.params.user; const password = req.params.password; const views = 0; req.session.user = user; user_views.push({ user, password, views }); writeUserViewFile(); res.send(`User ${user} created &amp;amp; logged in`);});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We should probably first check there&amp;rsquo;s not an existing user with the same name to avoid having two users and the second user never being able to log in.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user/:password&amp;#34;, (req, res) =&amp;gt; { if (findUser(req.params.user)) { res.send(`User ${req.params.user} already exists`); return; } const user = req.params.user; const password = req.params.password; const views = 0; req.session.user = user; user_views.push({ user, password, views }); writeUserViewFile(); res.send(`User ${user} created &amp;amp; logged in`);});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and logging in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/login/:user/:password&amp;#34;, (req, res) =&amp;gt; { const user = findUser(req.params.user); if (user &amp;amp;&amp;amp; user.password === req.params.password) { req.session.user = req.params.user; res.send(`User ${req.params.user} logged in`); return; } else { res.send(`Incorrect username or password`); }});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Although this is an improvement, clearly, it would a stretch to call this improved &lt;em&gt;security&lt;/em&gt;. Here&amp;rsquo;s a few things that are jumping out at me, and I&amp;rsquo;m sure there&amp;rsquo;s some being missed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We&amp;rsquo;re transmitting plaintext passwords over http&lt;/li&gt;
&lt;li&gt;We have plaintext passwords in a URL - any intermediate networking that&amp;rsquo;s recording access logs will the logging our passwords&lt;/li&gt;
&lt;li&gt;We&amp;rsquo;re storing plaintext passwords on our server in the &lt;code&gt;user_views.json&lt;/code&gt; file. So when our server is breached, our users&amp;rsquo; passwords will get sold on the dark web and they&amp;rsquo;ll be hacked if they&amp;rsquo;ve reused name/password combos.&lt;/li&gt;
&lt;li&gt;Passwords are limited to characters that reliably work in URLs&lt;/li&gt;
&lt;li&gt;Our passwords and user names may be open to injection attacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So it seems like there is four jobs to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Encrypt the passwords&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t use URLs for passing this data&lt;/li&gt;
&lt;li&gt;Sanitise our inputs&lt;/li&gt;
&lt;li&gt;Enforce HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="passwords"&gt;Passwords&lt;/h4&gt;
&lt;p&gt;Although I described this as &amp;rsquo;encrypting&amp;rsquo; that&amp;rsquo;s not exactly what we&amp;rsquo;re going to do. The current state of the art for this is to &lt;em&gt;hash&lt;/em&gt; and &lt;em&gt;salt&lt;/em&gt; passwords before storing them.&lt;/p&gt;
&lt;p&gt;hash - turn the password into some gooblygook such that that the hashed version always comes out the same if you put the same password into it. Preferably the hash is always the same length regardless of the length of the input password.&lt;/p&gt;
&lt;p&gt;salt - mix some random characters in to the the hashed password so that the combination of hashing and salting the password comes out different every time, even though you are putting in the same password as input to the process.&lt;/p&gt;
&lt;p&gt;How this is going to work is that once we get the users password, we&amp;rsquo;ll hash and salt it, then save the result of that in our array (and eventually file). If someone has access to that file, it&amp;rsquo;s practically impossible for them to reverse engineer the password - even if they have a known password somewhere else in the file to work from. Then when a user attempts to log in, we&amp;rsquo;ll take the password they gave us and hash it, and we&amp;rsquo;ll &amp;lsquo;un-salt&amp;rsquo; the password from the file and compare those two hashed versions of the passwords. If they are the same, then we know the passwords were also the same, and we can log this user in.&lt;/p&gt;
&lt;p&gt;This is a fun area of computational science, so it&amp;rsquo;s somewhat tempting to write our own hash and salting functions. This is widely regarded as a Bad Idea. As long as you don&amp;rsquo;t run into supply chain problems, a proper library, written by cryptographic experts, subject to public scrutiny, that gets updates when vulnerabilities are discovered is always going to be more secure. Almost universally, the answer for JS developers is &lt;a href="https://www.npmjs.com/package/bcrypt"&gt;bcrypt&lt;/a&gt;. We&amp;rsquo;ll install that with &lt;code&gt;npm i bcrypt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s our create and login endpoints using bcrypt with the changes highlighted.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user/:password&amp;#34;, (req, res) =&amp;gt; { if (findUser(req.params.user)) { res.send(`User ${req.params.user} already exists`); return; } const user = req.params.user; const hash = bcrypt.hashSync(req.params.password, saltRounds); const views = 0; req.session.user = user;  user_views.push({ user, hash, views });  writeUserViewFile();  res.send(`User ${user} created &amp;amp; logged in`); });});app.get(&amp;#34;/login/:user/:password&amp;#34;, (req, res) =&amp;gt; { const user = findUser(req.params.user); if (user &amp;amp;&amp;amp; bcrypt.compareSync(req.params.password, user.hash)) { req.session.user = req.params.user; res.send(`User ${req.params.user} logged in`); return; } else { res.send(`Incorrect username or password`); }});
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="forms"&gt;Forms&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-27-at-1.44.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-27-at-1.44.22-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To move the passwords out of the URLs, we&amp;rsquo;ll present a simple forms to the user for creating and logging in. The log in page is the basic user name / password form you&amp;rsquo;ve built before. This is served from the app.get(&amp;quot;/login&amp;quot;) route.&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s the endpoint it posts to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#34;/login&amp;#34;, (req, res) =&amp;gt; { const user = findUser(req.body.username); if (user &amp;amp;&amp;amp; bcrypt.compareSync(req.body.password, user.hash)) { req.session.user = req.body.username; req.session.save((err) =&amp;gt; { if (err) { res.send(&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;); } else { res.redirect(&amp;#34;/&amp;#34;); } }); } else { res.send(`Incorrect username or password, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`); }});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Normally express-session will deal with saving the session without us having to worry about, but I decided that a successful login should be followed to a re-direct to the view count page.&lt;/p&gt;
&lt;p&gt;Since express-session normally does its saving at the end of a http response, and if we&amp;rsquo;re redirecting, that response hasn&amp;rsquo;t happened. There&amp;rsquo;s a bit more about that &lt;a href="https://expressjs.com/en/resources/middleware/session.html"&gt;here&lt;/a&gt; (search for session save).&lt;/p&gt;
&lt;p&gt;Apart from that, the changes are really just grabbing the user name and passwords from the form post body instead of the URL.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/register&lt;/code&gt; form is the same as the log in, but with a second password field and some client side scripting to check the two passwords are the same. Processing the new user is very similar to the previous &lt;code&gt;create&lt;/code&gt; route.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#34;/register&amp;#34;, (req, res) =&amp;gt; { if (findUser(req.body.username)) { res.send(`User ${req.body.username} already exists`); return; } const user = req.body.username; const hash = bcrypt.hashSync(req.body.password, saltRounds); const views = 0; req.session.user = user; user_views.push({ user, hash, views }); writeUserViewFile(); req.session.save((err) =&amp;gt; { if (err) { res.send(&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;); } else { res.redirect(&amp;#34;/&amp;#34;); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="sanitising-inputs"&gt;Sanitising inputs&lt;/h4&gt;
&lt;p&gt;Cautious developers do not trust any input from users. There are numerous libraries to deal with cleaning it up which I&amp;rsquo;d recommend you consider, but for our case here let&amp;rsquo;s think through what the possibilities are:&lt;/p&gt;
&lt;p&gt;password - the password is going to be hashed and salted before it is stored, and never used to build HTML. The only risk I can think of would be if a very long password might cause some sort of trouble - so we can just truncate it. Note we don&amp;rsquo;t even need to tell the user - if we truncate it when they register, and when they log in, they&amp;rsquo;ll still match.&lt;/p&gt;
&lt;p&gt;user name - is stored in a json file, and is output to the user. In the future that output is likely to be HTML. &lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s our &lt;code&gt;users_view.json&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[ { &amp;#34;user&amp;#34;: &amp;#34;user1&amp;#34;, &amp;#34;hash&amp;#34;: &amp;#34;$2b$10$8Zf9LZWH78mWnSKjxKQxXe9TlPoqe7L3SOABcPHIUQ5Pq3jIbVQVm&amp;#34;, &amp;#34;views&amp;#34;: 16 }, { &amp;#34;user&amp;#34;: &amp;#34;user2&amp;#34;, &amp;#34;hash&amp;#34;: &amp;#34;$2b$10$f/QAQ7we6Hh/hTx35LjfGeYtCY8aRG3ZqbJZqhEZRDXUqxkKCgPhq&amp;#34;, &amp;#34;views&amp;#34;: 14 }, { &amp;#34;user&amp;#34;: &amp;#34;user3&amp;#34;, &amp;#34;hash&amp;#34;: &amp;#34;$2b$10$KBZe6orYpjG3JeIGhnLDCuyYQXxfVZYBjovGf3XIdU8rr6kXlNrLC&amp;#34;, &amp;#34;views&amp;#34;: 1 }]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The obvious attack here would be injection. For example a hacker might register with the user name:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;user4&amp;quot;, &amp;quot;role&amp;quot;: &amp;quot;admin&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a smart try at privileged escalation. Even if we had an &amp;lsquo;admin&amp;rsquo; role, it wouldn&amp;rsquo;t actually work though since any double quotes will be escaped by JSON.stringify(), but to be cautious, we can eliminate the possibility by just deleting any double quotes out.&lt;/p&gt;
&lt;p&gt;Another injection possibility would be with HTML. Perhaps we will display the logged in user later inside a &lt;div&gt;, something like:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;div&amp;gt;User: user1&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Our hacker might try registering this as their user name as:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;user1&amp;lt;/div&amp;gt;&amp;lt;script&amp;gt;alert('you've been hacked')&amp;lt;/script&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not great to allow anyone on the internet to run code in our visitors&amp;rsquo; browsers.&lt;/p&gt;
&lt;p&gt;You should really use a library designed by someone who knows what they are doing, but I just wanted to do enough here to prompt you to think about. For that demo purpose, I&amp;rsquo;m going to replace all &amp;quot; &amp;lt; and &amp;gt; with _&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// sanitise a string by replacing all &amp;#34; &amp;lt; and &amp;gt; with _ (underscore) // and truncating it at 20 charactersfunction sanitise(str) { return str.replace(/[&amp;#34;&amp;lt;&amp;gt;]/g, &amp;#34;_&amp;#34;).slice(0, 20);}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is all a bit doge. If we are going to have rules like this (and the other rules we should have about min lengths) we should be implementing them in the browser and on the backend, and there should be helpful prompting to the users to enable them to understand and correct their inputs. As I mentioned earlier, this is just to get you to think about it.&lt;/p&gt;
&lt;h4 id="https"&gt;HTTPS&lt;/h4&gt;
&lt;p&gt;In the list of four security improvements we wanted to make, the last one was to enforce HTTPS. There&amp;rsquo;s two places to do this. One is that when we initialise our session at the top of the app, we can tell it we want &amp;lsquo;secure&amp;rsquo; cookies. This setting means that cookies will not be sent over plain HTTP, but only the end-to-end encrypted HTTPS. Currently our cookies contain a user name:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{ &amp;#34;cookie&amp;#34;: { &amp;#34;originalMaxAge&amp;#34;: null, &amp;#34;expires&amp;#34;: null, &amp;#34;httpOnly&amp;#34;: true, &amp;#34;path&amp;#34;: &amp;#34;/&amp;#34; }, &amp;#34;__lastAccess&amp;#34;: 1706315491081, &amp;#34;user&amp;#34;: &amp;#34;user1&amp;#34;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even though a user name isn&amp;rsquo;t a lot of information, it could still be critical. If there was rumors about your company being acquired and a hacker leaked that j_bezos had been looking at your view counts, it could have implications. To turn on secure cookies:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.use( session({ secret: sessionSecret, resave: false, saveUninitialized: true, store: new FileStore(), name: cookie_name, cookie: { secure: true, httpOnly: true, }, }));
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that your infrastructure needs to support HTTPS for this to be useful - if the cookies are not sent, this particular app is rendered useless.&lt;/p&gt;
&lt;p&gt;But we want to enforce HTTPS anyway, because a bigger problem is that if we don&amp;rsquo;t someone could intercept the login form data and collect plaintext usernames and passwords. &lt;/p&gt;
&lt;p&gt;I generally do this by running all web services behind NGINX as a proxy. Then the NGINX configs can be set to redirect all HTTP requests to HTTPS, and valid requests can be passed off to your app. Here&amp;rsquo;s the sort of thing you might have in a config.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server { listen 80; server_name viewcount.example.com; return 301 https://$host$request_uri;}server { listen 443 ssl; server_name viewcount.example.com; # SSL certificate configuration ssl_certificate /path/to/your/certificate.crt; ssl_certificate_key /path/to/your/private-key.key; # Additional SSL configuration, such as preferred protocols and ciphers, can be added here location / { # Your application configuration goes here # Proxy pass to your Node.js or other application server # Example for proxying to a Node.js server running on localhost:3000 proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection &amp;#39;upgrade&amp;#39;; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; }}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There are other security actions that can be taken at the NGINX level - things like rate limiting, blocking particular IP ranges, and access logging - all of which can be handy for protecting your endpoint from bad actors.&lt;/p&gt;
&lt;h4 id="other-security"&gt;Other security&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;d normally have the cookie secret in a .env file that was being .gitignored. A good hacker hobby is to scan public code repos for API keys and other secrets.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/helmetjs/helmet"&gt;Helmet.js&lt;/a&gt; is a sort of magic bullet for enforcing some security around request headers.&lt;/li&gt;
&lt;li&gt;Probably a stack of other things - ask your senior dev.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s our app with the changes we&amp;rsquo;ve discussed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;)(&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bcrypt&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;bcrypt&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;saltRounds&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;./user_views.json&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;session_cookie&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;urlencoded&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;extended&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt; }));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// if the user_views.json file exists, read it and parse it to user_views array
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;existsSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;parse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;readFileSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;writeFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;), (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;find&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;) =&amp;gt; &lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// sanitise a string by replacing all &amp;#34; &amp;lt; and &amp;gt; with _ (underscore)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// and truncating it at 20 characters
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitise&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;str&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;str&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;replace&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/[&amp;#34;&amp;lt;&amp;gt;]/g&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;_&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;slice&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;your-secret-keyz&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// This should be a secret, used to sign the session ID cookie
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;store&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Error finding user, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;`User &amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; has &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; views, &amp;lt;a href=&amp;#34;/&amp;#34;&amp;gt;reload&amp;lt;/a&amp;gt; or &amp;lt;a href=&amp;#34;/logout&amp;#34;&amp;gt;logout&amp;lt;/a&amp;gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;destroy&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;clearCookie&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/register&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitise&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; already exists`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;hash&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bcrypt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;hashSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;saltRounds&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;push&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;hash&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;username&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitise&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bcrypt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;compareSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;hash&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Incorrect username or password, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;sendFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;__dirname&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/login.html&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/register&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;sendFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;__dirname&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/register.html&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Server is running on port 3000&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Nearly every web app we write is going to need a user auth and session management solution. In this very long post we&amp;rsquo;ve looked at a way to develop that from scratch in Express/Node. In the process our code base went from about 10 lines to 130. Now that it&amp;rsquo;s done however, the only extra code to ensure users are only accessing the routes they should will be a line at the entry point of each route.&lt;/p&gt;
&lt;p&gt;Since building out session management is such a common and onerous task, and one that can have serious consequences if not done correctly, you might be wondering if there&amp;rsquo;s libraries to do some of this, and other, lifting for us. There is, and I plan to look at some in the future.&lt;/p&gt;</description></item><item><title>Web Development Overview</title><link>https://blog.iankulin.com/web-development-overview/</link><pubDate>Mon, 05 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/web-development-overview/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-27-at-1.00.35-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t often just link to someone else&amp;rsquo;s content, but I was really impressed with &lt;a href="https://www.traversymedia.com/"&gt;Brad Traversy&lt;/a&gt;&amp;rsquo;s &amp;ldquo;Web Development In 2024 - A Practical Guide&amp;rdquo; video. Apparently he does these every year - it&amp;rsquo;s just a really comprehensive overview of Web Development pitched at beginners.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/8sXRyHI3bLw?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Fly.io, Uptime Kuma &amp; scraping a status page</title><link>https://blog.iankulin.com/fly-io-uptime-kuma-scraping-a-status-page/</link><pubDate>Fri, 02 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fly-io-uptime-kuma-scraping-a-status-page/</guid><description>&lt;p&gt;&lt;a href="https://dribbble.com/shots/5657880-Fly-io-Logo"&gt;&lt;img src="https://blog.iankulin.com/images/c1fef772e2dca5e1ab8c812f465c95a8.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been aware since I set up &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt; for my monitoring, that having an instance on my local network monitoring my VPS websites wasn&amp;rsquo;t ideal. The main reason being that the flakiest part of my infrastructure is my 4G home internet, so if that goes down I have no website monitoring, and even if I did, the notifications couldn&amp;rsquo;t get out.&lt;/p&gt;
&lt;p&gt;Of course, it would also be a simple matter to run an instance on the VPS that I host the sites on, but that has a similar problem in that if the VPS goes down, so does my monitoring of the VPS. What I really need is a third, independent space to run an instance.&lt;/p&gt;
&lt;h3 id="uptime-robot"&gt;Uptime Robot&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://uptimerobot.com/"&gt;Uptime Robot&lt;/a&gt; is a monitoring service that seems somehow related to Uptime Kuma? They have some of the same terminology and colour schemes - so I&amp;rsquo;m not really sure. Perhaps it&amp;rsquo;s a fork, or perhaps Uptime Kuma was inspired by Robot. Robot does have an API which is a nice addition, since ideally if my monitoring is spread around, I&amp;rsquo;d like to pull it all back into one &amp;lsquo;pane of glass&amp;rsquo; by having my system monitor the remote for how many &amp;lsquo;down&amp;rsquo; sites it&amp;rsquo;s tracking. It also has a number of other extra features such as heartbeat monitoring.&lt;/p&gt;
&lt;p&gt;Uptime Robot is a paid service, but like nearly all VC funded things growing a user base it has a free tier with some restrictions. I like NTFY for my notifications, but on Robot I could only access email notifications. There are iOS and Android apps, but I didn&amp;rsquo;t try them.&lt;/p&gt;
&lt;h3 id="third-space"&gt;Third Space&lt;/h3&gt;
&lt;p&gt;Ideally, I like to run another Uptime Kuma in a VPS on a different provider. I&amp;rsquo;ve heard that &lt;a href="https://www.oracle.com/au/cloud/free/"&gt;Oracle have a free tier&lt;/a&gt; which seems like it would be fine for this application, but a more interesting idea that I&amp;rsquo;ve been thinking of using for other projects is Fly.io.&lt;/p&gt;
&lt;h3 id="flyio"&gt;Fly.io&lt;/h3&gt;
&lt;p&gt;Fly.io own physical servers in colo datacentres around the world on which they offer compute based on &lt;a href="https://www.amazon.science/blog/how-awss-firecracker-virtual-machines-work"&gt;Firecracker VM&amp;rsquo;s&lt;/a&gt;. The cute bit is that you give them a Docker container, and they unpack it into one of these fast baby VM&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;The exact nature of their &amp;lsquo;free tier&amp;rsquo; is hard to figure out from their &lt;a href="https://fly.io/docs/about/pricing/"&gt;pricing page&lt;/a&gt;, but based on &lt;a href="https://community.fly.io/t/fly-io-free-tier-billing/11432"&gt;some answers to questions in their forum&lt;/a&gt;, and &lt;a href="https://jfmadrid.notion.site/Uptime-Kuma-for-Free-on-Fly-io-e5eeead6dfb4425b8403c100ec986191"&gt;blog posts from others who have set up Uptime Kuma&lt;/a&gt; there, it sounds like the deal is that if you use one shared CPU &lt;em&gt;and&lt;/em&gt; keep your storage under 3GB &lt;em&gt;and&lt;/em&gt; the charges for your use add up to less than $5/month - then it&amp;rsquo;s free. I did have to provide credit card details, so if &lt;a href="https://www.youtube.com/watch?v=N6lYcXjd4pg"&gt;I get a $71,393 bill,&lt;/a&gt; I&amp;rsquo;ll come back here and edit this. (&lt;em&gt;edit from the future: eight months later I haven&amp;rsquo;t paid a cent&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;To get Uptime Kuma running on Fly.io, I followed &lt;a href="https://jfmadrid.notion.site/Uptime-Kuma-for-Free-on-Fly-io-e5eeead6dfb4425b8403c100ec986191"&gt;this guide&lt;/a&gt;, but the steps where basically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create an account on Fly.io&lt;/li&gt;
&lt;li&gt;Install the Fly.io command line tools and run a command to &amp;lsquo;create&amp;rsquo; your app&lt;/li&gt;
&lt;li&gt;Create a &amp;lsquo;&lt;a href="https://github.com/lubien/fly-uptime-kuma/blob/main/fly.toml"&gt;fly.toml&lt;/a&gt;&amp;rsquo; file which is a text config file pointing to the docker image and supplying some details such as ports and location&lt;/li&gt;
&lt;li&gt;Use the CLI to set the disk space needed, and &amp;lsquo;deploy&amp;rsquo; the app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was impressive how simple all this was. If the intention of the free tier is to get you to try it, and show you how painless it is to deploy any dockerised app to the edge, then mission accomplished.&lt;/p&gt;
&lt;p&gt;You can check on the status of your app at &lt;a href="https://fly.io/dashboard"&gt;https://fly.io/dashboard&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-6.31.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-6.31.22-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And go to &lt;appname&gt;.fly.dev to see your app. On the free tier, you&amp;rsquo;re on a shared IPV4 address but it is possible to use your own domain if desired - that&amp;rsquo;s one of the things to set up in the .toml file.&lt;/p&gt;
&lt;p&gt;It is remarkable what you can deploy for free in the golden age of venture capital.&lt;/p&gt;
&lt;h3 id="extracting-status"&gt;Extracting Status&lt;/h3&gt;
&lt;p&gt;One of Uptime Kuma&amp;rsquo;s functions is to provide public (ie viewable without being logged in) &amp;lsquo;status&amp;rsquo; pages, and if all the services you&amp;rsquo;ve added to that status group are up, it has. great big heading saying &amp;ldquo;All Systems Operational&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.38.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.38.45-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So my plan to pull this status into my homelab instance of Uptime Kuma was just to add this remote status page as a monitor, and search for the keyword &amp;lsquo;All Systems Operational&amp;rsquo;. If that was found, I&amp;rsquo;d know everything was good. But of course, this is a modern web-app (I think using &lt;a href="https://vuejs.org/"&gt;Vue&lt;/a&gt;), so that text does not exist in the page, it&amp;rsquo;s added to the DOM by some JavaScript after the page is loaded based on some client side processing of (probably) some JSON data it pulls in.&lt;/p&gt;
&lt;p&gt;One option would be to use a web scraping library to write something to access this piece of information. On a page like this, that would involve a headless browser rendering the DOM then exposing it.&lt;/p&gt;
&lt;p&gt;But of course, the Javascript that is building the page we&amp;rsquo;re looking at is getting its data from somewhere, so it&amp;rsquo;s probably easier for us to grab that data directly and process it ourselves. How do we see where the data is from? We use the browser tools to look at the network requests when the page is loaded.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.20.50-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.20.50-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So if you view the status page at &lt;code&gt;&amp;lt;whatever.com&amp;gt;/status/&amp;lt;page_name&amp;gt;&lt;/code&gt;, it loads some data from &lt;code&gt;&amp;lt;whatever.com&amp;gt;/api/status-page/heartbeat/&amp;lt;page_name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The JSON that&amp;rsquo;s returned from this request contains two objects: &lt;code&gt;heartbeatlist&lt;/code&gt;, and &lt;code&gt;uptimelist&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.06.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.06.05-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heartbeatlist&lt;/code&gt; contains the last 50 retrievals for each of the URL&amp;rsquo;s being monitored. Each of these retrievals has a status (1 for up, 0 for down) and the response time. &lt;code&gt;uptimelist&lt;/code&gt; is the fraction of uptime. You can see in the data above that the first URL has a lower percentage of up-time (because I failed it to check my understanding of the status data).&lt;/p&gt;
&lt;p&gt;So I need to write an endpoint that requests this data, then checks the last array element of each of the URLs in the heartbeat list, then spit out some text saying if all the URL&amp;rsquo;s in this status group are available. That&amp;rsquo;s quite doable, I have the skills, but it&amp;rsquo;s probably a two hour job to do properly.&lt;/p&gt;
&lt;p&gt;Since this is an open source project, a better use of that time would be to add this functionality to Uptime Kuma so it would be available to anyone with the same problem. It might be a niche case, but the code to provide this output would be simpler inside the project and much more durable than reverse engineering it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the source and see what it&amp;rsquo;s like.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.34.24-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.34.24-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, well well. What do we have here? There&amp;rsquo;s an api route that outputs an SVG badge for a status page. The badge says &amp;lsquo;Degraded&amp;rsquo; in amber if some of the URL&amp;rsquo;s are down, and &amp;lsquo;Up&amp;rsquo; in green if they are all up. Those words are present in an aria label and the svg &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag, so they&amp;rsquo;ll be detectable by the Uptime Kuma &amp;lsquo;keyword&amp;rsquo; search.&lt;/p&gt;
&lt;p&gt;Five minutes later, we&amp;rsquo;re in business. Thank you open source!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.41.52-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.41.52-pm.png" width="772" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>How to Have Cooler File Icons in VS Code</title><link>https://blog.iankulin.com/how-to-have-cooler-file-icons-in-vs-code/</link><pubDate>Mon, 29 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-have-cooler-file-icons-in-vs-code/</guid><description>&lt;p&gt;I watch a lot of programming demos on Youtube, and it&amp;rsquo;s been low key bugging me for a while that everyone has cooler little icons in the explorer view of their VS Code than I do. For example, they have the HTML 5 shield logo next to their &lt;code&gt;index.html&lt;/code&gt;, but I have the little fragment tag &amp;lt;&amp;gt;. Really, there was no point spending two hours customising my OhMyZSH! terminal if I&amp;rsquo;m just going to let myself down with disappointing VS Code file icons.&lt;/p&gt;
&lt;p&gt;It turns out the magical incantation you need to solve this problem is &amp;lsquo;File Icon Theme&amp;rsquo;, if you hit up the command pallet (&lt;code&gt;Cmd+Shift+P&lt;/code&gt; - Mac or &lt;code&gt;Ctrl+Shift+P&lt;/code&gt; - Windows) and type in &amp;lsquo;File Icon Theme&amp;rsquo;. You can choose a theme from this list, but if you&amp;rsquo;ve got a vanilla install, you probably only have options for &amp;lsquo;seti&amp;rsquo; or &amp;lsquo;minimal&amp;rsquo;. So click on &amp;lsquo;Install Additional File Icon Themes&amp;rsquo; and it will open the extension marketplace.&lt;/p&gt;
&lt;p&gt;The theme that sixteen million developers are using is &amp;lsquo;VS Code Icons&amp;rsquo;. So I grabbed that one and went from this to this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-6.35.20-pm-1.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Getting Your Vite React App to Work on Github Pages</title><link>https://blog.iankulin.com/getting-your-vite-react-app-to-work-on-github-pages/</link><pubDate>Fri, 26 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-your-vite-react-app-to-work-on-github-pages/</guid><description>&lt;img src="https://blog.iankulin.com/images/combined.png" width="512" alt=""&gt;
&lt;p&gt;One of the many cool things about GitHub is &lt;a href="https://pages.github.com"&gt;GitHub Pages&lt;/a&gt; - the free web hosting Microsoft gives you while they vacuum up &lt;a href="https://docs.github.com/en/copilot/overview-of-github-copilot/about-github-copilot-individual"&gt;your code for CoPilot&lt;/a&gt; training. Each repository you keep there can have pages at &lt;code&gt;&amp;lt;your-github-username&amp;gt;.github.io/&amp;lt;repo-name&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="github"&gt;GitHub&lt;/h3&gt;
&lt;p&gt;To enable this, you need to go into the settings for the repository - look down the left for &amp;ldquo;Pages&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-1.58.05-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to have it based on a complicated GitHub action (where your build step happens on GitHub when you push your code), but the easiest thing is just to have it deployed from a branch. To do this you choose which branch (usually main) and whereabouts in the main branch your HTML is. The choices are in the root of your project, or in the &lt;code&gt;/docs&lt;/code&gt; directory. I&amp;rsquo;ve chosen the &lt;code&gt;/docs&lt;/code&gt; directory in the screenshot above, since my messy React project is in the root.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all the GitHub set up we need. Now whenever I push my project to the &lt;code&gt;main&lt;/code&gt; branch on GitHub, whatever is in the &lt;code&gt;/docs&lt;/code&gt; directory will be uploaded to my GitHub page for this repo.&lt;/p&gt;
&lt;h3 id="vitereact"&gt;Vite/React&lt;/h3&gt;
&lt;p&gt;Now we need to make a couple of changes to our project to get this to work. The first is to tell Vite the &amp;ldquo;base directory&amp;rdquo; for the project which needs to be the repo name you&amp;rsquo;ve used on GutHub.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.04.50-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.04.50-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is written into the &lt;code&gt;index.html&lt;/code&gt; that is built as part of this process. If it&amp;rsquo;s not there, then any browser accessing your &lt;code&gt;index.html&lt;/code&gt; on gh-pages won&amp;rsquo;t be able to find your JavaScript, and the user will be left looking at a blank white page instead of your amazing app.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.11.06-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.11.06-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My process from this point, is to build the project with &lt;code&gt;npm run build&lt;/code&gt;. By default, this creates a &lt;code&gt;/dist&lt;/code&gt; directory in your project (which is already added to &lt;code&gt;.gitignore&lt;/code&gt;) and puts the project artifacts (the HTML, JavaScript, CSS and any images) into it. I then manually copy the artifacts over to the &lt;code&gt;/docs&lt;/code&gt; directory of the project and push it up to GitHub to be published - which takes two or three minutes.&lt;/p&gt;
&lt;p&gt;I like this manual step of copying the files over so that publishing is an intentful action on my part, and also, for solo projects I generally just work out of the main branch rather than on feature branches that then get PR&amp;rsquo;d into main. If you did want the process to be more CI/CD flavoured, you can just make another change the &lt;code&gt;vite.config.ts&lt;/code&gt; file to have your builds go straight to the &lt;code&gt;/docs&lt;/code&gt; folder.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { defineConfig } from &amp;#39;vite&amp;#39;import react from &amp;#39;@vitejs/plugin-react&amp;#39;// https://vitejs.dev/config/export default defineConfig({ base: &amp;#34;/mosh-expense/&amp;#34;, plugins: [react()], build: { outDir: &amp;#39;docs&amp;#39;, }})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once all that&amp;rsquo;s working, and you&amp;rsquo;ve pushed your changes and waited a minute or two, your project should be live to the world on &lt;code&gt;github.io&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.45.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.45.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you want users browsing your repo to find the live version, it&amp;rsquo;s worth editing your repository about settings to point to it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.47.30-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.47.30-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>React Expense Tracker App</title><link>https://blog.iankulin.com/react-expense-tracker-app/</link><pubDate>Mon, 22 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/react-expense-tracker-app/</guid><description>&lt;p&gt;I&amp;rsquo;m focused on React frontend skills these holidays, and &lt;a href="https://codewithmosh.com/p/ultimate-react-part1"&gt;working through Mosh&amp;rsquo;s React 18&lt;/a&gt; course. The exercise today (which I think I nailed, although I spent more than the recommended hour on) was a small app to track expenses. Like most of Mosh&amp;rsquo;s exercises it was great because it exercised all the understandings up to that point - so it&amp;rsquo;s a good starting React project. It used Zod for the form validation which is completely new to me, but looks great.&lt;/p&gt;
&lt;h3 id="the-specification"&gt;The Specification&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-6.02.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-6.02.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is an app for tracking expenses. Expenses can be in one of three &lt;em&gt;categories&lt;/em&gt;, they also must have a &lt;em&gt;description&lt;/em&gt; and an &lt;em&gt;amount&lt;/em&gt;. There&amp;rsquo;s a form for entering a new expense with a submit button. The form fields are validated before the expense item is added.&lt;/p&gt;
&lt;p&gt;Below the form for adding a new expense item is a list of expenses. Each one shows its description, amount and category. There is also a button to delete this expense. At the bottom of the list is a total for the expenses shown. The expenses shown in the list can be filtered by category with a drop down.&lt;/p&gt;
&lt;h3 id="design-decisions"&gt;Design Decisions&lt;/h3&gt;
&lt;p&gt;The first decision in an React app is &amp;ldquo;What are the components going to be?&amp;rdquo;. Clearly the form at the top is a component (I called mine &lt;code&gt;AddForm&lt;/code&gt;). The bottom section could be two - the filter and the list, but my style is to start with less components then seperate them out if they are getting complex so I considered the filtered list a single component called &lt;code&gt;ExpenseList&lt;/code&gt;. In Mosh&amp;rsquo;s solution, he did have these as separate components, arguing that the filter is likely to become more complex in future which is a sound argument, but too much premature optimisation for me.&lt;/p&gt;
&lt;h3 id="code"&gt;Code&lt;/h3&gt;
&lt;p&gt;With that decision made, we have enough to write the App.tsx. I also decided to persist the array of &lt;code&gt;Expense&lt;/code&gt;s to local storage to make it a nicer demo app. So with that, the app component code looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { useState, useEffect } from &amp;#34;react&amp;#34;;import AddForm from &amp;#34;./AddForm&amp;#34;;import { Expense } from &amp;#34;./types.ts&amp;#34;;import ExpenseList from &amp;#34;./ExpenseList&amp;#34;;import { v4 as uuidv4 } from &amp;#34;uuid&amp;#34;;function App() { const [expenses, setExpenses] = useState&amp;lt;Expense[]&amp;gt;(() =&amp;gt; { // Try to load expenses from local storage const savedExpenses = localStorage.getItem(&amp;#39;mosh-expense&amp;#39;); if (savedExpenses) { return JSON.parse(savedExpenses); } else { // If there are no saved expenses, load the sample expenses return loadSampleExpenses(); } }); useEffect(() =&amp;gt; { // Save expenses to local storage whenever they change localStorage.setItem(&amp;#39;mosh-expense&amp;#39;, JSON.stringify(expenses)); }, [expenses]); return ( &amp;lt;&amp;gt; &amp;lt;AddForm expenses={expenses} setExpenses={setExpenses} /&amp;gt; &amp;lt;hr /&amp;gt; &amp;lt;ExpenseList expenses={expenses} setExpenses={setExpenses} /&amp;gt; &amp;lt;/&amp;gt; );}export default App;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Because I&amp;rsquo;ve got my big boy TypeScript pants on, there&amp;rsquo;s a types.ts file with the types defined since they are common across a number of components:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;export interface Expense { id: string; description: string; amount: number; category: string;}// needed so we can validate the form without the id then// add it laterexport interface FormExpense { description: string; amount: number; category: string;}export interface ExpenseProps { expenses: Expense[]; setExpenses: (expenses: Expense[]) =&amp;gt; void;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The reason for the existence of &lt;code&gt;FormExpense&lt;/code&gt; without the &lt;code&gt;id&lt;/code&gt; field is that the interaction between Zod and React got very complicated if the &lt;code&gt;id&lt;/code&gt; field existed - Zod was determined to validate it. If I left it out of the Zod schema it caused problems because the type and the schema didn&amp;rsquo;t match, if I made it optional in the schema, it created type mismatch problems that were solvable with typecasting calisthenics but it was ugly and difficult to follow. In the end, I went with these two types and just added the &lt;code&gt;id&lt;/code&gt; after validation.&lt;/p&gt;
&lt;p&gt;I also made a decision in coding the two types (&lt;code&gt;FormExpense&lt;/code&gt; and &lt;code&gt;Expense&lt;/code&gt;) like this. I&amp;rsquo;ve made a small footgun for a future developer in that they might add a new Field to one of them, and neglect to add it to the other. I could have avoided that by extending one, like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;export interface FormExpense { description: string; amount: number; category: string;}export interface Expense extends FormExpense { id: string;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That solves that problem, but to my mind is not as clear - conceptually, &lt;code&gt;Expense&lt;/code&gt; is the main thing here, and really &lt;code&gt;FormExpense&lt;/code&gt; is a derivative of it - just with the id removed. The TypeScript language designers could have helped me out here with some syntax, but I forgive you &lt;a href="https://en.wikipedia.org/wiki/Anders_Hejlsberg"&gt;Anders&lt;/a&gt; because of the good living I once made out of Delphi. In my alternative timeline TypeScript it could look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;export interface Expense { description: string; amount: number; category: string;  id: string;}export interface FormExpense reduces Expense { id: %removed;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, I don&amp;rsquo;t love the duplication in the existing type definition, but it seemed like the lessor of two evils, the definitions are right next to each other, and we are working in TypeScript so the future developer will discover their mistake at the time they&amp;rsquo;ll make it.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a subtle design decision in the very simple &lt;code&gt;ExpenseProps&lt;/code&gt; worth thinking about. Firstly, I like having the props here with the &lt;code&gt;Expense&lt;/code&gt; definition, but mostly how simple they are. There&amp;rsquo;s no &lt;code&gt;addExpense()&lt;/code&gt;, &lt;code&gt;updateExpense()&lt;/code&gt;, or &lt;code&gt;deleteExpense()&lt;/code&gt;. That&amp;rsquo;s because, to the extent they are needed, they are dealt with inside each component.&lt;/p&gt;
&lt;p&gt;Components are already tightly bound to their data types, so we&amp;rsquo;re not making that worse by having this code with the compnent, but it would be perfectly valid to argue that all the data manipulation for expenses should be in one place. That argument would be won for me the second I needed to duplicate any of it - for example if two different components needed to delete an expense. But as the app stands now, this is neater, so that&amp;rsquo;s what I&amp;rsquo;ve gone with.&lt;/p&gt;
&lt;h3 id="expenses-list"&gt;Expenses List&lt;/h3&gt;
&lt;p&gt;The mechanism for the filter is that we have a local function that returns the filtered list based on the selected category which is a state variable of the &lt;code&gt;ExpensesList&lt;/code&gt; component. That &lt;code&gt;selectedCategory&lt;/code&gt; is updated from the onChange of the drop-down selector.&lt;/p&gt;
&lt;p&gt;The JSX just builds a table by .&lt;code&gt;map&lt;/code&gt;ping the filtered expenses. I don&amp;rsquo;t love the table code - it looks clunky. When I came back to look at it there were still some refactoring opportunities in it. Let&amp;rsquo;s have a look:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;table className=&amp;#34;table-bordered&amp;#34;&amp;gt; &amp;lt;thead&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Category&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/thead&amp;gt; &amp;lt;tbody&amp;gt; {filteredExpenses.map((expense) =&amp;gt; ( &amp;lt;tr key={expense.id}&amp;gt; &amp;lt;td className=&amp;#34;centred-td&amp;#34;&amp;gt;{expense.description}&amp;lt;/td&amp;gt; &amp;lt;td style={{ textAlign: &amp;#34;right&amp;#34; }}&amp;gt; $ {expense.amount.toLocaleString(&amp;#34;en-US&amp;#34;, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} &amp;lt;/td&amp;gt; &amp;lt;td className=&amp;#34;centred-td&amp;#34;&amp;gt;{expense.category}&amp;lt;/td&amp;gt; &amp;lt;td className=&amp;#34;centred-td&amp;#34;&amp;gt; &amp;lt;button className=&amp;#34;btn btn-outline-danger&amp;#34; onClick={() =&amp;gt; handleDelete(expense.id)} &amp;gt; Delete &amp;lt;/button&amp;gt; &amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; ))} &amp;lt;/tbody&amp;gt; &amp;lt;tfoot&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;td&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;td style={{ textAlign: &amp;#34;right&amp;#34;, fontWeight: &amp;#34;bold&amp;#34; }}&amp;gt; $ {filteredExpenses .reduce((total, expense) =&amp;gt; { return total + expense.amount; }, 0) .toLocaleString(&amp;#34;en-US&amp;#34;, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} &amp;lt;/td&amp;gt; &amp;lt;td colSpan={2}&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/tfoot&amp;gt;&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing I&amp;rsquo;m noticing is the code to format the amounts to US currency. That&amp;rsquo;s in there twice - once for each expense, and once for the total. We could write a function for that, but since we&amp;rsquo;re in React-land, let&amp;rsquo;s make it a component. Ideally we&amp;rsquo;d extract the user&amp;rsquo;s local currency from the browser settings somehow, but I don&amp;rsquo;t think that&amp;rsquo;s possible. In a real app, I guess we&amp;rsquo;d let them set it in settings. For the moment, they&amp;rsquo;ll just have to live in dollar land.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;interface TDCurrencyProps { children: number; fontWeight?: string; }function TDCurrency({children, fontWeight = &amp;#34;normal&amp;#34;}:TDCurrencyProps) { return ( &amp;lt;td style={{ textAlign: &amp;#34;right&amp;#34;, fontWeight: fontWeight }}&amp;gt; $ {children.toLocaleString(&amp;#34;en-US&amp;#34;, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} &amp;lt;/td&amp;gt; );}export default TDCurrency;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we&amp;rsquo;re thinking React-ivly! The table looks better already. Also, since now the alignment for the number column is in its own component, the centred styling can be applied to all the &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;s so I can remove the class I had added for that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;table className=&amp;#34;table-bordered&amp;#34;&amp;gt; &amp;lt;thead&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Category&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/thead&amp;gt; &amp;lt;tbody&amp;gt; {filteredExpenses.map((expense) =&amp;gt; ( &amp;lt;tr key={expense.id}&amp;gt; &amp;lt;td&amp;gt;{expense.description}&amp;lt;/td&amp;gt; &amp;lt;TDCurrency&amp;gt;{expense.amount}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td&amp;gt;{expense.category}&amp;lt;/td&amp;gt; &amp;lt;td&amp;gt; &amp;lt;button className=&amp;#34;btn btn-outline-danger&amp;#34; onClick={() =&amp;gt; handleDelete(expense.id)} &amp;gt; Delete &amp;lt;/button&amp;gt; &amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; ))} &amp;lt;/tbody&amp;gt; &amp;lt;tfoot&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;td&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;TDCurrency fontWeight=&amp;#34;bold&amp;#34;&amp;gt;{filteredTotal}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td colSpan={2}&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/tfoot&amp;gt;&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Much nicer. Would it be crazy to component-ise that delete button as well? Probably, but while we&amp;rsquo;re on a roll, and this is starting to feel like fun.&lt;/p&gt;
&lt;p&gt;I really feel I&amp;rsquo;m starting to get the benefits of React that we&amp;rsquo;re paying for with the tooling complexity and larger bundle size.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;interface TDDeleteButtonProps { onClick: (id: string) =&amp;gt; void; id: string;}function TDDeleteButton({ onClick, id}: TDDeleteButtonProps) { return ( &amp;lt;td&amp;gt; &amp;lt;button className=&amp;#34;btn btn-outline-danger&amp;#34; onClick={() =&amp;gt; onClick(id)} &amp;gt; Delete &amp;lt;/button&amp;gt; &amp;lt;/td&amp;gt; );};export default TDDeleteButton;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m much happier with the table now:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;table className=&amp;#34;table-bordered&amp;#34;&amp;gt; &amp;lt;thead&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Category&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/thead&amp;gt; &amp;lt;tbody&amp;gt; {filteredExpenses.map((expense) =&amp;gt; ( &amp;lt;tr key={expense.id}&amp;gt; &amp;lt;td&amp;gt;{expense.description}&amp;lt;/td&amp;gt; &amp;lt;TDCurrency&amp;gt;{expense.amount}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td&amp;gt;{expense.category}&amp;lt;/td&amp;gt; &amp;lt;TDDeleteButton onClick={handleDelete} id={expense.id}/&amp;gt; &amp;lt;/tr&amp;gt; ))} &amp;lt;/tbody&amp;gt; &amp;lt;tfoot&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;td&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;TDCurrency fontWeight=&amp;#34;bold&amp;#34;&amp;gt;{filteredTotal}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td colSpan={2}&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/tfoot&amp;gt;&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The selector at the top of the expense list, is, well, okay.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;div className=&amp;#34;mb-3 category-selector&amp;#34;&amp;gt; &amp;lt;label htmlFor=&amp;#34;category&amp;#34; className=&amp;#34;form-label&amp;#34;&amp;gt; Category:{&amp;#34; &amp;#34;} &amp;lt;/label&amp;gt; &amp;lt;select id=&amp;#34;category&amp;#34; name=&amp;#34;category&amp;#34; className=&amp;#34;form-control drop-down&amp;#34; onChange={handleCategoryChange} &amp;gt; &amp;lt;option value=&amp;#34;All&amp;#34;&amp;gt;All&amp;lt;/option&amp;gt; &amp;lt;option value=&amp;#34;Groceries&amp;#34;&amp;gt;Groceries&amp;lt;/option&amp;gt; &amp;lt;option value=&amp;#34;Utilities&amp;#34;&amp;gt;Utilities&amp;lt;/option&amp;gt; &amp;lt;option value=&amp;#34;Entertainment&amp;#34;&amp;gt;Entertainment&amp;lt;/option&amp;gt; &amp;lt;/select&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The specification only specified these three categories - Groceries, Utilities, and Entertainment. and I have them hard-coded here, and in the add form. In Mosh&amp;rsquo;s version he&amp;rsquo;s made them a enum which is definitely nicer and future-proof-ier for a very small increase in complexity.&lt;/p&gt;
&lt;h3 id="mosh"&gt;Mosh&lt;/h3&gt;
&lt;p&gt;I really enjoy Mosh&amp;rsquo;s videos and courses. He generally puts the first hour of his &lt;a href="https://www.youtube.com/@programmingwithmosh"&gt;courses on Youtube&lt;/a&gt;, so you can try before you buy. He has a knack for anticipating the questions that occur to you as he&amp;rsquo;s explaining something so you feel a sense of continually being slightly challenged, but never overwhelmed. Hard recommend if you are learning programming.&lt;/p&gt;</description></item><item><title>What's unfinished in your Udemy?</title><link>https://blog.iankulin.com/whats-unfinished-in-your-udemy/</link><pubDate>Fri, 19 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/whats-unfinished-in-your-udemy/</guid><description>&lt;p&gt;If you work or study in tech, I always feel a good getting-to-know-you question is &amp;ldquo;what courses or tutorials did you start, but not finish?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;My Udemy doesn&amp;rsquo;t look &lt;em&gt;too&lt;/em&gt; bad:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-29-at-1.30.02-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The ZTM course was good, but I got stuck on an AI API exercise. I think it&amp;rsquo;s a common sticking point for students since Andrei includes a little rant about how it definitely does work - but I downloaded his repo with the solution and it was having the same errors I was and I gave up in frustration. I probably should have just skipped that one.&lt;/p&gt;
&lt;p&gt;The Linux one was really good - I learned a heap of basic little things (although I struggled with the guy&amp;rsquo;s accent a little) - things like tab for CLI completion. I guess you would learn this stuff from work colleagues, but if you&amp;rsquo;re self taught someone else needs to show you. This isn&amp;rsquo;t the highly recommended Linux basics course I wanted to do (&lt;a href="https://www.udemy.com/course/learn-linux-in-5-days/"&gt;Learn Linux in 5 days&lt;/a&gt;), but it was a lot cheaper.&lt;/p&gt;
&lt;h2 id="what-else"&gt;What else?&lt;/h2&gt;
&lt;p&gt;So that&amp;rsquo;s my Udemy, what else haven&amp;rsquo;t I finished?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of Swift UI&lt;/a&gt; (47/100) - This is the free Paul Hudson course. I so highly recommend it for budding iOS developers I paid to join his super club or whatever that&amp;rsquo;s called although you don&amp;rsquo;t really need to. I got up to day 47 before deciding I wanted to work on web dev rather than iOS. I still use these skills occasionally for writing little MacOS apps.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://missing.csail.mit.edu/"&gt;Missing Semester Lectures&lt;/a&gt; (6/11) - Some CS lecturers at MIT realised there was some mechanics of day-to-day development missing from their courses (such as source control) so they put these together. They are great. Some of it falls into the &amp;lsquo;didn&amp;rsquo;t know you needed to know&amp;rsquo; so I plan to come back to these and finish one day.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://cs193p.sites.stanford.edu/2021-0"&gt;CS193p&lt;/a&gt; (4/16) - These iOS development lectures are high quality and enjoyable, but if you run into issues (and are not enrolled in this unit at Stanford) you can get stuck - my best source of assistance was searching on github and finding others who had been through it. I gave up on these to focus on the Paul Hudson ones that were in more digestible chunks, and with some assistance (if you cared to pay for it).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also completed numerous little free courses and stand-alone videos from YouTube - names that spring to mind are Jay from &lt;a href="https://www.youtube.com/@LearnLinuxTV"&gt;Learn Linux TV&lt;/a&gt;, &lt;a href="https://www.youtube.com/@WebDevSimplified"&gt;Web Dev Simplified&lt;/a&gt;, &lt;a href="https://www.youtube.com/@Fireship"&gt;Fireship&lt;/a&gt;, the &lt;a href="https://www.youtube.com/@NetNinja"&gt;Net Ninja&lt;/a&gt;, &lt;a href="https://www.youtube.com/@programmingwithmosh"&gt;Mosh&lt;/a&gt;, &lt;a href="https://www.youtube.com/@t3dotgg"&gt;Theo&lt;/a&gt;, &lt;a href="https://www.youtube.com/@NetworkChuck"&gt;Network Chuck&lt;/a&gt;, &lt;a href="https://www.youtube.com/@JamesQQuick"&gt;James Quick&lt;/a&gt;, and &lt;a href="https://www.youtube.com/@apalrdsadventures"&gt;Apalrd&amp;rsquo;s Adventures&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Mosh - I&amp;rsquo;ve paid for a month with the intention of doing his React 18 course in that time. I&amp;rsquo;m optimistic I will.&lt;/p&gt;
&lt;h3 id="whats-better-than-finishing-a-course"&gt;What&amp;rsquo;s better than finishing a course?&lt;/h3&gt;
&lt;p&gt;In most cases, what&amp;rsquo;s prevented me from finishing these courses, is that I&amp;rsquo;ve invested the time into writing code or doing projects that use the skills instead. I don&amp;rsquo;t feel bad about this, and in fact I&amp;rsquo;d recommend it. The only benefit of a course over just building projects is that they can teach you the things you didn&amp;rsquo;t know you needed. I listen to industry podcasts, and follow a lot of webdev people on Masterdon to try and help with that sort of discovery.&lt;/p&gt;
&lt;p&gt;For example, I&amp;rsquo;ve never used Zod, NextJS or Tailwind. But I know what they are, and where they would be useful to me because I&amp;rsquo;m tuned in to developer chatter about things.&lt;/p&gt;</description></item><item><title>Copying Objects in JS</title><link>https://blog.iankulin.com/copying-objects-in-js/</link><pubDate>Mon, 15 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/copying-objects-in-js/</guid><description>&lt;p&gt;I&amp;rsquo;ve paid for a month of Mosh to do his &lt;a href="https://codewithmosh.com/p/ultimate-react-part1"&gt;React 18 course&lt;/a&gt;, and one of the things he makes a big deal about is not to go too deep with nested objects for your state. As soon as you start to update them it becomes apparent why.&lt;/p&gt;
&lt;p&gt;Because of the way state works in React, if we need to update part of an object it has to be deep copied, the changes applied to this copy, then that new copy passed back to React to replace the previous version. So, how we copy objects becomes a matter of particular interest.&lt;/p&gt;
&lt;h3 id="spread-operator"&gt;Spread operator&lt;/h3&gt;
&lt;p&gt;JavaScript has some good tools to help us here, the primary one being the spread operator. Imagine we want to create a value copy of this object:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const originalObject = { name: &amp;#39;John&amp;#39;, age: 25, hometown: &amp;#39;Birmingham&amp;#39; };
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can&amp;rsquo;t just assign this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const newObject = originalObject;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since now both &amp;rsquo;newObject&amp;rsquo; and &amp;lsquo;originalObject&amp;rsquo; both point to the same in-memory object. So if we changed &lt;code&gt;newObject.name = 'Ian',&lt;/code&gt; then &lt;code&gt;originalObject.name&lt;/code&gt; would also become &lt;code&gt;'Ian'&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What we really want is something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const newObject = {name: originalObject.name, age: originalObject.age, hometown: originalObject.hometown};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This conveys our intent really clearly, but is very quickly going to be come tedious, especially as the objects grow. Luckily, JS has a cool solution for this - the spread operator. We can replace the code above with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const newObject = { ...originalObject }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;What&amp;rsquo;s even nicer (especially in the context of making changes to React state) is that we can selectively replace parts during the copy. If we needed an object for John&amp;rsquo;s twin we could do this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const newObject = { ...originalObject, name: &amp;#39;Jill&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="nested"&gt;Nested&lt;/h3&gt;
&lt;p&gt;So the above works great for flat objects, but what about when there&amp;rsquo;s some nesting. Let&amp;rsquo;s consider this object:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{ name: &amp;#39;John&amp;#39;, age: 25, subjects: [ &amp;#39;engineering&amp;#39;, &amp;#39;anatomy&amp;#39; ], hometown: &amp;#39;Birmingham&amp;#39;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now there&amp;rsquo;s an array inside the object, if we do a shallow copy with a spread:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const newObject = { ...originalObject }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The the array will only have been copied by value, to copy the whole thing properly (which we&amp;rsquo;ll need to do before we alter it and give it back to React) we&amp;rsquo;ll need to manually spread the array as well.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const updatedObject = { ...originalObject, subjects: [...originalObject.subjects]};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we want to enroll John in maths, we can just add that in when creating the array:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const updatedObject = { ...originalObject, subjects: [...originalObject.subjects, &amp;#39;maths&amp;#39;]};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For removing a subject we can use JS&amp;rsquo;s array.filter() method:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const updatedObject = { ...originalObject, subjects: originalObject.subjects.filter(subject =&amp;gt; subject !== &amp;#39;anatomy&amp;#39;)};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see how this is quickly going to get messy if this was an array of objects inside out object, and we had to go down another level. Hence the advice to avoid that.&lt;/p&gt;</description></item><item><title>CSS for React Components</title><link>https://blog.iankulin.com/css-for-react-components/</link><pubDate>Fri, 12 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/css-for-react-components/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.30.32-pm.jpg" alt=""&gt;
&lt;em&gt;Subscribe to my UX design course 😉&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you think back to HTML as being a document with headings and paragraphs and other semantic bits, it made a lot of sense to have the styles (expressed as CSS) separate to the document. This allows us to change the styles without touching the document - perhaps the user wanted a dark theme, needed the text bigger for accessibility, or perhaps the document was being consumed in some other way - for example a screen reader - so the styles were superfluous.&lt;/p&gt;
&lt;p&gt;In tension to this idea, is the idea that all the code related to a single thing should be encapsulated in one place. This is why we invented object orientated programming - we are creating such huge software systems that for a human to be able to maintain them, they need broken down into chunks that can be fully held in mind while we are working on them. Also, when we are talking about a modern single page application, we&amp;rsquo;ve come a long way from thinking of a web &amp;lsquo;page&amp;rsquo; as being a document to be passively consumed.&lt;/p&gt;
&lt;p&gt;Since the point of React is to create reusable &amp;lsquo;components&amp;rsquo; where the JS and HTML are written together, it&amp;rsquo;s reasonable to wonder if perhaps the CSS shouldn&amp;rsquo;t be in there too.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at a few different approaches to managing styles for our components in React.&lt;/p&gt;
&lt;h3 id="old-style-global"&gt;Old Style Global&lt;/h3&gt;
&lt;p&gt;Most times when I&amp;rsquo;m writing vanilla JS, if I&amp;rsquo;m not leveraging off &lt;a href="https://picocss.com/"&gt;Pico&lt;/a&gt; or &lt;a href="https://getbootstrap.com/"&gt;Bootstrap&lt;/a&gt; I have a single site-wide &lt;code&gt;styles.css&lt;/code&gt; file. Obviously this is going to be an option for a React app too. We&amp;rsquo;d just link it from the index.html in the root of our folder. Job&amp;rsquo;s a goodun.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.27.51-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.27.51-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The most common downside of this approach, that I come across, is that I change the rest of the HTML through the various iterations, but never clean up the CSS. So I get left with bits of CSS that I&amp;rsquo;m not sure if they are being used somewhere - so I&amp;rsquo;m not brave enough to delete them because my UX testing is not good enough, so those fragments just end up sitting around forever.&lt;/p&gt;
&lt;h3 id="old-style-local"&gt;Old Style local&lt;/h3&gt;
&lt;p&gt;The first CSS we ever wrote was just stuffed in the HTML in style tags, and of course that&amp;rsquo;s still an option. We can use variables to help with the readability, and end up with something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function Card(props: CardProps) { const cardStyles: React.CSSProperties = { width: &amp;#39;120px&amp;#39;, boxShadow: &amp;#39;0 4px 8px 0 rgba(0, 0, 0, 0.2)&amp;#39;, textAlign: &amp;#39;center&amp;#39;, backgroundColor: &amp;#39;cornsilk&amp;#39;, margin: &amp;#39;10px&amp;#39;, padding: &amp;#39;10px&amp;#39;, transform: &amp;#39;scale(1)&amp;#39;, transition: &amp;#39;transform 0.3s&amp;#39;, }; const pictureStyles: React.CSSProperties = { width: &amp;#39;100px&amp;#39;, height: &amp;#39;150px&amp;#39;, margin: &amp;#39;auto&amp;#39;, borderRadius: &amp;#39;50%&amp;#39;, border: &amp;#39;2px solid #000&amp;#39;, }; const nameStyles: React.CSSProperties = { color: &amp;#39;black&amp;#39;, fontSize: &amp;#39;18px&amp;#39;, }; return ( &amp;lt;div style={cardStyles}&amp;gt; &amp;lt;img style={pictureStyles} src={props.image} alt={props.name} width={100} /&amp;gt; &amp;lt;p style={nameStyles}&amp;gt;{props.name}&amp;lt;/p&amp;gt; &amp;lt;/div&amp;gt; );}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Initially, this seemed like a good solution, but there&amp;rsquo;s a few bumps involved. The first is that, or course, this is not real CSS. You can see from the type that TypeScript forced me to use (React.CSSProperties) that these are React types that will get turned into CSS at some distant time in the future. Because of this, there are some oddities - my muscle memory wants to type the CSS property names like &lt;code&gt;box-shadow&lt;/code&gt;, but for this they need to be camel case.&lt;/p&gt;
&lt;p&gt;The next issue of &amp;lsquo;it&amp;rsquo;s not actually CSS&amp;rsquo; was when I wanted to do my hover effect for the cards. In CSS this is just:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.card:hover { transform: scale(1.05); background-color: lightgoldenrodyellow;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If I want to do that with inline styles, I need to capture the MouseEnter and MouseLeave events for the card, then swap the inline styles in and out in code. Ain&amp;rsquo;t nobody got the time for that.&lt;/p&gt;
&lt;h3 id="css--component-libraries"&gt;CSS &amp;amp; Component Libraries&lt;/h3&gt;
&lt;p&gt;A common and appealing answer to inline styles is to use CSS libraries such as BootStrap, TailWind etc. As well as allowing you to add complex styles into JSX with short memorable tags, they often enforce a design aesthetic without the developer having to put much thought into it. Since I have minimal design skills, that&amp;rsquo;s an appealing option.&lt;/p&gt;
&lt;p&gt;Taking this a step further are component are React component libraries such as &lt;a href="https://chakra-ui.com/"&gt;chakra&lt;/a&gt;, &lt;a href="https://mui.com/"&gt;Material UI&lt;/a&gt;, and &lt;a href="https://ant.design/components/overview"&gt;Ant&lt;/a&gt;. With these systems, you get styled components (that can be modified) that follow a unified design - you&amp;rsquo;re not really thinking of CSS but at a high level of abstraction.&lt;/p&gt;
&lt;h3 id="componentcss"&gt;component.css&lt;/h3&gt;
&lt;p&gt;I guess the default (since it&amp;rsquo;s generated like this in the create-app step) way of managing CSS closer to our components is to have a .CSS file for each component. This overcomes the &amp;lsquo;cleaning up&amp;rsquo; problem described above. If I&amp;rsquo;m deleting the NavBar component from this project, I can safely eliminate the NavBar.css file at the same time.&lt;/p&gt;
&lt;p&gt;The component CSS file is just imported at the top of the component file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.59.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.59.14-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you build an app with the CSS in component.css files like this, all the CSS is combined into a single CSS file by the bundler. This raises the possibility of naming conflicts leading the hard to track down problems later. To avoid this I tend to use the component name as a class name and use that to tightly scope the CSS to avoid it bleeding out into other elements.&lt;/p&gt;
&lt;h3 id="modules"&gt;Modules&lt;/h3&gt;
&lt;p&gt;A bit fancier approach is to use CSS modules. This is somewhat similar to the component CSS files described above. Our CSS files have to be renamed to end in &lt;code&gt;.module.css&lt;/code&gt;, then in the code where you normally insert the class names, you use the class names from the styles library:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import styles from &amp;#34;./Card.module.css&amp;#34;;// props for cardinterface CardProps { name: string; image: string;}function Card(props: CardProps) { return ( &amp;lt;div className={styles.card}&amp;gt; &amp;lt;img className={styles.picture} src={props.image} alt={props.name} width={100} /&amp;gt; &amp;lt;p className={styles.name}&amp;gt;{props.name}&amp;lt;/p&amp;gt; &amp;lt;/div&amp;gt; );}export default Card;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those class names match the CSS:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.card { width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; /* tilt and enlarge on mouseover */ transform: scale(1); transition: transform 0.3s;}.card:hover { transform: scale(1.05); background-color: lightgoldenrodyellow;}.name { font-size: 18px; color: black;}.picture { width: 100px; height: 150px; margin: auto; border-radius: 50%; border: 2px solid black;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The advantage of using modules, is that you now do &lt;em&gt;not&lt;/em&gt; have worry about name clashes. If I build the app and have a look in the generated CSS file, you can see how that works:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;._card_j2a8o_2 { width: 120px; box-shadow: 0 4px 8px #0003; text-align: center; background-color: #fff8dc; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All the generated CSS has unique class names created for it! Presumably these match the ones being used in the JSX, and this explains why you need to use the &lt;code&gt;styles.&lt;/code&gt; names in your components.&lt;/p&gt;
&lt;h3 id="styled-components"&gt;Styled Components&lt;/h3&gt;
&lt;p&gt;There are several libraries that tackle the issue of what to do about CSS in React. One of the more popular ones is &lt;code&gt;Styled Components&lt;/code&gt;. How this works is that you define a base component with some styles in it (with a weird backticky syntax), then build your own components from the styled one. It makes more sense when you see it than my written explanation.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import styled from &amp;#34;styled-components&amp;#34;;const StyledDiv = styled.div` width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s; &amp;amp;:hover { transform: scale(1.05); background-color: lightgoldenrodyellow; }`;const StyledImage = styled.img` width: 100px; height: 150px; margin: auto; border-radius: 50%; border: 2px solid black;`;const StyledP = styled.p` font-size: 18px; color: black;`;// props for cardinterface CardProps { name: string; image: string;}function Card(props: CardProps) { return ( &amp;lt;StyledDiv className=&amp;#34;card&amp;#34;&amp;gt; &amp;lt;StyledImage className=&amp;#34;picture&amp;#34; src={props.image} alt={props.name} width={100} /&amp;gt; &amp;lt;StyledP className=&amp;#34;name&amp;#34;&amp;gt;{props.name}&amp;lt;/StyledP&amp;gt; &amp;lt;/StyledDiv&amp;gt; );}export default Card;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you don&amp;rsquo;t mind more dependencies, this is a great solution - I like the clarity of the philosophy of creating styled versions of regular elements as React elements, then using them as the building blocks of our new component. All the CSS attributes you&amp;rsquo;ve learned are still there.&lt;/p&gt;
&lt;p&gt;The only thing I needed to look up was how to deal with my hover. In the CSS this had been:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.card { width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s;}.card:hover { transform: scale(1.05); background-color: lightgoldenrodyellow;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which had to be translated into this with the &amp;amp; syntax:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const StyledDiv = styled.div` width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s; &amp;amp;:hover { transform: scale(1.05); background-color: lightgoldenrodyellow; }`;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So - lots of options for dealing with CSS in React. I haven&amp;rsquo;t written much, so I&amp;rsquo;m not sure what my personal preference is. styled-components is definitely the most elegant of the approaches I&amp;rsquo;ve looked at here, but I&amp;rsquo;m a dependency calorie counter so my natural inclination is look elsewhere. The CSS file for each component seems like the next best system - I like that there&amp;rsquo;s no special hooks in the JSX besides the class names I would have used in ordinary HTML, but then you have the risk of name clashes. They can be avoided with the modules - but I am sort of used to dealing with that anyway.&lt;/p&gt;</description></item><item><title>React - a To Do Example</title><link>https://blog.iankulin.com/react-a-to-do-example/</link><pubDate>Mon, 08 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/react-a-to-do-example/</guid><description>&lt;p&gt;Since I&amp;rsquo;m on a roll making different versions of the To Do app, this might be a good time to talk about &lt;a href="https://react.dev/"&gt;React&lt;/a&gt;. React is one of the giants of front end libraries. It&amp;rsquo;s based on a few big ideas - and to work effectively in React you need to wrap your head around these.&lt;/p&gt;
&lt;h3 id="overview"&gt;Overview&lt;/h3&gt;
&lt;p&gt;Components - when you are developing in React, the starting point of your build is to decompose the user interface in to logical pieces. These components (comprising a mixture of HTML and Javascript) will be the building blocks of your app. In a good composable architecture components are reusable, and that is true for React (there are several sources of components you can pull in). For example, if you created some sort of special slider for your app, it is possible to reuse that quite easily.&lt;/p&gt;
&lt;p&gt;Declarative - this was one of the big barriers when I was learning SwiftUI. The UI is just described. It&amp;rsquo;s not a big step when you&amp;rsquo;re coming from HTML - all that is is a description of the user interface. The next step of this is that React deals with using the state of your data model to update the UI. This means that these state-UI connections are made very explicit (which I like) and protected. For example if there&amp;rsquo;s a counter on a web page, you can&amp;rsquo;t change the HTML of the page to increment the counter, and in fact, you can&amp;rsquo;t directly change the counter. These things are wrapped up so React can manage them, and you have to play by React&amp;rsquo;s rules.&lt;/p&gt;
&lt;p&gt;Virtual DOM - since each component knows what state it depends upon, and changing that state causes the component to be redrawn, it quickly gets expensive with parts of the page being reloaded. To improve performance, and reduce often unneeded flashes of content loading, React keeps a copy of the DOM and makes changes to it rather than the real DOM. Since this is instrumented, React can easily see what changes &lt;em&gt;really&lt;/em&gt; need to be percolated to the browser DOM and can manage how that happens in the most efficient way. Sometimes I think I know what I want to happen for something to be efficient but in React, it&amp;rsquo;s often not in your control, and you just need to trust the system.&lt;/p&gt;
&lt;h3 id="tooling"&gt;Tooling&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;re leaving my comfort zone of straightforward development environments, along with the major benefit of working in Javascript. The complexity of the React system requires a build step to produce the production artifacts. Whether you use the standard &lt;a href="https://create-react-app.dev/"&gt;Create-React&lt;/a&gt;, or &lt;a href="https://vitejs.dev/"&gt;Vite&lt;/a&gt; (as I did for this project) you get a system for bundling the code, mapping it (since for debugging you need a way to translate the source line that&amp;rsquo;s running back to the human readable source), and a development server to run the app while you&amp;rsquo;re working on it.&lt;/p&gt;
&lt;p&gt;These things inevitably add to complexity and errors, and are the reason that big projects start to need tools like development containers to remove a pain point. Like anything, you get better with experience, but especially at the start there&amp;rsquo;s a developer cost involved. React is incredibly popular, so most people&amp;rsquo;s calculus on this is that the extra complexity in project management is made up for with improved developer experience.&lt;/p&gt;
&lt;p&gt;In any case, I got started by typing &lt;code&gt;npm create vite@latest .&lt;/code&gt; and then choosing React and Javascript. This created a starter project, and spun it up in the development server. In the &lt;code&gt;package.json&lt;/code&gt; file it gives you, there is a build command that can be run with &lt;code&gt;npm run build&lt;/code&gt; to create the output files. I had issues with the development server that serves up these frontend files - since I also needed to run a real server (the unchanged REST API server for Todo Items data from the earlier project) on a different port. Then it complained - thinking it was part of a cross-site scripting hack. To overcome it, I just built the production code for each round of testing - with such a little project this wasn&amp;rsquo;t a hardship. But when I did have an error to track, it pointed me to a line number that turned out to be about 20 pages of minified code.&lt;/p&gt;
&lt;h3 id="components"&gt;Components&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-22-at-7.47.24-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Just a little reminder of how this app looks so we can think about how we&amp;rsquo;re going to break it down. There is a list of todo items, each one has a button to delete it (which we do when it has been completed) then at the bottom there&amp;rsquo;s a little form to add a new item (by clicking the button or hitting enter) to the bottom of the list.&lt;/p&gt;
&lt;p&gt;I ended up with three components - the App (every React app has one), the TodoList, and the AddTodoForm. Note that I could easily have had a forth component - the TodoItem. This is a bit of matter of taste - I probably would have if I wanted to do something fancier like editing in place - but for the current UX the cost of extracting out another component wasn&amp;rsquo;t worth the benefit.&lt;/p&gt;
&lt;h3 id="anatomy-of-a-component"&gt;Anatomy of a component&lt;/h3&gt;
&lt;p&gt;I claimed earlier that a component was it&amp;rsquo;s HTML and Javascript wrapped up together which, while a massive simpliciation, is a good place to start thinking about it. Every component is just a function that returns a bunch of (templated) HTML. We&amp;rsquo;ll start off by developing our AddTodoForm. At it&amp;rsquo;s simplest, it could be something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function AddTodoForm() { return ( &amp;lt;form&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; required /&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; );}export default AddTodoForm;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But this little form component can&amp;rsquo;t really talk to the world, where as we need it to add a todo item. First, let&amp;rsquo;s track any changes to the text field.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { useState } from &amp;#39;react&amp;#39;;function AddTodoForm(props) { const [value, setValue] = useState(&amp;#39;&amp;#39;); return ( &amp;lt;form onSubmit={handleSubmit}&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; value={value} onChange={e =&amp;gt; setValue(e.target.value)} required /&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; );}export default AddTodoForm;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is an example of the very explicit management of state I was talking about earlier.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const [value, setValue] = useState('');&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;useState() is a React hook for managing state. This line gives us a getter (value) and setter (setValue) for this variable, and set&amp;rsquo;s its initial state to &amp;lsquo;&amp;rsquo;. If the value changes the component will be redrawn. React will know that the value has changed as this is built into the setValue() function where we never need see or worry about it. If you foolishly decided to side-step React and assign directly to &lt;code&gt;value&lt;/code&gt;, I guess there&amp;rsquo;d be a runtime error, or even worse, no error and the management of the DOM state wuld fall into some type of chaos.&lt;/p&gt;
&lt;p&gt;What we&amp;rsquo;re doing with this value (which I&amp;rsquo;m now realising is very badly named) is using it to collect the text input from out form. It&amp;rsquo;s constantly updated as the user types.&lt;/p&gt;
&lt;p&gt;onChange={e =&amp;gt; setValue(e.target.value)}&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s our value sorted, but of course we need to add it to our data model. This model is being managed at the App level so there&amp;rsquo;s a bit of juggling needed to get it out to there. This &amp;lsquo;plumbing&amp;rsquo; cost is the downside of these types or framework, but it&amp;rsquo;s not really complex and quickly becomes routine.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll start at the top, here&amp;rsquo;s the relevant code from App.jsx - our App component.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; const [todos, setTodos] = useState([]); const addTodo = (newTodo) =&amp;gt; { fetch(&amp;#39;http://localhost:3000/todos&amp;#39;, { method: &amp;#39;POST&amp;#39;, headers: { &amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39;, }, body: JSON.stringify(newTodo), }) .then(response =&amp;gt; response.json()) .then(data =&amp;gt; { // Update the todos state with the new todo setTodos([...todos, data]); }); };
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This useState hook should be starting to look familiar. We read the todos from &lt;code&gt;todos&lt;/code&gt;, and write to them with &lt;code&gt;setTodos()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;addTodo&lt;/code&gt; does the actual work of saving it to the database (via our REST API) then creates a new &lt;code&gt;todos&lt;/code&gt; array by adding our new one to the end. But we need to pass this down into our AddTodoForm. Here&amp;rsquo;s the main part of the App that returns out HTML, in this case that&amp;rsquo;s the list of todos and our form:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; return ( &amp;lt;main&amp;gt; &amp;lt;h1&amp;gt;To do&amp;lt;/h1&amp;gt; &amp;lt;TodoList todos={todos} onDeleteTodo={deleteTodo}/&amp;gt; &amp;lt;AddTodoForm onAddTodo={addTodo} /&amp;gt; &amp;lt;/main&amp;gt; )
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see here that the todos array and a function &lt;em&gt;deleteTodo&lt;/em&gt;() are passed into the the TodoList component, and that our &lt;em&gt;addTodo()&lt;/em&gt; function is passed to the AddTodoForm.&lt;/p&gt;
&lt;p&gt;In React, things like this that are passed into a component are passed as a single object variable called &amp;lsquo;props&amp;rsquo; - short for properties. It seems crazy to me to be bundling them like this in a language in 2023 rather than passing them explicitly as separate variables. This lack or clarity about what&amp;rsquo;s being passed into a component is doubtless one of the reasons TypeScript is such a common combo with React. It&amp;rsquo;s certainly the first time I&amp;rsquo;ve felt the need of it.&lt;/p&gt;
&lt;p&gt;There is a lighter partial solution to adding types to props so that the linter can call out any issues - this is the PropTypes library - once it&amp;rsquo;s installed, we import it with &lt;code&gt;import PropTypes from 'prop-types';&lt;/code&gt; then we can add a definition for the props at the bottom of the file. For our AddTodoForm, this would look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AddTodoForm.propTypes = { onAddTodo: PropTypes.func.isRequired,};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now the linter will prevent us from using anything other than props.onAddToDo, and it will flag if it&amp;rsquo;s being used as anything other than a function.&lt;/p&gt;
&lt;p&gt;Anyway, the props are passed in and we can extract the function from it to use.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { useState } from &amp;#39;react&amp;#39;;function AddTodoForm(props) { const [value, setValue] = useState(&amp;#39;&amp;#39;); const handleSubmit = (event) =&amp;gt; { event.preventDefault(); if (!value) return; props.onAddTodo({ todo_item: value }); setValue(&amp;#39;&amp;#39;); }; return ( &amp;lt;form onSubmit={handleSubmit}&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; value={value} onChange={e =&amp;gt; setValue(e.target.value)} required /&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; );}export default AddTodoForm;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The arrangement of elements in this file will start to become familiar - there&amp;rsquo;s often some state at the top with the useState() hook, then a few handler functions, then our final return of the &amp;lsquo;HTML&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;Our TodoList is a bit simpler in that it doesn&amp;rsquo;t have any handlers, but a better illustration of using PropTypes since it has access to the global state of the todos.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import PropTypes from &amp;#39;prop-types&amp;#39;;function TodoList(props) { return ( &amp;lt;ul&amp;gt; {props.todos.map(todo =&amp;gt; ( &amp;lt;li key={todo.id}&amp;gt; {todo.todo_item} &amp;lt;button onClick={() =&amp;gt; props.onDeleteTodo(todo.id)}&amp;gt; Done &amp;lt;/button&amp;gt; &amp;lt;/li&amp;gt; ))} &amp;lt;/ul&amp;gt; )}TodoList.propTypes = { todos: PropTypes.array.isRequired, onDeleteTodo: PropTypes.func.isRequired};export default TodoList;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;You would not sensibly reach for React for a project this size - the complexity of the tooling, and the fact that we&amp;rsquo;re now shipping 150K of Javascript to do something we were nicely achieving in 2K of vanilla, or 15K with htmx makes me deeply uncomfortable. Nevertheless, hundreds of thousands of developers can&amp;rsquo;t be wrong - React&amp;rsquo;s component model is a powerful one for building modern single page applications, especially when it allows you to pull in components from public or corporate collections.&lt;/p&gt;
&lt;p&gt;I plan on doing some more React - partly because its just such big part of the webdev world, and partly because I&amp;rsquo;d like to get some experience with TypeScript, so I&amp;rsquo;m fishing around for a medium size project to play with both of these technologies.&lt;/p&gt;
&lt;p&gt;(&lt;a href="https://github.com/IanKulin/todo/tree/react"&gt;source&lt;/a&gt;)&lt;/p&gt;</description></item><item><title>htmx - A To Do Example</title><link>https://blog.iankulin.com/htmx-a-to-do-example/</link><pubDate>Fri, 05 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/htmx-a-to-do-example/</guid><description>&lt;img src="https://blog.iankulin.com/images/0-eawgkaegdkhvqwcg.png" width="1000" alt=""&gt;
&lt;p&gt;HTMX is an interesting project to me, and I&amp;rsquo;ve used it a bit in my large collection of 70% completed side projects, but haven&amp;rsquo;t really discussed it here. The plan for this post is to talk briefly about what it is exactly, then convert a simple &amp;lsquo;conventional&amp;rsquo; (HTML/CSS/Javascript) app to htmx and think about some the differences.&lt;/p&gt;
&lt;h3 id="htmx"&gt;htmx&lt;/h3&gt;
&lt;p&gt;You could (I recommend you do) read the &lt;a href="https://hypermedia.systems/book/contents/"&gt;book&lt;/a&gt; about the concepts behind &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt;. Carson Gross (the man behind htmx) calls it a book, but its quite the treatise, it could fairly be called a manifesto.&lt;/p&gt;
&lt;p&gt;The book points out that the &amp;lsquo;hyper&amp;rsquo; bit of hypertext markup language, is currently limited to a couple of tags - &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The anchor tag sends a request to the server that says something like &amp;lsquo;fetch the HTML from this URL and completely replace the current view with it&amp;rsquo;&lt;/li&gt;
&lt;li&gt;The form tag with a &lt;code&gt;post&lt;/code&gt; method says to the server &amp;lsquo;do something with this data&amp;rsquo;, or with the &lt;code&gt;get&lt;/code&gt; method, &amp;lsquo;get me the thing I&amp;rsquo;m describing&amp;rsquo;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the first paradigm shift in htmx is &lt;em&gt;&amp;lsquo;why don&amp;rsquo;t we give all html tags the superpower to make server calls&lt;/em&gt;?&lt;em&gt;&amp;rsquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Of course, we can do this in JavaScript - call any endpoint with &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;patch&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt; etc, then add listeners for the code to many parts of the DOM, but Carson has imagined (and then implemented) an alternative timeline where HTML kept being developed to have this capability without the developer having to leave their beloved HTML.&lt;/p&gt;
&lt;p&gt;The other &amp;lsquo;big thing&amp;rsquo; is what the server returns, and what we do with it. We&amp;rsquo;re used to just getting data (JSON these days, but XML in the days when senior devs had big beards) then the front-end JavaScript needing to know about the data and it&amp;rsquo;s format and the intent so it can present it, and the affordances (options for the user to do things with it) available. These things (the presented data and the affordances) combine to represent the application state.&lt;/p&gt;
&lt;p&gt;This is an old concept from the birth of &lt;a href="https://en.wikipedia.org/wiki/HATEOAS"&gt;REST&lt;/a&gt; with a terrible acronym HATEOAS - &amp;ldquo;Hypermedia as the engine of application state&amp;rdquo;. Just passing some JSON (which is usually regarded as a REST practice) breaks this constraint. Instead, we&amp;rsquo;d pass back (in practical terms) HTML that contains the data and it&amp;rsquo;s affordances (the books sometimes calls this the hypermedia &lt;em&gt;representation&lt;/em&gt; of the data). In my Todo app, this might be the to do items, in a list, with the button to mark them as done.&lt;/p&gt;
&lt;p&gt;A key benefit of HATEOAS at it&amp;rsquo;s inception was that the server in this relationship could change business logic without any change to the client end. That argument can still be made to some extent, but a more important benefit of HATEOAS for htmx is that it means so little processing needs done in the client, we don&amp;rsquo;t need a programming language beyond what we&amp;rsquo;ve got in html/x. This is the second big thing in htmx:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Returning application state (data plus affordances) in HTML from the server means we don&amp;rsquo;t need an extra programming language to process it.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="in-practice"&gt;In practice&lt;/h3&gt;
&lt;p&gt;So what does this all mean in practice?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In htmx, HTML tags get &lt;em&gt;attributes&lt;/em&gt; if they need to talk to the server. The attributes say what endpoint they are hitting, with what method, and where to put the returned HTML.&lt;/li&gt;
&lt;li&gt;The server responds with chunks of HTML.&lt;/li&gt;
&lt;li&gt;The client slots that in where it&amp;rsquo;s supposed to go.&lt;/li&gt;
&lt;li&gt;You can write SPA type applications where a part of the page can be updated without a full refresh, without any Javascript*&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This last point explains some of the keen interest of htmx from the non-JavaScript language people. If you&amp;rsquo;re a Python, Go or Ruby developer with low love for JS, this is an easy sell.&lt;/p&gt;
&lt;h3 id="traditional-version"&gt;Traditional Version&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-22-at-7.47.24-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I want to show you a demo htmx app, but first let&amp;rsquo;s look at the Javascript version. It is the Todo app from day one of that coding Udemy you never finished. There&amp;rsquo;s a list of items to do, shown sequentially on the screen. Each one has a button to mark it as done, and at the bottom, a spot to enter a new one.&lt;/p&gt;
&lt;p&gt;My &amp;lsquo;basic&amp;rsquo; Javascript version is based around a Node/Express server. Express serves the .html, .css &amp;amp; .js statically, then runs an API for creating, reading and deleting the Todo items as JSON.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- index.html for simple todo app that uses node endpoints to process json--&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lang&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;charset&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;UTF-8&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;Todos&amp;lt;/&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;./styles.css&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- Main section--&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;To do&amp;lt;/&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;ul&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;todos_list&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;!-- the list of todo items from the database gets inserted here--&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;!-- form to add a todo --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;form&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;action&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/todos&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;text&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;required&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt;&amp;gt;Add&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;form&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;index.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing fancy there. The Todo items will go in the &lt;ul&gt; once we&amp;rsquo;ve got them. The endpoints won&amp;rsquo;t really surprise you either. We&amp;rsquo;re using SQLite for the persistence. There&amp;rsquo;s a &lt;code&gt;get /todos&lt;/code&gt; to get the whole list, a &lt;code&gt;post&lt;/code&gt; to add one, and a &lt;code&gt;delete&lt;/code&gt; to remove one.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Simple Express ToDo app using SQLite3
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;port&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;sqlite3&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;verbose&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Database&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;db/todos.sqlite&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;all&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SELECT * FROM todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;rows&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;rows&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Missing todo_item field in request body&amp;#39;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;INSERT INTO todos (todo_item) VALUES (?)&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// well behaved APIs return the newly created resource
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SELECT * FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;lastID&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;201&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;delete&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos/:id&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;DELETE FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;end&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// if it doesn&amp;#39;t exist, create the table
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, todo_item TEXT)&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// close the database gracefully on exit
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;on&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SIGINT&amp;#39;&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Database closed.&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exit&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// start the server on port 3000 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;, () =&amp;gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Todo app listening on http://localhost:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All the work of translating the data into a representation is being done in the Javascript.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// index.js - code for simple todo app
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// json data served from node endpoint
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todoItemText&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;createElement&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;li&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;button&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;createElement&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;button&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;button&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;innerHTML&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Done&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;button&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;click&amp;#39;&lt;/span&gt;, () =&amp;gt; &lt;span style="color:#a6e22e"&gt;handleDelete&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Create a text node with the todo text and append it to the li
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;todoTextNode&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;createTextNode&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todoItemText&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todoTextNode&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Then append the delete button
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;button&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleDelete&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos/&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;DELETE&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;remove&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Fetch the todo items to build the list
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;res&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todos&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Loop through todos and add to list
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;todos&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;forEach&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todo&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todo&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;todo&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todos_list&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// handler for adding a todo item
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;form&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;preventDefault&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todo&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;headers&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Content-Type&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;application/json&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;res&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todos_list&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todo&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, there&amp;rsquo;s no startling innovation. When it loads, the API is called to get the list of todo items which are turned into list items with &amp;lsquo;Done&amp;rsquo; buttons and appended to the &lt;ul&gt;. Then there&amp;rsquo;s some code for adding a new item. We&amp;rsquo;ll come back to some of this in more detail later when we look at the htmx.&lt;/p&gt;
&lt;h3 id="htmx-version"&gt;htmx Version&lt;/h3&gt;
&lt;h4 id="indexhtml"&gt;index.html&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;&amp;lt;head&amp;gt; &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt; &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&amp;gt; https://unpkg.com/htmx.org/dist/htmx.min.js &amp;lt;title&amp;gt;Todos&amp;lt;/title&amp;gt; &amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;./styles.css&amp;#34;&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;!-- Main section--&amp;gt; &amp;lt;main&amp;gt; &amp;lt;h1&amp;gt;To do&amp;lt;/h1&amp;gt; &amp;lt;ul id=&amp;#34;todos_list&amp;#34; hx-get=&amp;#34;/todos&amp;#34; hx-trigger=&amp;#34;load&amp;#34;&amp;gt;&amp;lt;/ul&amp;gt; &amp;lt;!-- the list of todo items from the database gets inserted here--&amp;gt; &amp;lt;!-- form to add a todo --&amp;gt; &amp;lt;form hx-post=&amp;#34;/todos&amp;#34; hx-target=&amp;#34;#todos_list&amp;#34; hx-swap=&amp;#34;beforeend&amp;#34; hx-on::after-request=&amp;#34;this.reset()&amp;#34;&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; required&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; &amp;lt;/main&amp;gt;                  &amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The script tag at the top pulls in html (which is actually just 14K of gzipped Javascript) from a CDN. You can alternatively download it and serve it statically with your other assets. It&amp;rsquo;s worth noting, that&amp;rsquo;s all the tooling involved - no build tools etc.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;ul id=&amp;#34;todos_list&amp;#34; hx-get=&amp;#34;/todos&amp;#34; hx-trigger=&amp;#34;load&amp;#34;&amp;gt;&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is the unordered list I want the todo items to go into. When the page is loaded (hx-trigger) we&amp;rsquo;ll hit the &lt;code&gt;app.get(&amp;quot;/todos&amp;quot;)&lt;/code&gt; endpoint (hx-get). I don&amp;rsquo;t need to specify where the returned html goes to - by default it&amp;rsquo;s the innerHTML of the calling tag.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;form hx-post=&amp;#34;/todos&amp;#34; hx-target=&amp;#34;#todos_list&amp;#34; hx-swap=&amp;#34;beforeend&amp;#34; hx-on::after-request=&amp;#34;this.reset()&amp;#34;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is the form for adding a new todo item. It&amp;rsquo;s going to hit the &lt;code&gt;app.post(&amp;quot;/todos&amp;quot;)&lt;/code&gt; endpoint (hx-post), and the returned HTML (which will be the the new list item to add to the list) needs to go onto the unordered list we talked about earlier (hx-target). The hx-swap=&amp;ldquo;beforeend&amp;rdquo; part means the returned list item will be inserted just before the end of the &lt;ul&gt; - ie as the last item in the list.&lt;/p&gt;
&lt;p&gt;After the user has hit return or the &amp;lsquo;Add&amp;rsquo; button to save their todo item, I don&amp;rsquo;t want the text they just entered to be sitting there, so a tiny Javascript snippet needs to be run. There are a heap of html hooks for these sorts of jobs (hx-on::afterrequest).&lt;/p&gt;
&lt;p&gt;The final change is that we&amp;rsquo;ve removed the script tag at the bottom that referred to our own Javascript code. None of that is needed now - the application state is delivered as complete HTML by the server.&lt;/p&gt;
&lt;h4 id="serverjs"&gt;server.js&lt;/h4&gt;
&lt;p&gt;Now our node/express server. I&amp;rsquo;ll dump the whole file here, then talk about each part.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Simple Express ToDo app using SQLite3 &amp;amp; HTMX
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;port&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;sqlite3&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;verbose&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Database&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;db/todos.sqlite&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;urlencoded&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;extended&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; }));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;htmlForTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;uid&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;item_text&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;item_text&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&amp;lt;button hx-delete=&amp;#34;todos/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;uid&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; hx-target=&amp;#34;closest li&amp;#34; `&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;hx-swap=&amp;#34;outerHTML&amp;#34;&amp;gt;Done&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;all&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SELECT * FROM todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;rows&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// loop through the rows and create a list item for each
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;list&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;rows&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;forEach&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;row&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;list&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;htmlForTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;list&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Missing todo_item field in request body&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;INSERT INTO todos (todo_item) VALUES (?)&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// return just this item for HTMX to insert at the list end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;htmlForTodoItem&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;lastID&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;delete&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos/:id&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;DELETE FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;end&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// if it doesn&amp;#39;t exist, create the table
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, todo_item TEXT)&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// close the database gracefully on exit
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;on&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SIGINT&amp;#39;&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Database closed.&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exit&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// start the server on port 3000 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;, () =&amp;gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Todo app listening on http://localhost:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first change in the top of the file is that we&amp;rsquo;ve removed the middleware for JSON request bodies and switched to URLEncoded which is what htmx will be sending us by default. Then we dive into this function which builds the HTML for each of the Todo items encapsulated in an &lt;li&gt; with it&amp;rsquo;s &amp;lsquo;Done&amp;rsquo; button to delete it.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function htmlForTodoItem(uid, item_text) { let html = `&amp;lt;li&amp;gt;${item_text}`; html += `&amp;lt;button hx-delete=&amp;#34;todos/${uid}&amp;#34; hx-target=&amp;#34;closest li&amp;#34; `; html += &amp;#39;hx-swap=&amp;#34;outerHTML&amp;#34;&amp;gt;Done&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;&amp;#39;; return html;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s my habit to name these little fragments like this - htmlForXXXX - and group them at the top of the file. I used to use EJS templates, and that&amp;rsquo;s a valid approach, but the functions seem less complicated somehow.&lt;/p&gt;
&lt;p&gt;The remaining code is our endpoints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app.get('/todos'&lt;/code&gt; - called when the page loads. Calls the htmlForTodoItem() for each todo item in the database, the returns all of that to be inserted into the &lt;UL&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.post('/todos'&lt;/code&gt; - for adding a single new todo item. It saves it in the database, then returns the new item as an &lt;li&gt; to be inserted at the end of the list.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.delete('/todos/:id&lt;/code&gt;&amp;rsquo; - this is the route called by the &amp;lsquo;Done&amp;rsquo; button on each todo item. It deletes the item from the database and pointedly returns nothing with that &lt;code&gt;res.status(200).end();&lt;/code&gt; - this is important, because the way the &lt;li&gt; is being deleted from the page is that it&amp;rsquo;s being replaced with what is returned - ie nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These match up to the original versions, with the only significant difference being that instead of returning raw JSON, the HTML is being returned.&lt;/p&gt;
&lt;h3 id="reflections"&gt;Reflections&lt;/h3&gt;
&lt;p&gt;As far as I can see, these two apps are identical to the user. The htmx version is going to be a bit larger over the wire with that initial pull down of 14K, but we&amp;rsquo;re only saving 1.6K of Javascript by eliminating our index.js. That 14K is a big deal in a tiny app like this, but probably not for any serious app.&lt;/p&gt;
&lt;p&gt;In regard to the developer experience; Is the htmx version easier to live with and maintain? For me, I think the answer is yes - I&amp;rsquo;d rather think at the level of &amp;lsquo;add this html to the end of the list&amp;rsquo; rather than &amp;lsquo;document query selector appendtochild&amp;rsquo; then programmatically build a list item from the JSON i&amp;rsquo;m interpreting , so it&amp;rsquo;s a useful abstraction. I acknowledge this is going to be a highly subjective thing.&lt;/p&gt;
&lt;p&gt;The killer usecase for htmx is going to be for people who want a bunch of cool modern stuff in their web apps, but who don&amp;rsquo;t want to write frontend Javascript. So for Go, Rust, PHP, Ruby etc people it&amp;rsquo;s probably a no-brainer. This is probably also my situation, I&amp;rsquo;m a strong server-side Javascript programmer, but have little interest in learning all the cool stuff around the DOM.&lt;/p&gt;
&lt;p&gt;htmx might appeal to developers with a &lt;a href="https://benhoyt.com/writings/the-small-web-is-beautiful/"&gt;small-web&lt;/a&gt; flavour to their dev-politics. If you like semantic html, accessibility, reducing bandwidth and power consumption of your apps, and being a good guardian of your users&amp;rsquo; data on the servers you control, you&amp;rsquo;ll probably like htmx. If you like AWS lambda functions, Angular, Next, Vercel and outsourcing your auth to Octa, htmx may not be your thing.&lt;/p&gt;
&lt;p&gt;The type of app is going to be a major consideration when deciding if htmx is an appropriate choice. If you are writing the next Google Sheets, htmx is not going to be able to do that, you need the raw power of JS. If your bread and butter is commercial CRUD apps and you want to make them quicker, avoid page flashes, and have modern UI magic such as search results that update as you type, then htmx is going to be your jam.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not an unrealistic dream that the functionality in htmx becomes part of the HTML specification. The &lt;a href="http://hypermedia.systems/book/contents/"&gt;book&lt;/a&gt; sets out some good arguments for it, and the htmx implementation of that shows how possible and appealing it is. Whether it does or doesn&amp;rsquo;t, I expect to be using a lot in the future.&lt;/p&gt;</description></item><item><title>Testing Node.js apps - Mocha, Chai, and Supertest</title><link>https://blog.iankulin.com/testing-node-js-apps-mocha-chai-and-supertest/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/testing-node-js-apps-mocha-chai-and-supertest/</guid><description>&lt;p&gt;Bruno is a great open source Postman/Insomnia replacement, and I&amp;rsquo;ve been using it for basic tests of my node servers using the built in asserts and loving it. This is pretty great, and I gather it&amp;rsquo;s also possible to go beyond this and &lt;a href="https://docs.usebruno.com/testing/introduction.html"&gt;write tests in JS in Bruno&lt;/a&gt;. I believe it also has the hooks needed to build it into your CI/CD systems.&lt;/p&gt;
&lt;p&gt;Any large project is probably going to benefit from a more comprehensive suit of testing tools, and while I&amp;rsquo;ll still be using Bruno, my serious tests will be managed with these other tools.&lt;/p&gt;
&lt;p&gt;I admit I&amp;rsquo;ve probably put this off a bit longer than I should have - I didn&amp;rsquo;t really want to install four dependencies and learn four different things just to test my endpoints. It turns out that using the tools together is seamless, and setting it all up was trivial.&lt;/p&gt;
&lt;p&gt;Speaking of trivial, here&amp;rsquo;s my brilliant Node app. It has two endpoints, both of which do a bit of maths.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#39;express&amp;#39;);const app = express();const port = 3000;app.use(express.json());// endpoint that takes two numbers and returns their sumapp.post(&amp;#39;/sum&amp;#39;, (req, res) =&amp;gt; { const { a, b } = req.body; res.json({ sum: a + b });});// endpoint that takes two numbers and multiplies themapp.post(&amp;#39;/multiply&amp;#39;, (req, res) =&amp;gt; { const { a, b } = req.body; res.json({ product: a * b });});// start the serverapp.listen(port, () =&amp;gt; { console.log(`Maths server is running at http://localhost:${port}`);});
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="setting-up-your-project-for-testing"&gt;Setting up your project for testing&lt;/h3&gt;
&lt;h4 id="install-the-tools"&gt;Install the tools&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;npm install --save-dev mocha chai supertest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;--save-dev&lt;/code&gt; bit installs them as a development dependencies - they will go in your &lt;code&gt;package.json&lt;/code&gt; and everyone who clones the repo will be working with the same version. Additionally, they won&amp;rsquo;t needlessly be installed when deployed to production.&lt;/p&gt;
&lt;h4 id="export-the-app"&gt;Export the app&lt;/h4&gt;
&lt;p&gt;The testing system needs to be able to control the app a little bit - start it, stop it, and hook into it. To do that, we&amp;rsquo;ll complicate our &lt;code&gt;app.listen&lt;/code&gt; code a bit so that we&amp;rsquo;ve also got a server variable, then we&amp;rsquo;ll export the app and server so out test files can import them. It will end up looking something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let server;if (process.env.NODE_ENV !== &amp;#39;test&amp;#39;) { server = app.listen(3000, () =&amp;gt; console.log(`Maths server is running at http://localhost:${port}`));}module.exports = { app, server };
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This stops the server being started here if we&amp;rsquo;re in test mode, but exports the bits the test framework needs to manage things.&lt;/p&gt;
&lt;h4 id="create-the-test-files"&gt;Create the test files&lt;/h4&gt;
&lt;p&gt;Our JS test code is all going in a &lt;code&gt;test/&lt;/code&gt; directory in our project, and they will all be named &lt;code&gt;&amp;lt;something&amp;gt;.test.js&lt;/code&gt; I usually use the file name of the file I&amp;rsquo;m testing. So today I&amp;rsquo;m writing tests for &lt;code&gt;app.js&lt;/code&gt; my tests will be in &lt;code&gt;apps.test.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Each test file will need to pull in our tools (supertest and chai) and the server and app variables.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s followed by one or more &lt;em&gt;test suites&lt;/em&gt;; each test suite contains one or more &lt;em&gt;test cases&lt;/em&gt;. This might be easier to explain if we look at a real file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const supertest = require(&amp;#39;supertest&amp;#39;);const chai = require(&amp;#39;chai&amp;#39;);const { app, server } = require(&amp;#39;../app&amp;#39;);const expect = chai.expect;describe(&amp;#39;POST / add&amp;#39;, () =&amp;gt; { it(&amp;#39;should return the correct sum&amp;#39;, () =&amp;gt; { return supertest(app) .post(&amp;#39;/sum&amp;#39;) .send({ a: 5, b: 5 }) .expect(200) .then(res =&amp;gt; { expect(res.body.sum).to.equal(10); }); }); it(&amp;#39;should return the correct sum with negative numbers&amp;#39;, () =&amp;gt; { return supertest(app) .post(&amp;#39;/sum&amp;#39;) .send({ a: -5, b: -5 }) .expect(200) .then(res =&amp;gt; { expect(res.body.sum).to.equal(-10); }); });});describe(&amp;#39;POST / multiply&amp;#39;, () =&amp;gt; { it(&amp;#39;should return the correct product&amp;#39;, () =&amp;gt; { return supertest(app) .post(&amp;#39;/multiply&amp;#39;) .send({ a: 5, b: 5 }) .expect(200) .then(res =&amp;gt; { expect(res.body.product).to.equal(25); }); });});server.close();
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This file contains two test suites - &amp;lsquo;POST/add&amp;rsquo; and &amp;lsquo;POST/multiply&amp;rsquo;. POST/add contains two test cases (each begins with &lt;code&gt;it&amp;lt;statement of what the test subject should do&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no end to the tests you can write. I normally do basic functionality as I&amp;rsquo;m writing code, and I also add in tests for anything that emerges as a bug. If you get into the rhythm of bug -&amp;gt; write failing test -&amp;gt; fix bug -&amp;gt; test passes you can have your day punctuated by little doses of dopamine. I often write a timed test - that an endpoint should respond in 10ms. These don&amp;rsquo;t help you when you are developing, but sure will later. You should also check that all of the wrong inputs users will eventually try have been handled. If an API expects a number, check for errors being thrown for strings, for negative numbers, for huge numbers, for decimals, for booleans, for objects etc etc.&lt;/p&gt;
&lt;p&gt;Another thing I will do is use a code coverage tool to check my test covers all the branches and error conditions. I plan to talk about that another day. First I need to show you how to run the tests.&lt;/p&gt;
&lt;h4 id="add-test-script"&gt;Add test script&lt;/h4&gt;
&lt;p&gt;If we had installed mocha globally, we could just call it from the command line with something like:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mocha ./test/app.test.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;But we didn&amp;rsquo;t do that, so we need npm to start it up for us. I know this seems like another time wasting step, but it&amp;rsquo;s one of those do it once, benefit from it thousands of times things.&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;package.json&lt;/code&gt; file, we can add a section called scripts. If you started you project with &lt;code&gt;npm init&lt;/code&gt; you may already have this section, if not, just add it in. It&amp;rsquo;s common to have a &lt;code&gt;run&lt;/code&gt; and a &lt;code&gt;test&lt;/code&gt; script, and I often have one or two others. Here&amp;rsquo;s the sort of thing you want.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{ &amp;#34;name&amp;#34;: &amp;#34;test-demo&amp;#34;, &amp;#34;version&amp;#34;: &amp;#34;0.1.0&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;Simple Maths API&amp;#34;, &amp;#34;main&amp;#34;: &amp;#34;app.js&amp;#34;, &amp;#34;scripts&amp;#34;: { &amp;#34;start&amp;#34;: &amp;#34;node app.js&amp;#34;, &amp;#34;test&amp;#34;: &amp;#34;mocha &amp;#39;./test/*.test.js&amp;#39;&amp;#34; }, &amp;#34;dependencies&amp;#34;: { &amp;#34;express&amp;#34;: &amp;#34;^4.18.2&amp;#34; }, &amp;#34;devDependencies&amp;#34;: { &amp;#34;chai&amp;#34;: &amp;#34;^4.3.10&amp;#34;, &amp;#34;mocha&amp;#34;: &amp;#34;^10.2.0&amp;#34;, &amp;#34;nyc&amp;#34;: &amp;#34;^15.1.0&amp;#34;, &amp;#34;supertest&amp;#34;: &amp;#34;^6.3.3&amp;#34; }}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;npm&lt;/code&gt; does the magic to make the correct version of the library available when this script is run. The end effect of these is that you can type &lt;code&gt;npm test&lt;/code&gt; at the command line, and mocha will run your tests. Let&amp;rsquo;s try it would our tests.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.18.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.18.28-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what we like to see, passing tests. I&amp;rsquo;ll make one fail by telling it to expect 5x5=26.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.22.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.22.53-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it, you&amp;rsquo;re all set up to write tests against your node apps.&lt;/p&gt;
&lt;h3 id="what-do-the-different-bits-do"&gt;What do the different bits do?&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s a lot of moving parts here, lets tease those out a little.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mochajs.org/#getting-started"&gt;mocha&lt;/a&gt; - this is the test framework. As we&amp;rsquo;ve discussed, it&amp;rsquo;s the command line tool that runs the tests and produces the output.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.npmjs.com/package/supertest"&gt;supertest&lt;/a&gt; - manages the connections between the test runner/framework and the code being tested. When I&amp;rsquo;m pressing a button in Bruno, it&amp;rsquo;s actually hitting localhost:3000 to exercise the server which I&amp;rsquo;ve previously started. supertest is doing magic to make that connection without going through the network layers.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.chaijs.com"&gt;chai&lt;/a&gt; - it provides the assert()s, expect()s and should()s that we use in the test cases. You could, in theory make do with the assert() library built into node - especially for our toy demo app - but it&amp;rsquo;s no where near as nice, and in particular chai has a massive set of plugins that both extend it&amp;rsquo;s use generally, but also into working at a detailed level with other vendor packages.&lt;/p&gt;</description></item><item><title>Simple SQLite in Express</title><link>https://blog.iankulin.com/simple-sqlite-in-express/</link><pubDate>Thu, 28 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/simple-sqlite-in-express/</guid><description>&lt;p&gt;I don&amp;rsquo;t have experience with &lt;a href="https://www.sqlite.org/index.html"&gt;SQLite&lt;/a&gt; and want to shift one of my apps over from Mongoose since apparently SQLite is &lt;a href="https://www.sqlite.org/whentouse.html"&gt;much more capable&lt;/a&gt; than I imagined. My usual tactic when trying something new is to try and get a minimal project working on it, so what follows is the simplest possible node/express REST API to demo SQLite.&lt;/p&gt;
&lt;p&gt;The simplest possible Express app is going to look something like this. Of course we would have gone to the terminal with &lt;code&gt;npm i express&lt;/code&gt; first so this could run.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#39;express&amp;#39;);const app = express();const port = 3000;app.get(&amp;#39;/&amp;#39;, (req, res) =&amp;gt; { res.send(&amp;#39;Hello, World!&amp;#39;);});app.listen(port, () =&amp;gt; { console.log(`Server is running at http://localhost:${port}`);});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The only thing to add to this for the moment is some middleware to allow Express to parse JSON body payloads.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.use(express.json());
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="body-v-query"&gt;Body v Query&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;ll just take you on a short detour here if you&amp;rsquo;re not familiar with HTTP requests. There&amp;rsquo;s a couple of common ways to send some data along with a request. The oldest one is to shove it all in the URL. You will have seen these sorts of things:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://localhost:3000/adduser?name=Fred&amp;amp;email=fred@example.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If you want all of your data to fit in a link, these are great. They can be bookmarked and so on. You see them all the time - especially in links with heaps of tracking data. The &amp;lsquo;?&amp;rsquo; denotes it as a query.&lt;/p&gt;
&lt;p&gt;The code to process the GET request above would look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#39;/adduser&amp;#39;, (req, res) =&amp;gt; { const name = req.query.name; const email = req.query.email;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that I&amp;rsquo;ve used a GET request here, when semantically a POST would make more sense. That&amp;rsquo;s because you can only use query strings for GETs.&lt;/p&gt;
&lt;p&gt;Often the data you want to pass is going to be more complex, or you don&amp;rsquo;t want it in the URL for other reasons (for example, you wouldn&amp;rsquo;t want a user to be able to bookmark a record delete request). In that case you use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/body"&gt;body&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can stuff all sorts of text data in the body. Most times you are going to want JSON. I do for this demo, so that&amp;rsquo;s why I&amp;rsquo;ve added the &lt;code&gt;expresss.json()&lt;/code&gt; middleware - all the hard work will be done for me and I can just do this to access the information that&amp;rsquo;s passed as part of the request:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#39;/users&amp;#39;, (req, res) =&amp;gt; { const name = req.body.name; const email = req.body.email;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="adding-sqlite"&gt;Adding SQLite&lt;/h3&gt;
&lt;p&gt;Once you&amp;rsquo;ve run &lt;code&gt;npm i sqlite3&lt;/code&gt; at the terminal to install the package, at the top of our app somewhere - probably where we&amp;rsquo;re requiring the other packages - we&amp;rsquo;ll need this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const sqlite3 = require(&amp;#39;sqlite3&amp;#39;).verbose();const db = new sqlite3.Database(&amp;#39;db/test.sqlite&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is just requiring the package, and giving us &lt;code&gt;db&lt;/code&gt; as the variable for the SQLite database connection. The database is actually a file in the &lt;code&gt;db/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;On the first run, obviously this will be empty, so somewhere before the listen command, we need to create a table.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// if the &amp;#39;users&amp;#39; table doesn&amp;#39;t exist, // create it with &amp;#39;name&amp;#39; and &amp;#39;email&amp;#39; columnsdb.run(&amp;#39;CREATE TABLE IF NOT EXISTS users (name TEXT, email TEXT)&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you&amp;rsquo;ve never encountered SQL before, and was expecting a bunch of methods being passing in structs to do this, this is going to be alarming. But no - in SQL we do things by sending the engine a string. This has created a massive &lt;a href="https://en.wikipedia.org/wiki/SQL_injection"&gt;attack surface&lt;/a&gt;, but it&amp;rsquo;s also a convenient and very readable convention.&lt;/p&gt;
&lt;p&gt;For our simple purposes today, that could be enough infrastructure for SQLite, but because we are good programmers, we&amp;rsquo;ll correctly close the database when the app undergoes an orderly shutdown by adding this before the &lt;code&gt;app.listen&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// close the database connection when the app is shutting downprocess.on(&amp;#39;SIGINT&amp;#39;, () =&amp;gt; { db.close((err) =&amp;gt; { if (err) { console.error(&amp;#39;Error closing SQLite database:&amp;#39;, err.message); } else { process.exit(0); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="working-with-sqlite"&gt;Working with SQLite&lt;/h3&gt;
&lt;p&gt;That&amp;rsquo;s the infrastructure out of the way. Now, onto our CRUD (create, read, update, delete) operations to manipulate our stored data. Since this is just a demo, the simplest way to show these is just to have endpoints for each one. To exercise these endpoints you could use the development tools in your browser, but most people will use an API testing tool like Postman, or Insomnia. I much prefer &lt;a href="https://blog.iankulin.com/we-need-to-talk-about-bruno/"&gt;Bruno&lt;/a&gt; for this job, so I&amp;rsquo;ll use that (and suggest you do too, get it &lt;a href="https://www.usebruno.com/"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id="create-add"&gt;Create (add)&lt;/h4&gt;
&lt;p&gt;We already started on that earlier, and explained the concept of passing in data via the &amp;lsquo;body&amp;rsquo; here&amp;rsquo;s the complete thing:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// endpoint to add a user record to the users table// expects a name and email in the JSON body of the requestapp.post(&amp;#39;/users&amp;#39;, (req, res) =&amp;gt; { const name = req.body.name; const email = req.body.email; const sql = `INSERT INTO users (name, email) VALUES (&amp;#34;${name}&amp;#34;, &amp;#34;${email}&amp;#34;)`; db.run(sql, function(err) { if (err) { res.status(500).send(err.message); } else { res.status(201).json({ rowid: this.lastID }); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So this code processes a request to our server - something like &lt;code&gt;http://localhost:3000/users&lt;/code&gt; and expects the body payload to contain some JSON with a name and email. It could look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{ &amp;#34;name&amp;#34;: &amp;#34;John Doe&amp;#34;, &amp;#34;email&amp;#34;: &amp;#34;john.doe@example.com&amp;#34;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And when run in Bruno:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.24.54-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.24.54-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="read"&gt;Read&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s a couple of reads we can do, one where all the data is returned, and one where only a specific record is. Let&amp;rsquo;s do the big one first, since we&amp;rsquo;ll use it a lot while we&amp;rsquo;re writing the rest!&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// endpoint to get all users from the users table in the databaseapp.get(&amp;#39;/users&amp;#39;, (req, res) =&amp;gt; { const sql = &amp;#39;SELECT rowid, * FROM users&amp;#39;; db.all(sql, (err, rows) =&amp;gt; { if (err) { res.status(500).send(err.message); } else { res.status(200).send(rows); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which looks like this in Bruno:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.35.56-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.35.56-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Or if we just want one in particular, we&amp;rsquo;ll pass the id in the URL.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// endpoint to get a single user from the users table in the database// expects an id in the URLapp.get(&amp;#39;/user/:id&amp;#39;, (req, res) =&amp;gt; { const id = req.params.id; const sql = `SELECT rowid, * FROM users WHERE rowid = ${id}`; db.get(sql, (err, row) =&amp;gt; { if (err) { res.status(500).send(err.message); } else { res.status(200).send(row); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.55.25-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.55.25-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="update"&gt;Update&lt;/h4&gt;
&lt;p&gt;You&amp;rsquo;re probably getting the hang of this now.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// endpoint to update a user record in the users table in the database// expects a name, and email in the JSON body of the request// expects an id in the URLapp.put(&amp;#39;/user/:id&amp;#39;, (req, res) =&amp;gt; { const id = req.params.id; const name = req.body.name; const email = req.body.email; const sql = `UPDATE users SET name = &amp;#34;${name}&amp;#34;, email = &amp;#34;${email}&amp;#34; WHERE rowid = ${id}`; db.run(sql, (err) =&amp;gt; { if (err) { res.status(500).send(err.message); } else { res.status(200).send(&amp;#39;User updated.&amp;#39;); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="delete"&gt;Delete&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// endpoint to delete a user record from the users table in the database// expects an id in the URL. Doesn&amp;#39;t complain if the id doesn&amp;#39;t exist.app.delete(&amp;#39;/user/:id&amp;#39;, (req, res) =&amp;gt; { const id = req.params.id; const sql = `DELETE FROM users WHERE rowid = ${id}`; db.run(sql, (err) =&amp;gt; { if (err) { res.status(500).send(err.message); } else { res.status(200).send(); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="hardening"&gt;Hardening&lt;/h3&gt;
&lt;p&gt;To keep things simple (since I was just trying to show basic examples of using sqlite) I used string interpolation when making the SQL to run against the database. That&amp;rsquo;s not a great technique because of the danger of SQL injection; so we should routinely use parameterized queries instead. Here&amp;rsquo;s how adding a user looks if we use parameterized queries:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// endpoint to add a user record to the users table// expects a name and email in the JSON body of the requestapp.post(&amp;#39;/users&amp;#39;, (req, res) =&amp;gt; { const name = req.body.name; const email = req.body.email; const sql = `INSERT INTO users (name, email) VALUES (?, ?)`; db.run(sql, [name, email], function(err) { if (err) { res.status(500).send(err.message); } else { res.status(201).json({ rowid: this.lastID }); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With parameterized queries, whatever the user passes in ends up in the database rather than being executed as part of the query string.&lt;/p&gt;
&lt;p&gt;For example, imagine if an API user tried to add a user with this body:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{ &amp;#34;name&amp;#34;: &amp;#34;John&amp;#34;, &amp;#34;email&amp;#34;: &amp;#34;john@example.com\&amp;#34;); DROP TABLE users; --&amp;#34;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then my original add code:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#39;/users&amp;#39;, (req, res) =&amp;gt; { const name = req.body.name; const email = req.body.email; const sql = `INSERT INTO users (name, email) VALUES (&amp;#34;${name}&amp;#34;, &amp;#34;${email}&amp;#34;)`; db.run(sql, function(err) { if (err) { res.status(500).send(err.message); } else { res.status(201).json({ rowid: this.lastID }); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;would result in executing this against the database:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;INSERT INTO users (name, email) VALUES (&amp;#34;John&amp;#34;, &amp;#34;john@example.com&amp;#34;); DROP TABLE users; --&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;which would delete the entire users table from the database. This is &lt;a href="https://xkcd.com/327/"&gt;widely known as the &amp;ldquo;Bobby Tables&amp;rdquo; problem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With the parameterized version, you just end up with an ugly user record.&lt;/p&gt;
&lt;p&gt;Changing all of these doesn&amp;rsquo;t add much code, but does make it a little bit harder to follow, hence showing you the old version first.&lt;/p&gt;
&lt;h3 id="rest-api-conventions"&gt;REST API conventions&lt;/h3&gt;
&lt;p&gt;You may have noticed in this code I&amp;rsquo;ve used a variety of HTTP request types - GET, POST, PUT, DELETE etc. There&amp;rsquo;s no rules for these things, but if someone else (including future you) is going to have to maintain or use your API, it&amp;rsquo;s a good idea to follow the conventions.&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Situation&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Request&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;URL&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Return&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Add a record&lt;/td&gt;&lt;td&gt;POST&lt;/td&gt;&lt;td&gt;/users&lt;/td&gt;&lt;td&gt;record id&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Replace a whole record&lt;/td&gt;&lt;td&gt;PUT&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;the whole record&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Replace part of a record&lt;/td&gt;&lt;td&gt;PATCH&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;the whole record&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Get all the records&lt;/td&gt;&lt;td&gt;GET&lt;/td&gt;&lt;td&gt;/users&lt;/td&gt;&lt;td&gt;all the records&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Get a particular record&lt;/td&gt;&lt;td&gt;GET&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;that record&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Delete a record&lt;/td&gt;&lt;td&gt;DELETE&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;nothing&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;You might have noticed that I haven&amp;rsquo;t done the PATCH - the difference between that and the PUT is that with the PATCH we don&amp;rsquo;t supply the whole record, just the fields we want to change. I&amp;rsquo;m not going to worry about that for this API since our record is so small.&lt;/p&gt;
&lt;p&gt;But I also don&amp;rsquo;t return the whole record after a PUT. Unfortunately, it means a second request - but there&amp;rsquo;s probably not much of a performance hit since it will be in the cache.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// endpoint to update a user record in the users table in the database// expects a name, and email in the JSON body of the request// expects an id in the URLapp.put(&amp;#39;/user/:id&amp;#39;, (req, res) =&amp;gt; { const id = req.params.id; const name = req.body.name; const email = req.body.email; const updateSql = `UPDATE users SET name = ?, email = ? WHERE rowid = ?`; db.run(updateSql, [name, email, id], (err) =&amp;gt; { if (err) { res.status(500).send(err.message); } else { const selectSql = `SELECT rowid, * FROM users WHERE rowid = ?`; db.get(selectSql, [id], (err, row) =&amp;gt; { if (err) { res.status(500).send(err.message); } else { res.status(200).json(row); } }); } });});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://github.com/IanKulin/sqlite-rest-demo/blob/main/app.js"&gt;Link to the completed project on Github&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Using LXC templates in Proxmox</title><link>https://blog.iankulin.com/using-lxc-templates-in-proxmox/</link><pubDate>Sun, 24 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-lxc-templates-in-proxmox/</guid><description>&lt;p&gt;I wrote a couple of weeks ago about a &lt;a href="https://blog.iankulin.com/new-self-hosted-service-workflow/"&gt;standard workflow&lt;/a&gt; I use to spin up a web service in an LXC container to add to my self-hosted collection of services. It went a bit like: do this, and then this, then this other thing. Whenever you find yourself repeating a set of steps like this, it&amp;rsquo;s usually a sign that you should be automating it. Not just to save time (although this is a key benefit) but also to improve repeatability and to avoid introducing errors.&lt;/p&gt;
&lt;p&gt;In Proxmox, this particular task is easily systematized using container &lt;em&gt;templates&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The simplest way to think of a container template is that it&amp;rsquo;s just a one-for-one snapshot of a container (ie the disk image, the configuration file that contains all the VM hardware information) all squashed up into a tarball - basically the same as a backup. This is then copied to create new containers.&lt;/p&gt;
&lt;p&gt;If we create new containers from a template, all the software and configuration that was in the template will be present in the new container. This is obviously the desired behaviour, but it presents some issues - we probably don&amp;rsquo;t want multiple containers with the same host name, or MAC address, or SSH host keys. Some of these issues Proxmox will sort out for us, some we&amp;rsquo;ll need to tidy up manually.&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Issue&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Host name&lt;/td&gt;&lt;td&gt;When you 'clone' the template in Proxmox, it will ask you the new host name.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MAC address&lt;/td&gt;&lt;td&gt;Proxmox just creates a new one with no input needed from you.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Machine ID&lt;/td&gt;&lt;td&gt;If you truncate it in the template before you save it as a template, a new one will be created then the container is.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SSH host keys&lt;/td&gt;&lt;td&gt;Manually delete them in the template before saving the template, then manually re-create them in the new container once it's booted up.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id="making-the-template"&gt;Making the template&lt;/h3&gt;
&lt;p&gt;Create an LXC container as normal - ie chose &amp;ldquo;Create CT&amp;rdquo; in Proxmox, give it a name, choose a password, then a template, make the decisions about memory, disk, networking etc. Note that when you are choosing an official template to create it from (Apline, Debian, Ubuntu etc) , these files are almost identical to what we&amp;rsquo;ll be creating in this process.&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s up and running, I &lt;code&gt;ssh&lt;/code&gt; in and run all my apt updates and install any software or make any other changes. For me this includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Making it a client of &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;my local apt-cache&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;running ssh update and upgrades&lt;/li&gt;
&lt;li&gt;Copying in my SSH keys (ssh-copy-id)&lt;/li&gt;
&lt;li&gt;Installing sudo and adding myself as a sudo user&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/engine/install/debian/"&gt;Installing Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1174/install-debian-bookworm/"&gt;Installing Tailscale,&lt;/a&gt; and doing the &lt;a href="https://blog.iankulin.com/getting-tailscale-working-in-lxc-containers/"&gt;Tailscale LXC fix&lt;/a&gt; (but not running &lt;code&gt;tailscale up&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Installing &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;my simple machine status server&lt;/a&gt; that&amp;rsquo;s used for monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s all done, we&amp;rsquo;ve got a nice clean container, but with all the software and config that we need for most future containers.&lt;/p&gt;
&lt;p&gt;Now we need to address a couple of the issues that could be caused by cloning this LXC from the table above.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Machine ID - you could probably get away with not worrying about this, but might run into a confusing issue later. A simple &lt;code&gt;sudo truncate -s 0 /etc/machine-id&lt;/code&gt; will nuke it, then a new unique one will be created when the clone container boots up.&lt;/li&gt;
&lt;li&gt;SSH host keys - you know when you ssh into a new system for the first time and OpenSSH asks you if you&amp;rsquo;re sure you want to recognise this server? This is done by the server identifying itself with one of these keys. If these are left the same for all of the clones of our template, you&amp;rsquo;ll have to be constantly deleting the keys out of your &lt;code&gt;known_hosts&lt;/code&gt; file. We can delete them now (which will make this template and any clones impossible to &lt;code&gt;ssh&lt;/code&gt; into) or later. I choose now. &lt;code&gt;sudo rm /etc/ssh/ssh_host_*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once this is all done, we are ready to convert this container into a template. Shut it down, then if you are cautious, back it up (you can&amp;rsquo;t convert a template back into a container). Then right click on it in Proxmox and choose &amp;lsquo;Convert to Template&amp;quot;. After a few seconds, it will be in your server view as a template with a slightly different icon.&lt;/p&gt;
&lt;h3 id="using-the-template"&gt;Using the template&lt;/h3&gt;
&lt;p&gt;The process of using our new template is called cloning. Right click on the template in Proxmox, and choose clone. You&amp;rsquo;ll be presented with a dialogue to give it a number, choose a host name, select the clone type (you want a &amp;lsquo;full clone&amp;rsquo;) and where this container&amp;rsquo;s storage will be.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-03-at-12.43.10-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-03-at-12.43.10-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A few seconds later the new LXC container will be in your server view and can be started.&lt;/p&gt;
&lt;p&gt;You won&amp;rsquo;t be able to ssh into this container yet as we deleted the host keys. Use the console in Proxmox to log in (with the root or sudo user credentials you set up earlier) and recreate the ssh host keys with &lt;code&gt;sudo dpkg-reconfigure openssh-server&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;While you are here, you should probably change the passwords for both users with &lt;code&gt;passwd&lt;/code&gt; or &lt;code&gt;sudo passwd &amp;lt;username&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The other thing I&amp;rsquo;ll need to do to use my container with Tailscale is to run &lt;code&gt;sudo tailscale up&lt;/code&gt; and complete the steps for that.&lt;/p&gt;
&lt;p&gt;And we&amp;rsquo;re done. You&amp;rsquo;ve now got a container that&amp;rsquo;s identical to our template, except for the things that need to be different. You can go ahead and use it as needed now.&lt;/p&gt;
&lt;h4 id="resources"&gt;Resources&lt;/h4&gt;
&lt;p&gt;Here&amp;rsquo;s a couple of useful things I came across in the writing of this post:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=J29onrRqE_I&amp;amp;t=619s"&gt;Proxmox VE Full Course: Class 8&lt;/a&gt; - Creating Container Templates - video from Jay (Learn Linux TV)&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pve.proxmox.com/wiki/Linux_Container"&gt;Linux Containers&lt;/a&gt; - from the Proxmox docs&lt;/p&gt;</description></item><item><title>Practice your restore strategy</title><link>https://blog.iankulin.com/practice-your-restore-strategy/</link><pubDate>Thu, 21 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/practice-your-restore-strategy/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_7342.jpg" width="1000" alt=""&gt;
&lt;p&gt;My homelab set up is a production node, (pve-prod1) a backup production node (pve-prod2) and a development machine (pve-dev1). They are all G2 800 minis, but pve-prod1 has a i7 6700T and 32GB RAM, where as the other two are i5 6500T with 16GB. My thinking is that the older two can easily share the workload of the main production machine for disaster recovery. Everything is virtualised on top of Proxmox, so sharing up the VM&amp;rsquo;s and containers is trivial.&lt;/p&gt;
&lt;p&gt;Every three or four months, I run the nightly backups, turn off the production machine and restore back on to pve-prod2 and boot everything up. That was today&amp;rsquo;s job, and in the process I discovered a couple of things to address.&lt;/p&gt;
&lt;h3 id="issues"&gt;Issues&lt;/h3&gt;
&lt;p&gt;Issues were minor - everything was up again quite quickly, but they were:&lt;/p&gt;
&lt;h4 id="vm-disk-storage"&gt;VM disk storage&lt;/h4&gt;
&lt;p&gt;VM disk storage - I ran out on pve-prod2. Quite often when pve-prod1 is offline, it gets a new SSD, or most recently and 512GB of NMVE. So there&amp;rsquo;s oodles of room for the VM disks. As a result, I&amp;rsquo;m never mean with the sizes when I&amp;rsquo;m guessing what an application might need. I hate not allocating enough because expanding them is hard.&lt;/p&gt;
&lt;p&gt;Also, I&amp;rsquo;ve been moving docker workloads off the big docker VM and into their own LXC&amp;rsquo;s. But I&amp;rsquo;m still running the VM since it still has a couple of containers. All this adds up to there wasn&amp;rsquo;t enough room on the pve-prod2 SSD for all the VM disks. This is not the end of the world, I can leave the VM disks on the NAS and work over the network - but it&amp;rsquo;s a reminder to me to not let the backup hardware get to far behind the production hardware.&lt;/p&gt;
&lt;p&gt;Of course I could have moved some of these onto pve-dev1 (which is massively overspec&amp;rsquo;d) but I don&amp;rsquo;t really want to power two machines if I can get by with one. I have asked Father Christmas for another 512GB NMVE M2, so I&amp;rsquo;m optimistic this will be solved shortly.&lt;/p&gt;
&lt;h4 id="versions"&gt;Versions&lt;/h4&gt;
&lt;p&gt;After I moved all the VMs and LXCs, I realised I that pve-prod2 is running an old version of Proxmox - it&amp;rsquo;s on 7.4 and the others are on 8.1. Everything works (unless you need dark mode) but it was a mistake on my part, when I&amp;rsquo;d upgraded pve-prod1 I deliberately left prod2 on the old, known good, version but with the intention I&amp;rsquo;d upgrade it in a month or so, then never did.&lt;/p&gt;
&lt;h4 id="lxc-backup-to-nas"&gt;LXC Backup to NAS&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/problems-backing-up-lxc-to-nfs-in-proxmox/"&gt;I&amp;rsquo;ve previously discussed this issue&lt;/a&gt;, where an LXC apparently does not have the require permissions for it&amp;rsquo;s temporary files on an NFS share but does have them for the finished backup. It&amp;rsquo;s a simple config change, but one that I hadn&amp;rsquo;t made to prod2. This is a good case for maintaining a post-proxmox-install Ansible playbook.&lt;/p&gt;
&lt;h3 id="bouquets"&gt;Bouquets&lt;/h3&gt;
&lt;h4 id="proxmox"&gt;Proxmox&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;ve been pondering if I should move away from Proxmox. I imagine I can achieve something similar with some combination of KVM, QEMU, Virt-Manager or Cockpit. I&amp;rsquo;d be learning some new things and be closer to a generic solution. On the other hand, I&amp;rsquo;m still learning about Proxmox, especially the command line stuff as I convert more of the homelab to infrastructure as code.&lt;/p&gt;
&lt;p&gt;Also, it&amp;rsquo;s just worked flawlessly. I was reminded today as I did this now routine task of the first time I moved a VM between two computers how exciting it was - and I was doing that as a noob using the web interface. Proxmox certainly meets all my current needs so I&amp;rsquo;ll be sticking with it. If I&amp;rsquo;m eBay tempted by more iron, I might have a play with some of the other options, but for the moment, I&amp;rsquo;m sticking with it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also conscious that the NAS is filling up (although slowly) and a future improvement would be to start to use the &lt;a href="https://www.proxmox.com/en/proxmox-backup-server/overview"&gt;Proxmox Backup Server&lt;/a&gt;. This delta&amp;rsquo;s your backups to allow a more comprehensive history to be kept while reducing the disk space being used. This will lock me into the Proxmox ecosystem a little more.&lt;/p&gt;
&lt;h4 id="synology"&gt;Synology&lt;/h4&gt;
&lt;p&gt;Also I need to shoutout Synology NAS&amp;rsquo;s. Just super reliable. I yearn for a ZFS solution, but if you just want reliable, gets things done storage for your homelab, they are an excellent choice for most situations. They are not sexy.&lt;/p&gt;
&lt;h4 id="monitoring"&gt;Monitoring&lt;/h4&gt;
&lt;img src="https://blog.iankulin.com/images/img_b42eca952bee-1.jpeg" width="577" alt=""&gt;
&lt;p&gt;A lot of the time I don&amp;rsquo;t really think about my monitoring - which consists or Uptime Kuma hooked up to Ntfy for phone notifications, and a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;custom Go program&lt;/a&gt; that exposes the RAM and disk use on each container and VM.&lt;/p&gt;
&lt;p&gt;But when you power down your production server, and your phone lights up in red, followed by green messages as each service comes back up, that&amp;rsquo;s a good feeling.&lt;/p&gt;
&lt;p&gt;Anyway, here&amp;rsquo;s your reminder to test your backup strategy if you haven&amp;rsquo;t done that for a while. Like me, you might learn something to your advantage.&lt;/p&gt;</description></item><item><title>Gogs, Gitea, Forgejo</title><link>https://blog.iankulin.com/gogs-gitea-forgejo/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gogs-gitea-forgejo/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_7071-1.png" width="640" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve been really pleased with &lt;a href="https://blog.iankulin.com/tags/gogs/"&gt;Gogs&lt;/a&gt; - it&amp;rsquo;s lightweight, was simple to spin up, and has worked perfectly. But then this morning on Mastodon, there&amp;rsquo;s a &lt;a href="https://mastodon.social/@Codeberg@social.anoxinon.de/111471407276450348"&gt;post from @Codeberg.org&lt;/a&gt; describing a security vulnerability in their Git hosting project Forgejo. This issue also apparently affects Gitea and Gogs - what&amp;rsquo;s up with that?&lt;/p&gt;
&lt;p&gt;I actually already did spend a bit of time comparing Gogs and Gitea before deciding on Gogs, since I&amp;rsquo;d heard of people running Gitea over the past year or so, but only seen that Gogs seemed to be popular with self-hosters in a Lemmy post I&amp;rsquo;d read. My first impression was that Gitea was more focused on CI/CD and seemed to have a more complicated install process.&lt;/p&gt;
&lt;p&gt;What I didn&amp;rsquo;t do, was think about the project management and teams. It turns out that &lt;a href="https://about.gitea.com/"&gt;Gitea&lt;/a&gt; was forked from &lt;a href="https://gogs.io/"&gt;Gogs&lt;/a&gt; by contributors in 2016 due to &lt;a href="https://blog.gitea.com/welcome-to-gitea/"&gt;disagreements about the project management&lt;/a&gt;. Then at the end of 2022 &lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt; was forked from Gitea due to &lt;a href="https://forgejo.org/2022-12-15-hello-forgejo/"&gt;Gitea moving the trademarks and domain into a company&lt;/a&gt; providing Gitea support.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://forgejo.org/2023-11-release-v1-20-5-1/"&gt;CVE announcement from Forgeo&lt;/a&gt;, while a little snarky about their ancestors, does give the impression of a functional organisation that&amp;rsquo;s able to deal with issues as they come up. It&amp;rsquo;s a credit to the group to be in that position after just a year, and their &lt;a href="https://codeberg.org/forgejo/forgejo"&gt;repo&lt;/a&gt; (which is dogfooded) seems plenty active.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve only just started on Gogs, so it&amp;rsquo;s still easy to move if that&amp;rsquo;s what I decide. I guess my learning from stumbling upon this security announcement is more that I should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;take into account more than just project features when making these decisions&lt;/li&gt;
&lt;li&gt;I need to be subscribed to the channels where I&amp;rsquo;d learn about security issues in the projects I&amp;rsquo;m using and their major dependencies.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Git - pushing to two remotes</title><link>https://blog.iankulin.com/git-pushing-to-two-remotes/</link><pubDate>Fri, 15 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-pushing-to-two-remotes/</guid><description>&lt;p&gt;I am loving running a local Gogs instance - it&amp;rsquo;s nice pushing my git repos to a totally private hub that I know is backed up with all my other self-hosted infrastructure.&lt;/p&gt;
&lt;p&gt;Of course, there&amp;rsquo;s good reasons to have code in GitHub as well - my build-in-public philosophy, the vague possibility that some of it might be useful to someone, my contribution to our future AI overlords, and when I need to make some code linkable - for example from one of these posts. And of course there&amp;rsquo;s this bit of social-engineering which I assume was inspired by the bathroom decor in &lt;a href="https://i.pinimg.com/originals/94/23/85/9423854153f55938c454a061ad5462fe.gif"&gt;Veronica Mars&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-5.45.50-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Git is an amazing tool, so of course this is possible. Normally my workflow is that I &lt;code&gt;git init&lt;/code&gt; whenever I&amp;rsquo;m working on a new something, then at some point I think &amp;ldquo;I should really push all this so it&amp;rsquo;s backed up&amp;rdquo;. I create the repository for it on GitHub or Gogs via the web interface, then come back to my project and:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git remote add origin git@github.com:IanKulin/test.git&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is making the connection between my local project and the GitHub repo. I&amp;rsquo;d never really thought about what &lt;code&gt;origin&lt;/code&gt; meant in this context the hundreds of times I&amp;rsquo;ve previously typed it in, but actually it&amp;rsquo;s just the name we are giving to this connection. It&amp;rsquo;s just a convention to call it &amp;lsquo;origin&amp;rsquo;, it could just as easily be called &amp;lsquo;fred&amp;rsquo; or &amp;lsquo;github&amp;rsquo;. Since I am now planning to push to two separate remotes, it&amp;rsquo;s going to make sense to give them meaningful names. So in that case, we can do this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git remote add github git@github.com:IanKulin/test.git
git remote add gogs http://ct-gogs/iankulin/test.git
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, we can push with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git push github main
git push gogs main
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You might be wondering what happens if you just do a &lt;code&gt;git push&lt;/code&gt; at this stage (or as I like to call it &amp;ldquo;&lt;em&gt;Pressing the &amp;lsquo;Publish Branch&amp;rsquo; button on the VS Code source control panel&amp;rdquo;&lt;/em&gt;). The answer is that at the command line you&amp;rsquo;ll get an error saying you haven&amp;rsquo;t specified the destination, or in VS Code, it will ask you which one.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-9.41.08-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We can set the default remote with the -u flag when we&amp;rsquo;re pushing&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git push -u gogs main
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-9.46.26-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now the button in VS Code will say something &amp;ldquo;Sync Changes&amp;rdquo; and when you press it, it will only push to the remote we used in the last &lt;code&gt;-u&lt;/code&gt; push. Same thing if we &lt;code&gt;git push&lt;/code&gt; at the command line - it will work, but only push to the remote we used in the last &lt;code&gt;-u&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also worth noting that when we&amp;rsquo;ve set the default remote with the &lt;code&gt;-u&lt;/code&gt; flag in a &lt;code&gt;push&lt;/code&gt;, it is also the default remote for pulling from. Essentially this remote becomes the source-of-truth.&lt;/p&gt;
&lt;p&gt;For me this setup is usually fine - I&amp;rsquo;m generally working on my local gogs remote, that&amp;rsquo;s the source of truth so I specify it as the default with the &lt;code&gt;push -u&lt;/code&gt;. Then, when I&amp;rsquo;m done, I manually push to github so I can share it. If it was a project I needed to work on with anyone else, that would have to be the other way around - I&amp;rsquo;d use GitHub (or GitLab, Bitbucket etc) as the source of truth, and probably not even worry about hosting a copy on my home network unless I was worried about the repo being deleted.&lt;/p&gt;</description></item><item><title>Concurrency and channels in Go</title><link>https://blog.iankulin.com/concurrency-and-channels-in-go/</link><pubDate>Tue, 12 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/concurrency-and-channels-in-go/</guid><description>&lt;img src="https://blog.iankulin.com/images/portal-logo.jpg" width="400" alt=""&gt;
&lt;p&gt;In the long ago times, I&amp;rsquo;d done several years of commercial programming before I ever had to worry about dealing with multiple things happening at the same time. Perhaps because of the rarity of this problem, doing it in traditional languages was not always elegant.&lt;/p&gt;
&lt;p&gt;In the modern world of everything happening on the network, and systems being build out of micro-services and APIs, the beginning programmer probably has to deal with this stuff in Programming 102. Luckily, modern languages have these considerations built in, and one language with a particular reputation for that is Go.&lt;/p&gt;
&lt;p&gt;In Go, we have &lt;em&gt;Goroutines&lt;/em&gt;. This is basically a way of calling a function in such a way that the function goes away and does it&amp;rsquo;s thing and the rest of the program doesn&amp;rsquo;t wait for it. To do this, you just pop a go directive in front of the function call. Consider this little program:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;package main

import (
	&amp;#34;fmt&amp;#34;
	&amp;#34;math/rand&amp;#34;
	&amp;#34;time&amp;#34;
)

func waitAndReportWorker() {
	for {
		sleepTime := time.Duration(rand.Intn(5)) * time.Second
		time.Sleep(sleepTime)
		fmt.Printf(&amp;#34;Worker slept for %s&amp;#34;, sleepTime)
	}
}

func main() {
	waitAndReportWorker()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we run this, main hands control over to the worker, which sleeps for a bit, prints a message then repeats (normally the worker would some, like, actual work; but for our demo purposes, having a little nap then reporting it it fine).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-23-at-8.34.12-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-23-at-8.34.12-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We can convert it to a Goroutine, just by putting a go in front of the function call,&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func main() {
	go waitAndReportWorker()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Yay! Our first baby Goroutine. Sadly, this program will exit before the worker ever reports, so let&amp;rsquo;s add an infinite loop after we&amp;rsquo;ve launched the Goroutine. And we&amp;rsquo;ll do something in the loop so you can see that there things are happening concurrently in our program.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-23-at-8.46.11-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="collisions"&gt;Collisions&lt;/h3&gt;
&lt;p&gt;In my confected example, it&amp;rsquo;s extremely unlikely that the message from the worker would collide with the message being printed in the main loop, but it&amp;rsquo;s possible. But if we scaled up to a worldwide networked system processing millions of something a minute, it becomes almost guaranteed. Maybe a couple of sentences being mangled in output to the terminal is no big drama, but if we were writing something to a memory location, a file, a heart surgery robot interface, a database etc it could be bad. So we need to avoid that.&lt;/p&gt;
&lt;p&gt;The way Go deals with this is with &lt;em&gt;channels&lt;/em&gt;. A channel is like a portal between the main program in sequential procedural program land, to the worker function. When the worker needs to interact in some way with the main program it passes something back through the portal, and Go deals with it to avoid the dreaded collision. The portal/channel works the other way as well - the main program can pass information through the portal to the worker function.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the changes for this, then tease them out a little:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func waitAndReportWorker(resultChan chan&amp;lt;- string) {
	for {
		sleepTime := time.Duration(rand.Intn(5)) * time.Second
		time.Sleep(sleepTime)
		resultChan &amp;lt;- fmt.Sprintf(&amp;#34;\nWorker slept for %s\n&amp;#34;, sleepTime)
	}
}

func main() {

	resultChan := make(chan string)

	go waitAndReportWorker(resultChan)
	for {
		time.Sleep(250 * time.Millisecond)
		fmt.Print(&amp;#34;Nothing happening here &amp;#34;)
		result := &amp;lt;-resultChan
		fmt.Println(result)
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We create the channel with the &lt;code&gt;make&lt;/code&gt; function. The type for the channel is the type that we&amp;rsquo;re going to be passing through it. We pass the channel to our worker, where the function signature is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;func waitAndReportWorker(resultChan chan&amp;lt;- string)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I like this little arrow, it&amp;rsquo;s showing which way the portal works. In this case its a portal (channel) for passing a string out of the worker back to the main program. Channels can go the other way, ie. to pass things into the worker, or they can be bi-directional, which I don&amp;rsquo;t really think I&amp;rsquo;d do - I&amp;rsquo;d just add another channel.&lt;/p&gt;
&lt;p&gt;In our worker, we stuff something into the channel with that same arrow:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;resultChan &amp;lt;- fmt.Sprintf(&amp;quot;\nWorker slept for %s\n&amp;quot;, sleepTime)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;On the other end of our portal/channel (I wish they&amp;rsquo;d just called them portals - it&amp;rsquo;s no quirkier than the date formatting) in the main program we use another arrow to pull the value out:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;result := &amp;lt;-resultChan&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If we run this code, it works, sort of.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-24-at-4.51.21-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you look at the output at the bottom, you can see that extracting the string out of our channel is a blocking operation. The program is waiting there until it gets a value. That&amp;rsquo;s no use - we could have done that without mucking about with channels.&lt;/p&gt;
&lt;p&gt;Of course, there is a way around this. What we really want to to is check if there&amp;rsquo;s a value in the channel. If there is, process it, or if not, travel around our loop again. What we do is put the retrieval of the channel value as a case in a select block.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func main() {

	resultChan := make(chan string)
	go waitAndReportWorker(resultChan)

	for {
		select {
		case result := &amp;lt;-resultChan:
			fmt.Println(result)
		default:
			time.Sleep(250 * time.Millisecond)
			fmt.Print(&amp;#34;Nothing happening here &amp;#34;)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This version of the program will work exactly how we want. The worker goroutine will execute independently of the main loop which runs permanently, but then when the worker goroutine has something to say, it uses the channel to pass it back to the main routine which deals with it at the first opportunity.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-24-at-6.04.35-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Even though this is all working how we&amp;rsquo;d like, there is bit of programming craftsmanship needed. You may already know &lt;code&gt;make()&lt;/code&gt; from using it for slices. When we&amp;rsquo;re using it we&amp;rsquo;re allocating some resources - so now we have the responsibility to release them.&lt;/p&gt;
&lt;p&gt;To release the channel we made above, we close it:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;close(resultChan)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s add that for completeness. I&amp;rsquo;ll the time to exit my infinite loop. In practice You&amp;rsquo;ll have some other condition.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;package main

import (
	&amp;#34;fmt&amp;#34;
	&amp;#34;math/rand&amp;#34;
	&amp;#34;time&amp;#34;
)

func waitAndReportWorker(resultChan chan&amp;lt;- string) {
	for {
		sleepTime := time.Duration(rand.Intn(5)) * time.Second
		time.Sleep(sleepTime)
		resultChan &amp;lt;- fmt.Sprintf(&amp;#34;\nWorker slept for %s\n&amp;#34;, sleepTime)
	}
}

func main() {

	resultChan := make(chan string)
	go waitAndReportWorker(resultChan)

	startTime := time.Now()

	for {
		select {
		case result := &amp;lt;-resultChan:
			fmt.Println(result)
		default:
			time.Sleep(250 * time.Millisecond)
			fmt.Print(&amp;#34;Nothing happening here &amp;#34;)
		}
		if time.Since(startTime).Seconds() &amp;gt;= 10 {
			break
		}
	}

	close(resultChan)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is pretty much the minimal set up you need to get going with concurrency with Go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the channel&lt;/li&gt;
&lt;li&gt;a goroutine&lt;/li&gt;
&lt;li&gt;a select in a loop&lt;/li&gt;
&lt;li&gt;some cleanup by closing the channel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/gochanneldemo"&gt;code on github&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Date formatting in Go is quirky</title><link>https://blog.iankulin.com/date-formatting-in-go-is-quirky/</link><pubDate>Sat, 09 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/date-formatting-in-go-is-quirky/</guid><description>&lt;p&gt;When I&amp;rsquo;m working in an unfamiliar language, I find its quicker to just ask ChatGPT to write samples of anything I need than to look it up. For instance, last night I needed to format a date in Go, and rather than Google that and pick one of the results and scroll past the ads to read something, I just asked ChatGPT to give me a code example of formatting a date I gave it to DDMMYYYY.&lt;/p&gt;
&lt;p&gt;The answer it spat out, was something like:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dateString := currentTime.Format(&amp;quot;02012006&amp;quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Well, clearly it was hallucinating - it must have gotten confused between the date I gave it and the formatting string. Odd, but this flavour of things happens. It&amp;rsquo;s usually pretty good about fixing it if you point out an error, so I did that. It immediately apologised, agreed it had made an error, and gave me back the exact same thing. Poop, I guess it&amp;rsquo;s back to googling some docs then.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://go.dev/src/time/format.go"&gt;&lt;img src="https://blog.iankulin.com/images/gotimeformat.jpg" alt=""&gt;&lt;/a&gt;
&lt;em&gt;wtf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Well there you go. 1/2 3:04:05pm 2006 - 1-2-3-4-5-6. That&amp;rsquo;s what&amp;rsquo;s up with Go time/date formatting. I probably would have thought that was cute when I was 20 something as well.&lt;/p&gt;</description></item><item><title>Gogs - your own tiny GitHub</title><link>https://blog.iankulin.com/gogs-your-own-tiny-github/</link><pubDate>Wed, 06 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gogs-your-own-tiny-github/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-8.08.37-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;(edit: - I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/gogs-gitea-forgejo/"&gt;had a rethink about&lt;/a&gt; my source hosting)&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;re familiar with coding tools, like the excellent &lt;a href="https://code.visualstudio.com/"&gt;VS Code&lt;/a&gt;, and &lt;a href="https://git-scm.com/docs/git"&gt;git&lt;/a&gt;, it&amp;rsquo;s immediately apparent that these tools can be applicable for other purposes. A great example is that I now do my financial accounting in plain text (using &lt;a href="https://github.com/beancount/beancount"&gt;beancount&lt;/a&gt;). I have a python script that converts by bank account data in to the beancount format text files, I edit them in VS Code with a &lt;a href="https://marketplace.visualstudio.com/items?itemName=Lencerf.beancount"&gt;plugin&lt;/a&gt; that does the syntax highlighting and checks everything balances.&lt;/p&gt;
&lt;p&gt;Naturally, I want to version control that, so my text based accounts are all committed to git. But I don&amp;rsquo;t really want to push them up to GitHub, even to a private repo. I want to push them to a git server that&amp;rsquo;s available for me to pull down from anywhere, and is backed up with all my other data.&lt;/p&gt;
&lt;p&gt;It actually is possible to run a git server to do this with vanilla git, and I&amp;rsquo;m sure that&amp;rsquo;s how the hairy chested &lt;a href="https://www.linuxfoundation.org/blog/blog/classic-sysadmin-how-to-run-your-own-git-server"&gt;sysadmins of old&lt;/a&gt; do it. But I want a web gui a bit like GitGub that I&amp;rsquo;m familiar with. Of course, GutHub does all that other sweet stuff like CI/CD, but I don&amp;rsquo;t need that for my accounts.&lt;/p&gt;
&lt;p&gt;There are a couple of popular options for this job, one is &lt;a href="https://about.gitea.com/"&gt;Gitea&lt;/a&gt; which does lean into that CI/CD functionality, and the other is &lt;a href="https://gogs.io/"&gt;Gogs&lt;/a&gt;, which being a bit simpler, is also a bit simpler to get going. When I say simpler, it&amp;rsquo;s still massive overkill for my needs - you can have thousands of users, do pull requests, track issues, write project wikis - all that good stuff. It also has webhooks so you can knit together a pipeline with drone.io, Jenkins or other CI/CD tools. So I went with Gogs&lt;/p&gt;
&lt;h3 id="installing"&gt;Installing&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s not mentioned on the &lt;a href="https://gogs.io/docs/installation"&gt;install page for Gogs&lt;/a&gt;, but there is an official &lt;a href="https://hub.docker.com/r/gogs/gogs/"&gt;container build&lt;/a&gt;. Possibly this is because there&amp;rsquo;s a couple of rough edges that I&amp;rsquo;ll get to shortly. I&amp;rsquo;ve talked before about how I like to run services in Docker on LXC, so I won&amp;rsquo;t go over that again. Here&amp;rsquo;s my docker-compose:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;

services:
 gogs:
 image: gogs/gogs
 container_name: gogs
 ports:
 - &amp;#34;23:22&amp;#34;
 - &amp;#34;80:3000&amp;#34;
 volumes:
 - ./data:/data
 restart: always
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;However, there is a gotcha I hadn&amp;rsquo;t encountered before - when I &lt;code&gt;docker compose up&lt;/code&gt; with this, I got the error &amp;ldquo;failed to register layer: unlinkat /app/gogs/docker/build: invalid argument&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-8.36.42-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-8.36.42-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I asked ChatGPT about this, she thought it might be to do with the storage driver. I didn&amp;rsquo;t know what that was so I spent time googling around.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-8.40.36-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-8.40.36-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Pretty soon, I discovered &lt;a href="https://forum.proxmox.com/threads/docker-failed-to-register-layer-applylayer-exit-status-1-stdout-stderr-unlinkat-var-log-apt-invalid-argument.119954/"&gt;this thread&lt;/a&gt;. Part way down there&amp;rsquo;s the suggestion to edit &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; to add a different storage driver, followed by many comments of &amp;ldquo;Thanks!&amp;rdquo; and &amp;ldquo;That fixed it&amp;rdquo;. I followed that advice, (it uses a different driver &amp;ldquo;vfs&amp;rdquo; rather than &amp;ldquo;aufs&amp;rdquo; as suggested by ChatGPT) and then the container came up properly.&lt;/p&gt;
&lt;p&gt;With that out of the way, and the container live, if you go to the port you&amp;rsquo;ve specified in the docker-compose file (mine was :80), you&amp;rsquo;ll be greeted with the &amp;ldquo;Install Steps For First-time Run&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-8.54.19-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-8.54.19-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;They are not joking. You won&amp;rsquo;t be able to guess these settings. I guess they haven&amp;rsquo;t put a lot of work into the container experience - some of these settings need to be for inside the container, and some are used for prompting the user, which are outside of the container settings. I suspect this rough edge is why the container install is not on the Gogs website yet.&lt;/p&gt;
&lt;p&gt;Anyway, after I&amp;rsquo;d ignored this suggestion, run into problems, google them, found closed issues where people had had the same problem and the devs pointed them to the perfectly clear guidelines we hadn&amp;rsquo;t read&amp;hellip;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll leave you to read the guidelines. The only other things of note is that I used the SQLite database to make my life simpler, and you don&amp;rsquo;t need to muck around making an admin account - it just makes the first person to log in the admin. Once that&amp;rsquo;s all done, you have to create a user account, then log in with it. You&amp;rsquo;ll be greeted by a reasonably familiar sight.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-9.07.30-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-9.07.30-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you go ahead and create a repository in Gogs, it will give you the commands to push a repo up:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-9.09.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-9.09.28-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s do that:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-9.21.38-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The back in our repo on Gogs:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-9.22.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-20-at-9.22.53-pm.png" width="1023" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>New Self-Hosted Service Workflow</title><link>https://blog.iankulin.com/new-self-hosted-service-workflow/</link><pubDate>Sun, 03 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/new-self-hosted-service-workflow/</guid><description>&lt;p&gt;I&amp;rsquo;ve developed a bit of a workflow for setting up a new service of some type on the homelab. Installing it is the obvious thing, but I also have a few quality of life things I do to make it a full production-quality part of my installation. I thought it might be helpful to run through those things using a recent example of adding &lt;a href="https://www.audiobookshelf.org/"&gt;audiobookshelf&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="audiobookshelf"&gt;audiobookshelf&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.audiobookshelf.org/"&gt;audiobookshelf&lt;/a&gt; is a web based system for viewing, playing, downloading and/or generally managing your audio books. I&amp;rsquo;ve been an &lt;a href="https://www.audible.com.au/"&gt;Audible&lt;/a&gt; user/subscriber, but recently got grumpy at them about something - I think I had paused my subscription, and my downloaded books were still available on my phone. I was halfway through one, upgraded the app, and then wasn&amp;rsquo;t able to play the book without re-subscribing. That might not be exactly right, but it was some type of frustrating carry on like that.&lt;/p&gt;
&lt;p&gt;In any case, that made me decide I couldn&amp;rsquo;t trust them, and it was time to reassert my digital sovereignty by downloading the books I&amp;rsquo;d paid for (and the ones they&amp;rsquo;d given me), removing the &lt;a href="https://en.wikipedia.org/wiki/Digital_rights_management"&gt;DRM&lt;/a&gt;, and hosting it myself. The first two steps of that process were easily carried out with a brilliant bit of software called &lt;a href="https://openaudible.org/"&gt;OpenAudible&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="do-it-on-dev"&gt;Do it on dev&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/img_7003.jpg" width="900" alt=""&gt;
&lt;p&gt;Since I have the luxury of having separate production and development servers, I generally play around with new things I&amp;rsquo;m trying out on the dev instance of Proxmox. Note that this is almost entirely unnecessary - since everything is virtualised in Proxmox on the production server, there&amp;rsquo;s hardly any damage I could cause in one VM or container that would adversely affect anything else.&lt;/p&gt;
&lt;p&gt;Nevertheless, whether it&amp;rsquo;s caution, or a need to justify the size of the homelab, I always start building new things on the dev server. Once it&amp;rsquo;s all working perfectly, it&amp;rsquo;s a simple matter (that we&amp;rsquo;ll get to later) to move it as-is to the production server.&lt;/p&gt;
&lt;h3 id="installation-stack"&gt;Installation Stack&lt;/h3&gt;
&lt;p&gt;My default setup now is a Docker container, inside an LXC container on Proxmox. Although this originally felt like a comical number of levels of abstraction, each layer is doing something for me, and now it just feels like the cost of doing business.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox - virtualising everything insulates services from each other, makes moving them around easier, backing them up and restoring them trivial, and provides a level of high availability.&lt;/li&gt;
&lt;li&gt;LXC - lighter than a full VM, more VM like than Docker, and quicker to play with. Does add a bit of complexity we&amp;rsquo;ll get to later.&lt;/li&gt;
&lt;li&gt;Docker - OCI compliant containers are the bomb. This is how we do software now. I pushed back as long as I could but the logic is too strong. There are problems still to solve around &lt;a href="https://www.cisa.gov/sbom"&gt;SBOM&lt;/a&gt;, but the reduction in the work of managing installations is compelling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I create a non-root user, and the &lt;code&gt;docker-compose.yml&lt;/code&gt; and the directories for any config or data all go in that user&amp;rsquo;s home directory. I don&amp;rsquo;t prefer &lt;a href="https://docs.docker.com/storage/volumes/"&gt;Docker volumes&lt;/a&gt; for the data any more since the &lt;a href="https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/"&gt;downsides&lt;/a&gt; annoy me and the upsides must be in order to solve problems I haven&amp;rsquo;t encountered yet.&lt;/p&gt;
&lt;p&gt;Since there are a few little gotchas using LXC, when I&amp;rsquo;m trying something for the very first time, and I&amp;rsquo;m not even sure if it&amp;rsquo;s going to end up being used, I&amp;rsquo;ll do it in an VM first. I have a bunch of VM&amp;rsquo;s on the dev machine in varying states, so I normally pick one of them that already had Docker installed. This also gives me an idea for the amount of RAM and disk space the container is going to need. Changing the memory size once it&amp;rsquo;s in production is no biggie, but expanding the disk space is a bit of stuffing around.&lt;/p&gt;
&lt;p&gt;When I&amp;rsquo;m ready to make the container, it&amp;rsquo;s always the latest Debian stable, unprivileged, nesting turned on. Very few web services require more than 1GB RAM, and I guess the disk usage from the earlier trials then add a bit. I have lots of disk space and CPU time - it&amp;rsquo;s usually memory that&amp;rsquo;s the first bottleneck you&amp;rsquo;ll run into on little homelab servers. I&amp;rsquo;m sure I&amp;rsquo;ve heard &lt;a href="https://2.5admins.com/"&gt;Jim Salter and Allan Jude&lt;/a&gt; recommend that you should keep the VM memory low to leave more for the host so the it can effectively cache for all the guests.&lt;/p&gt;
&lt;p&gt;I always use docker-compose. Too many times I&amp;rsquo;ve wanted to upgrade a container, and have to waste time figuring out what the run command was. The compose file is good documentation for where your data is as well if you are, like me, avoiding volumes.&lt;/p&gt;
&lt;h3 id="the-steps"&gt;The Steps&lt;/h3&gt;
&lt;h4 id="some-installs"&gt;Some installs&lt;/h4&gt;
&lt;p&gt;With the fresh LXC created (latest Debian stable, unprivileged, nesting turned on), and started, I use the Proxmox console to log in, do some &lt;code&gt;apt&lt;/code&gt; updates, use &lt;code&gt;adduser&lt;/code&gt; to add my user, &lt;code&gt;apt install sudo&lt;/code&gt; and then &lt;code&gt;usermod&lt;/code&gt; to add my user to the sudo group.&lt;/p&gt;
&lt;p&gt;I then switch to a real terminal and ssh in as that user to install Docker. While that&amp;rsquo;s happening, I log into my router and reserve the IP address for the new container. This will follow when I move the container to the production server since it takes it&amp;rsquo;s MAC address with it.&lt;/p&gt;
&lt;p&gt;My pattern for SSH keys, which might not be the most secure, is that I have a key per device. So there&amp;rsquo;s one from my laptop, one for the terminal on my phone, and one for a VM that I sometimes use as an entry point to my home network via Tailnet. My theory with all this is that if any of those devices are compromised (for example my laptop is stolen) I can revoke that key from each of my services.&lt;/p&gt;
&lt;h4 id="nas-mount"&gt;NAS Mount&lt;/h4&gt;
&lt;p&gt;Often the service I&amp;rsquo;m installing needs access to the NAS - and that&amp;rsquo;s the case for audibookshelf which obviously needs access to my collection of audio books on my four bay Synology. I use an &lt;code&gt;/etc/fstab&lt;/code&gt; entry to mount the folder I&amp;rsquo;m interested in. I&amp;rsquo;ve set up the NAS to share these over SMB. The entry for audiobookshelf looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.100.32/media/books/audio/ /mnt/media cifs username=abs_user,password=SeCrErpaSSword,file_mode=0660,dir_mode=07
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s a bit going on here, let&amp;rsquo;s pull it apart:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;//192.168.100.32/media/books/audio/&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The directory on the NAS where my audiobooks are stored. I&amp;rsquo;ve been a bit slack here. It would have been better for that directory to have been it&amp;rsquo;s own share to reduce the attack surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;/mnt/media&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the directory in the LXC container that we&amp;rsquo;re mounting the books to. If I could go back in time to when I started by Linux &amp;amp; self-hosting journey, I would not have used the word media, since in Linux that more refers to things like USB drives and less like entertainment to consume. &lt;a href="https://www.karlton.org/2017/12/naming-things-hard/"&gt;Naming things is hard&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;cifs&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The protocol being used for the share. I&amp;rsquo;ve got this shared folder set up as SMB, so I use CIFS. Some of my shares are NFS, so you could have &lt;code&gt;nfs&lt;/code&gt; at this position in the entry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;username=abs_user,password=SeCrErpaSSword&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It seems bad to have these credentials in /etc/fstab where any user on this system can read them, but I am the only user on this system and I don&amp;rsquo;t know what other convenient way I could get around this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;file_mode=0660&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read/write for user and group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;dir_mode=07&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read/write/execute on directories for user &amp;amp; group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s in the &lt;code&gt;/etc/fstab&lt;/code&gt;, you need to mount it with a &lt;code&gt;mount -a&lt;/code&gt;, then you should see the share by &lt;code&gt;ls&lt;/code&gt;-ing the mount point.&lt;/p&gt;
&lt;h4 id="docker-compose"&gt;Docker compose&lt;/h4&gt;
&lt;p&gt;Obviously this will vary with whatever service you&amp;rsquo;re running. Here&amp;rsquo;s mine for audiobookshare.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;

services:
 audiobookshelf:
 image: ghcr.io/advplyr/audiobookshelf
 container_name: audiobookshelf
 ports:
 - &amp;#34;80:80&amp;#34;
 volumes:
 - ./config:/config
 - ./metadata:/metadata
 - /mnt/media:/audiobooks
 restart: always
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The notable things here are that I store all the container data - in this case &lt;code&gt;/config&lt;/code&gt; and &lt;code&gt;/metadata&lt;/code&gt; in subdirectories from the current directory, which is actually the user&amp;rsquo;s home directory. This LXC container is only for running this single service, so as soon as I &lt;code&gt;ssh&lt;/code&gt; in, everything I need to know or find out is easily discoverable, and easily accessible if I want to &lt;code&gt;scp&lt;/code&gt; it without a convoluted path.&lt;/p&gt;
&lt;p&gt;Another benefit of running in individual LXC&amp;rsquo;s is that each service has its own IP address - so I can use port 80 for every service.&lt;/p&gt;
&lt;h4 id="tailscale"&gt;Tailscale&lt;/h4&gt;
&lt;p&gt;Now that we can have up to 100 Tailscales on the free tier, every real service gets one. For the install, I just follow the &lt;a href="https://tailscale.com/kb/1174/install-debian-bookworm/"&gt;Debian Tailscale installation instructions&lt;/a&gt; since I&amp;rsquo;m using a Debian LXC. And now when we try &lt;code&gt;tailscale up&lt;/code&gt; we run into the LXC problem. I&amp;rsquo;ve already documented how to overcome that in &lt;a href="https://blog.iankulin.com/getting-tailscale-working-in-lxc-containers/"&gt;an earlier post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The combination of using Tailscale, and having access to port 80 means that the web address for this service will just be whatever hostname I gave it, in this case http://ct327-audiobookshelf&lt;/p&gt;
&lt;h4 id="ansible"&gt;Ansible&lt;/h4&gt;
&lt;p&gt;Some of the next steps are so common, I&amp;rsquo;ve set up Ansible playbooks for them, but to allow me to apply them to the new server, they need to be added into my Ansible infrastructure. First the hosts file where they get a host entry and some variables.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.48.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.48.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then in the encrypted &lt;code&gt;vault.yml&lt;/code&gt; file for the secrets. I&amp;rsquo;ve written about these before &lt;a href="https://blog.iankulin.com/first-ansible-playbook/"&gt;here&lt;/a&gt; and &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;here&lt;/a&gt;. Since I have &lt;code&gt;hosts:all&lt;/code&gt; in the playbook that runs all my &lt;a href="https://gist.github.com/IanKulin/41dbf097ac6bddd9e315859d3a06fe02"&gt;&lt;code&gt;apt&lt;/code&gt; updates&lt;/a&gt;, this now means the LXC container will get all it&amp;rsquo;s updates.&lt;/p&gt;
&lt;p&gt;Now we can automate some tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make this server use our &lt;code&gt;apt-cache&lt;/code&gt; server to make updates a bit faster and efficient. Described &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Install a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;little endpoint&lt;/a&gt; so the available memory and disk space can be monitored.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once that endpoint is installed, I can add a couple of entries to my &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt; instance to keep track of the server health and notify me with &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;ntfy&lt;/a&gt; - so that&amp;rsquo;s monitoring covered off.&lt;/p&gt;
&lt;h4 id="backups"&gt;Backups&lt;/h4&gt;
&lt;p&gt;Backups in Proxmox are easy. I already have a general backup job set up for the prod DataCenter - it just snapshots every VM and LXC to the NAS at 1:00am each day. That&amp;rsquo;s plenty for this service - the only thing that would get lost would be a day&amp;rsquo;s worth of metadata, most of which is automatically pulled from web services anyway.&lt;/p&gt;
&lt;p&gt;This backup is of the LXC container with all the audiobookshelf config and code - not my book library. There is a backup process for it that&amp;rsquo;s a complicated collection of and external USB drive and &lt;code&gt;rsync&lt;/code&gt;-ing to a remote that might be a story for another day.&lt;/p&gt;
&lt;h3 id="done"&gt;Done&lt;/h3&gt;
&lt;p&gt;And that&amp;rsquo;s it. Now my audiobookshelf is running in an LXC container, serving the books off my NAS. The service is monitored for health, and there&amp;rsquo;s a backup plan in place. I can kick back and catch up on some technical reading.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_7018.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Ansible - Importing a Playbook</title><link>https://blog.iankulin.com/ansible-importing-a-playbook/</link><pubDate>Thu, 30 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ansible-importing-a-playbook/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/tags/ansible/"&gt;Ansible&lt;/a&gt; is a system for automating server tasks, and these tasks are written in a special yaml file called a playbook. I had need to call one playbook from another today and learned a couple of things.&lt;/p&gt;
&lt;h3 id="plays-vs-tasks"&gt;Plays vs Tasks&lt;/h3&gt;
&lt;p&gt;In Ansible we run &lt;em&gt;tasks&lt;/em&gt;. A group of tasks run against one particular sets of hosts is called a &lt;em&gt;play&lt;/em&gt;. Here is a playbook with one play, and two tasks:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Play 1 - Print the Book 1 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 1, Task 1

 - name: Print message 2
 debug:
 msg: Book 1, Task 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s possible to have multiple plays in a single &lt;em&gt;playbook&lt;/em&gt;. This would often be done if there was different tasks to run on different servers. For example you might want to run log rotations on you web servers, but reindexing on the database servers.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Play 1 - Print the Book 1 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 1, Task 1

 - name: Print message 2
 debug:
 msg: Book 1, Task 2

- name: Play 2
 hosts: 127.0.0.1

 tasks:
 - name: Print message
 debug:
 msg: This is the second play in Book 1
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="importing-playbooks"&gt;Importing Playbooks&lt;/h3&gt;
&lt;p&gt;When you run a playbook, generally the whole playbook gets run. So if we did have a playbook that included the tasks for the web servers and database servers, they would all be executed. That makes sense a lot of the time, but what if you usually wanted to run them together, but then sometimes just the database tasks?&lt;/p&gt;
&lt;p&gt;Ansible has an answer for this - you put the plays in different playbooks, but then import one into the top of the other one.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s start over. Say this is our first playbook - &lt;code&gt;book1.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Play 1 - Print the Book 1 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message
 debug:
 msg: Book 1, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we run that with &lt;code&gt;ansible-playbook book1.yml&lt;/code&gt; the output will be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PLAY [Play 1 - Print the Book 1 messages] *************************************************************************

TASK [Gathering Facts] *******************************************************************************
ok: [127.0.0.1]

TASK [Print message] *******************************************************************************
ok: [127.0.0.1] =&amp;gt; {
 &amp;#34;msg&amp;#34;: &amp;#34;Book 1, Task 1&amp;#34;
}

PLAY RECAP *******************************************************************************
127.0.0.1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And we&amp;rsquo;ll make a &lt;code&gt;book2,yml&lt;/code&gt; very similar:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message
 debug:
 msg: Book 2, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can probably imagine the output from the example above - it&amp;rsquo;s identical except for the numbers in the messages.&lt;/p&gt;
&lt;p&gt;These two playbooks can easily be run separately, but then if we wanted to run them together sometimes, we could just make a new playbook that imported both of them:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- import_playbook: book1.yml
- import_playbook: book2.yml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we run this playbook, the output is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PLAY [Play 1 - Print the Book 1 messages] 
TASK [Gathering Facts] *******************************************************************************ok: [127.0.0.1]TASK [Print message] *******************************************************************************ok: [127.0.0.1] =&amp;gt; { &amp;#34;msg&amp;#34;: &amp;#34;Book 1, Task 1&amp;#34;}PLAY [Print the Book 2 messages] *******************************************************************************TASK [Gathering Facts] *******************************************************************************ok: [127.0.0.1]TASK [Print message 1] *******************************************************************************ok: [127.0.0.1] =&amp;gt; { &amp;#34;msg&amp;#34;: &amp;#34;Book 2, Task 1&amp;#34;}PLAY RECAP *******************************************************************************127.0.0.1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="potential-problems"&gt;Potential problems&lt;/h3&gt;
&lt;p&gt;Instead of making a new file to import both playbooks, you might want to just import one playbook into the other. For instance, you could achieve the same as we&amp;rsquo;ve done above by editing &lt;code&gt;book2.yml&lt;/code&gt; to import &lt;code&gt;book1.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- import_playbook: book1.yml
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 2, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s important to note that this has to be done at that level in the hierarchy. You can&amp;rsquo;t import a playbook in the middle of a play. For example this is legal yaml, but won&amp;rsquo;t work.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 import_playbook: book1.yml

 tasks:
 - name: Print message 1
 debug:
 msg: Book 2, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Instead, we&amp;rsquo;ll get an message like &lt;code&gt;ERROR! 'hosts' is not a valid attribute for a PlaybookInclude&lt;/code&gt;. However this is fine:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 2, Task 1

- import_playbook: book1.yml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The import above is at the highest level of yaml, not inside the play, so it works well.&lt;/p&gt;</description></item><item><title>ViewTube</title><link>https://blog.iankulin.com/viewtube/</link><pubDate>Mon, 27 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/viewtube/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.17.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Whenever I encounter one of those &amp;ldquo;What are you self-hosting?&amp;rdquo; threads, I know I&amp;rsquo;m about to waste an hour looking at, and often trying out, software I probably don&amp;rsquo;t really need, and that was the case with &lt;a href="https://lemmy.world/post/8385160"&gt;this post&lt;/a&gt; on the &lt;a href="https://lemmy.world/c/selfhost@lemmy.ml"&gt;lemmy.world Selfhosted&lt;/a&gt; community.&lt;/p&gt;
&lt;p&gt;The basic idea of ViewTube is that it&amp;rsquo;s a self-hosted front end for YouTube, which just happens to strip out all the advertising and tracking. You can create your own local accounts which allows you to subscribe to channels and which keeps your progress so you don&amp;rsquo;t start over if you go back to a video - although I couldn&amp;rsquo;t see a history list. Forgetting your history might be a feature in an app designed to prevent tracking.&lt;/p&gt;
&lt;p&gt;It only took five minutes to get it running to try out, and most of that was downloading the docker images. I just made a directory in a VM and dropped this docker compose into it.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;

services:
 viewtube:
 restart: unless-stopped
 # Or use mauriceo/viewtube:dev for the development version
 image: mauriceo/viewtube:latest
 # ViewTube will not start until the database and redis are ready
 depends_on:
 - viewtube-mongodb
 - viewtube-redis
 # Make sure all services are in the same network
 networks:
 - viewtube
 volumes:
 # This will map ViewTube&amp;#39;s data directory to the local folder ./data/viewtube/
 - ./data/viewtube:/data
 environment:
 - VIEWTUBE_DATABASE_HOST=viewtube-mongodb
 - VIEWTUBE_REDIS_HOST=viewtube-redis
 ports:
 - 8066:8066

 viewtube-mongodb:
 restart: unless-stopped
 image: mongo:4.4
 networks:
 - viewtube
 volumes:
 - ./data/db:/data/db

 viewtube-redis:
 restart: unless-stopped
 image: redis:7
 networks:
 - viewtube
 volumes:
 - ./data/redis:/data

networks:
 viewtube:
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The only change in here from the &lt;a href="https://viewtube.wiki/installation/docker"&gt;official one&lt;/a&gt; was to change to an older version since I hadn&amp;rsquo;t passed through the CPU in host mode, so there was no &lt;a href="https://old.reddit.com/r/homelab/comments/yvo4jm/how_do_i_enable_avx_on_my_server/"&gt;AVX support which is required by newer versions&lt;/a&gt; of MongoDB.&lt;/p&gt;</description></item><item><title>Adding Front Matter To mdserver</title><link>https://blog.iankulin.com/adding-front-matter-to-mdserver/</link><pubDate>Fri, 24 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/adding-front-matter-to-mdserver/</guid><description>&lt;p&gt;The very first issue I opened on &lt;a href="https://blog.iankulin.com/displaying-markdown-as-html/"&gt;mdserver&lt;/a&gt; - my server project that serves HTML from markdown files - was that the title of the page (which shows in the browser tab, and is used for browser bookmarks) needed to be set &lt;em&gt;inside&lt;/em&gt; the markdown file, rather than generated from the file name. I didn&amp;rsquo;t invent this idea - I&amp;rsquo;ve seen this sort of metadata in the top of Jekyll and Hugo markdown. Here&amp;rsquo;s an example from the &lt;a href="https://jekyllrb.com/docs/front-matter/"&gt;Jekyll website&lt;/a&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
layout: post
title: Blogging Like a Hacker
---
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can&amp;rsquo;t really see in this example, but the format is YAML. Although I might be interested in using it for other things (such as selecting a template) later, for now, all I need is a title. The process would be that the server would extract the title from the front matter, then inject that into the template HTML so the page had a proper title.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using the &lt;a href="https://showdownjs.com/"&gt;Showdown&lt;/a&gt; library to do the conversion from markdown. Here&amp;rsquo;s a short demo of how that works:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const showdown = require(&amp;#39;showdown&amp;#39;);
const converter = new showdown.Converter();

const markdown = `
# heading
Some random Text
* list item
* another`

const rawHtml = converter.makeHtml(markdown);

console.log(rawHtml);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This would output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;h1 id=&amp;#34;heading&amp;#34;&amp;gt;heading&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Some random Text&amp;lt;/p&amp;gt;
&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;list item&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;another&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Showdown is an 827K dependency, so I figured it might already deal with front matter, or would at least have some sort of extension hooks so I could write something to scrape the title out. In fact it has both.&lt;/p&gt;
&lt;p&gt;To enable front matter, you just have to set a flag in the converter, then there&amp;rsquo;s a .getMetadata() method on the converter to get an object of all the metadata. Let&amp;rsquo;s flesh out my demo code a bit to show this, I&amp;rsquo;ll highlight the changes.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const showdown = require(&amp;#39;showdown&amp;#39;);
const converter = new showdown.Converter({metadata: true});

const markdown = `
---
title: Test Title
---
# heading
Some random Text
* list item
* another`

const rawHtml = converter.makeHtml(markdown);

//console.log(rawHtml);
console.log(converter.getMetadata().title);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This simply outputs &lt;code&gt;Test Title&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re wondering if that YAML pollutes the HTML output at all, it does not. The HTML from this second example is exactly the same as the first example above without the YAML.&lt;/p&gt;</description></item><item><title>Building Docker images for multiple architectures</title><link>https://blog.iankulin.com/building-docker-images-for-multiple-architectures/</link><pubDate>Mon, 20 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/building-docker-images-for-multiple-architectures/</guid><description>&lt;p&gt;My little mdserver app has been a good way for me to start experimenting with the the devops side of things, especially building for Docker. Since I wanted to make the Docker image available for ARM Linux &amp;amp; x86 Linux I had a janky shell script that looked like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash

# Extract the version number from package.json using jq
VERSION=$(jq -r .version package.json)

docker build --platform linux/amd64 -t iankulin/mdserver:$VERSION -t iankulin/mdserver:latest .
docker build --platform linux/arm64 -t iankulin/mdserver:arm64-$VERSION -t iankulin/mdserver:arm64-latest .

docker push iankulin/mdserver:arm64-$VERSION 
docker push iankulin/mdserver:arm64-latest 

docker push iankulin/mdserver:$VERSION
docker push iankulin/mdserver:latest 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So I&amp;rsquo;d build two different versions, and use the tags to separate them. In the registry it&amp;rsquo;d look like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.36.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.36.45-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But the big official images, for instance Node, have a long list of architectures associated with each tag - these are &lt;a href="https://docs.docker.com/build/building/multi-platform/"&gt;multi-platform images&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To create these, we need to use the &lt;code&gt;docker buildx&lt;/code&gt; feature. If you google how to do this, there&amp;rsquo;s a few mentions of how to &amp;rsquo;turn on&amp;rsquo; this &amp;rsquo;experimental&amp;rsquo; feature. I didn&amp;rsquo;t do that, so perhaps it&amp;rsquo;s been mainstreamed now. What I did to enable it was to enter:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker buildx create --use
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then to create my dual architecture image:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker buildx build --push \
--platform linux/arm64,linux/amd64 \
-t iankulin/mdserver:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then 113 seconds later (thank you Apple silicon), this showed up in my Docker Hub:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.47.50-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.47.50-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lovely!&lt;/p&gt;
&lt;p&gt;Buoyed by success, I decided I should also be shipping a Raspberry Pi version, which I guess is 32 bit ARM? So I dropped this into the CLI:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker buildx build --push \
--platform linux/arm64,linux/amd64,linux/arm/v7 \
-t iankulin/mdserver:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This was somewhat less successful. I think the story of the building for other architectures with &lt;code&gt;buildx&lt;/code&gt; is that the container has QEMU binaries for them - ie like it&amp;rsquo;s running a little VM to do the build inside of. That&amp;rsquo;s three inception layers in, so I guess that&amp;rsquo;s why it&amp;rsquo;s slow for an alien architecture.&lt;/p&gt;
&lt;p&gt;In any case, this may still work, but at the time of writing, the &lt;code&gt;NPM install&lt;/code&gt; had been running overnight . If this speed turns out to be typical, it&amp;rsquo;s a good reason to look at outsourcing your Docker builds to GitHub actions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-30-at-7.03.18-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;With all eight cores pegged at 100% on an M1 MacBook:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-30-at-7.05.19-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-30-at-7.05.19-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Docker volume backup is more complicated than it should be</title><link>https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/</link><pubDate>Fri, 17 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/</guid><description>&lt;p&gt;&lt;a href="https://unccelearn.org/course/view.php?id=128&amp;page=overview&amp;lang=en"&gt;&lt;img src="https://blog.iankulin.com/images/big.jpg" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I set up my first Docker container (I think for &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;), I had read around and understood there were two choices for persistent; &lt;em&gt;bind mounts&lt;/em&gt; (where the data inside the container is effectively a symlink to a location on the local file system) or &lt;em&gt;name volumes&lt;/em&gt; where Docker abstracted that away a bit, so you didn&amp;rsquo;t have to worry where it was - I sort of understood Docker &amp;lsquo;managed&amp;rsquo; it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been lazily doing my &amp;lsquo;backups&amp;rsquo; by just saving snapshots of entire VM&amp;rsquo;s - which works really well, Proxmox handles the scheduling of them, I regularly test them (every month I run off the backup production server for a couple of days from the backups). I don&amp;rsquo;t mind that backing up up an entire VM for a couple of Dockerised apps is expensive in disk because local disk is cheap and it&amp;rsquo;s super convenient.&lt;/p&gt;
&lt;p&gt;However, I&amp;rsquo;ve got a couple of projects on the list where I&amp;rsquo;d like to move a container and it&amp;rsquo;s data between VM&amp;rsquo;s. One is trying out Jellyfin in Docker in an LXC, and another is moving the containers on my general utility dockerhost to a new VM with a bit larger disk since that seems easier than expanding the disk.&lt;/p&gt;
&lt;p&gt;I assumed I&amp;rsquo;d be stoping the container and doing something like &lt;code&gt;docker export portainer_data somebackupfile.name&lt;/code&gt; then moving that file over to the new system and running &lt;code&gt;docker import portainer_data somebackupfile.name&lt;/code&gt; to re-create it.&lt;/p&gt;
&lt;p&gt;But no, that&amp;rsquo;s not how it works. According to the Docker people, I need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use inspect to find out the internal data directories of the container&lt;/li&gt;
&lt;li&gt;Stop the container&lt;/li&gt;
&lt;li&gt;Create a new generic linux container&lt;/li&gt;
&lt;li&gt;Have it mount the docker volumes&lt;/li&gt;
&lt;li&gt;Also have it bind mount to the current directory&lt;/li&gt;
&lt;li&gt;Run a command inside the container to tar ball the internal data directory and save it to the bind mount&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The only real concession to usability along the way is that there&amp;rsquo;s a &lt;code&gt;--volumes_from&lt;/code&gt; flag that saves you from extracting all the volume names from a &lt;code&gt;docker inspect&lt;/code&gt; of the container whose data you want to back up.&lt;/p&gt;
&lt;h3 id="example"&gt;Example&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s run through those steps with an example. I&amp;rsquo;m going to set up &lt;a href="https://uptime.kuma.pet/"&gt;Uptime Kuma&lt;/a&gt; in Docker. I&amp;rsquo;ll use the &lt;a href="https://github.com/louislam/uptime-kuma/blob/master/docker/docker-compose.yml"&gt;suggested compose file&lt;/a&gt; which creates a named volume &lt;code&gt;uptime-kuma&lt;/code&gt;. I tested that&amp;rsquo;s up and running on port 3001 - when I visited there, it wanted me to create an admin account.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-9.55.47-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-9.55.47-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For demo purposes, I created the admin user &lt;code&gt;ian&lt;/code&gt; and set up Uptime Kuma to monitor Google for us.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.39.40-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.39.40-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you started the app from a docker compose file, you can just look in there to see what the internal data directories that are being mounted to are:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3.8&amp;#39;

services:
 uptime-kuma:
 image: louislam/uptime-kuma:1
 container_name: uptime-kuma
 volumes:
 - uptime-kuma:/app/data
 ports:
 - &amp;#34;3001:3001&amp;#34; # &amp;lt;Host Port&amp;gt;:&amp;lt;Container Port&amp;gt;
 restart: always

volumes:
 uptime-kuma:
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or alternatively use the &lt;code&gt;docker inspect &amp;lt;container name&amp;gt;&lt;/code&gt; command. You&amp;rsquo;ll get back a barrage of Json - somewhere in there will be the mount details:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;Mounts&amp;#34;: [
 {
 &amp;#34;Type&amp;#34;: &amp;#34;volume&amp;#34;,
 &amp;#34;Name&amp;#34;: &amp;#34;uptimekuma_uptime-kuma&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/var/lib/docker/volumes/uptimekuma_uptime-kuma/_data&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/app/data&amp;#34;,
 &amp;#34;Driver&amp;#34;: &amp;#34;local&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;z&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;&amp;#34;
 }
],
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Either way, we now know that the internal directory for data is &lt;code&gt;/app/data&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next stop the container with &lt;code&gt;docker stop uptime-kuma&lt;/code&gt;, then type in this bad boy based on the one in the docs.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker run --rm --volumes-from uptime-kuma -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /app/data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The highlighted bits are the pieces I changed for our demo - the name of our container and the internal data directory for it that we found in the steps above. Pulling down an entire &lt;a href="https://hub.docker.com/_/ubuntu"&gt;Ubuntu container&lt;/a&gt; seemed overkill - we&amp;rsquo;re just running a tar command so perhaps &lt;a href="https://hub.docker.com/_/alpine"&gt;Alpine&lt;/a&gt; or &lt;a href="https://hub.docker.com/_/busybox"&gt;Busybox&lt;/a&gt; would be fine, however, it pulled down quite quickly so it&amp;rsquo;s either smaller that I imagined or I already had the main layers locally.&lt;/p&gt;
&lt;p&gt;Now if we look in the directory where we ran that command, there should be a &lt;code&gt;backup.tar&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.34.38-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.34.38-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, for the purposes of this demo, I&amp;rsquo;ll copy the backup.tar (and my compose file) over to another VM and we&amp;rsquo;ll see if we can recreate this install.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d copied them over and &lt;a href="https://docs.docker.com/engine/install/debian/"&gt;installed Docker&lt;/a&gt;, I ran &lt;code&gt;docker compose up&lt;/code&gt; to start a new, empty Uptime Kuma. As expected, when I tried to visit the main page, it wanted me to create an admin user. Then I stopped the container. Note that you don&amp;rsquo;t want to &lt;code&gt;docker compose down&lt;/code&gt; to stop the container since that also removed it. If it&amp;rsquo;s removed, the next command won&amp;rsquo;t be able to find the name volumes it uses.&lt;/p&gt;
&lt;p&gt;Now we need copy the backed up data (which is just sitting in the current directory) into the named volume. Once again, this will be achieved by creating a new container, mounting the named volume and and current external working directory.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker run --rm --volumes-from uptime-kuma -v $(pwd):/backup ubuntu bash -c &amp;#34;cd /app &amp;amp;&amp;amp; tar xvf /backup/backup.tar --strip 1&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once again, I&amp;rsquo;ve highlighted the bits I&amp;rsquo;ve changed from the &lt;a href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"&gt;instructions&lt;/a&gt;. It&amp;rsquo;s important to note I&amp;rsquo;ve changed the destination directory. We backed up from &lt;code&gt;/app/data&lt;/code&gt; but we&amp;rsquo;re just restoring to &lt;code&gt;/app&lt;/code&gt; - the un-taring will copy the backed up data into the existing data directory. That&amp;rsquo;s a trick for young players - when I blindly followed the official instructions, I ended up with an &lt;code&gt;/app/data/data&lt;/code&gt; directory with the backed info which was, or course, ignored, and only discoverable buy &lt;code&gt;exec&lt;/code&gt;-ing into the container to see what was happening.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-11.34.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-11.34.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="why-not-just-copy-the-local-file-system-version"&gt;Why not just copy the local file system version?&lt;/h3&gt;
&lt;p&gt;The named docker volume is just stored on our local file system, usually at &lt;code&gt;/var/lib/docker/volumes&lt;/code&gt; so it would be reasonable to wonder why we don&amp;rsquo;t just copy that. I don&amp;rsquo;t have a great explanation for why not. I assume since the &lt;a href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"&gt;official docs&lt;/a&gt; suggest something different and more complex that there must be a reason. Possibly there&amp;rsquo;s some extra Docker magic (file locks, caching, etc) going on we don&amp;rsquo;t know about, or there&amp;rsquo;s some planned for the future.&lt;/p&gt;</description></item><item><title>Bruno asserts</title><link>https://blog.iankulin.com/bruno-asserts/</link><pubDate>Sat, 11 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bruno-asserts/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.11.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.11.09-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I mentioned &lt;a href="https://www.usebruno.com/"&gt;Bruno&lt;/a&gt; the other day. Although it&amp;rsquo;s still very much under development, it is shaping up as a great Postman/Insomnia replacement.&lt;/p&gt;
&lt;p&gt;One of the aspects I&amp;rsquo;ve been using today is asserts. As part of a request, you can add some asserts - so when you&amp;rsquo;re hitting an endpoint it will check what status should it be returning, or given the data you&amp;rsquo;re passing in, what should be in the response body.&lt;/p&gt;
&lt;p&gt;When I&amp;rsquo;d asked ChatGPT to to review the mdserver code, it had suggested that I should be sanitising URL inputs better to prevent users transversing out of the &amp;lsquo;public&amp;rsquo; file directory to other places in the file system. I thought Express had already taken care of this for me, but wanted to check. I had ChatGPT generate a bunch of pass and fail URL examples, then just created asserts for each one in Bruno.&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done, you can just right click on the collection and have it run all of those.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.19.59-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;An extra benefit of Bruno is that all these requests are stored as JSON-like, version-controllable text. I store them in my project and commit them along with the rest of my code.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.25.43-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.25.43-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Displaying markdown as HTML</title><link>https://blog.iankulin.com/displaying-markdown-as-html/</link><pubDate>Wed, 08 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/displaying-markdown-as-html/</guid><description>&lt;p&gt;In the spirit of over-complicating things, when I wanted to collect all the links to the services on my homelab into one place, I decided I needed to write them in markdown, and have them converted on the fly into HTML by a server. Then when I couldn&amp;rsquo;t find exactly what I was after (&lt;a href="http://harpjs.com/"&gt;Harp&lt;/a&gt; was closest) of course, I decided to write it.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/distracted.jpg" width="1000" alt=""&gt;
&lt;h3 id="markdown"&gt;Markdown&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Markdown"&gt;Markdown&lt;/a&gt; has definitely been having it&amp;rsquo;s moment over the last couple of years. It&amp;rsquo;s a simple open format mark-up language that is quite readable in it&amp;rsquo;s source form. Although it&amp;rsquo;s now very fashionable as an input for static site generators, most people will have run in to it when adding simple formatting to forum comments or on instant messaging platforms.&lt;/p&gt;
&lt;p&gt;It supports text formatting such as bold, italic and underlining as well as links, and in some extended versions, tables and so on.&lt;/p&gt;
&lt;h3 id="middleware"&gt;Middleware&lt;/h3&gt;
&lt;p&gt;My plan for tackling this is to have a simple Node.js/Express web server that&amp;rsquo;s serving static files from a public sub-directory. As it receives requests for each file, it checks if it&amp;rsquo;s a markdown file (which normally if served directly to a browser would trigger it to be downloaded instead of displayed). If it is markdown, it&amp;rsquo;s translated into HTML and passed to the browser.&lt;/p&gt;
&lt;p&gt;This is easily accomplished in a simple Express server which has the concept of &amp;lsquo;&lt;a href="https://expressjs.com/en/guide/using-middleware.html"&gt;middleware&lt;/a&gt;&amp;rsquo;. This are just layers of processing that each request goes through. If a layer can deal with a request it does, otherwise it passes it off to the next layer. You&amp;rsquo;ll often see this type of pattern (usually with more layers) in an Express app, where each of the &lt;code&gt;app.use&lt;/code&gt; declarations is another middleware layer:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const app = express();
app.use(mdParser);
app.use(express.static(publicDirectory));
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this case, &lt;code&gt;mdParser&lt;/code&gt; is my middleware function that checks if a file is markdown, then if it is returns a HTML version of the file to the browser, or if not, just lets the request go through to the next layer. A simple version might look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;fs&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;showdown&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;showdown&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;converter&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;showdown&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Converter&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;publicDirectory&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;staticRoot&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;__dirname&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;publicDirectory&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// middleware for processing markdown files
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mdParser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;next&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;url&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;endsWith&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;.md&amp;#39;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mdFilePath&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;staticRoot&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;url&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;readFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;mdFilePath&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;utf8&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;404&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;File not found&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;htmlContent&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;converter&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;makeHtml&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;htmlContent&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;next&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The actual heavy lifting of converting the markdown into HTML is done in line 20 with a library called &lt;a href="https://showdownjs.com/"&gt;ShowDown&lt;/a&gt;. There are a few of these floating around, I tried &lt;a href="https://marked.js.org/"&gt;Marked&lt;/a&gt; first, but it didn&amp;rsquo;t immediately work how I expected without reading any documentation, so I moved on ¯\_(ツ)_/¯&lt;/p&gt;
&lt;h3 id="templating"&gt;Templating&lt;/h3&gt;
&lt;p&gt;This simple version works - the markdown is correctly displayed in the browser, but there&amp;rsquo;s a couple of things going on that are not great.&lt;/p&gt;
&lt;p&gt;The first is that it&amp;rsquo;s not actually well formed HTML. If we load a markdown file containing this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Test.md

* A sample mark down file
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It looks like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-7.42.46-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-7.42.46-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But if we view the page source, it&amp;rsquo;s this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;h1 id=&amp;#34;testmd&amp;#34;&amp;gt;Test.md&amp;lt;/h1&amp;gt;
&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;A sample mark down file&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;No &lt;code&gt;DOCTYPE, &amp;lt;html&amp;gt;, &amp;lt;head&amp;gt;&lt;/code&gt; etc. Since forever, browsers have been expected to deal gracefully with malformed HTML, and they generally do, but as someone who still feels bound by the ethics printed on my 1991 &lt;a href="https://www.acs.org.au/content/dam/acs/rules-and-regulations/Code-of-Ethics.pdf"&gt;ACS membership certificate&lt;/a&gt;, I can&amp;rsquo;t accept this low standard.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a second related problem I don&amp;rsquo;t like, that&amp;rsquo;s that the title of this page (displayed in the browser tab, and used if we bookmark the page) is &amp;ldquo;http://127.0.0.1:3000&amp;rdquo; instead of what I would like it to be - probably &amp;ldquo;Test&amp;rdquo;. This is not Showdown&amp;rsquo;s fault, it doesn&amp;rsquo;t really have any way of guessing what we&amp;rsquo;d like for the title.&lt;/p&gt;
&lt;p&gt;As usual, these are a class of problem that&amp;rsquo;s long been solved, in this case with templates. Essentially what I need to do is take the generated (but not correctly formed) HTML output from Showdown, and insert it in the middle of some boilerplate HTML. Perhaps the template could look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt;
 &amp;lt;title&amp;gt;{{title}}&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
 &amp;lt;main&amp;gt;
 {{content}}
 &amp;lt;/main&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;d put the title I wanted for the page in &lt;code&gt;{{title}}&lt;/code&gt; and the converted markdown into &lt;code&gt;{{content}}&lt;/code&gt;. These double curly braces are a reasonably common convention for templating.&lt;/p&gt;
&lt;p&gt;If I load the template file (which can include all sorts of lovely CSS and JS) into &lt;code&gt;templateData&lt;/code&gt; at start up, I can just use a string replace when I need to serve the file at request time:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;if (useTemplate) {
 // Replace placeholders with title and content 
 const title = path.basename(mdFilePath);
 const templatedHtml = templateData.replace(&amp;#39;{{title}}&amp;#39;,
 title).replace(&amp;#39;{{content}}&amp;#39;,
 htmlContent);
 res.send(templatedHtml);
} else {
 res.send(htmlContent);
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m just using the file name for the title here, I&amp;rsquo;ll think about &lt;a href="https://github.com/IanKulin/mdserver/issues/1"&gt;how to improve that&lt;/a&gt; in a later installment.&lt;/p&gt;
&lt;p&gt;Now the output looks like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt;
 &amp;lt;title&amp;gt;test.md&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
 &amp;lt;main&amp;gt;
 &amp;lt;h1 id=&amp;#34;testmd&amp;#34;&amp;gt;Test.md&amp;lt;/h1&amp;gt;
&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;A sample mark down file&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
 &amp;lt;/main&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which, while &lt;a href="https://github.com/IanKulin/mdserver/issues/2"&gt;not well indented&lt;/a&gt;, at least meets the HTML specification.&lt;/p&gt;
&lt;h3 id="done"&gt;Done&lt;/h3&gt;
&lt;p&gt;I really enjoyed making this - it&amp;rsquo;s one of those compact sized projects you can start and finish on a Saturday between house jobs, and although small, it does address a genuine use case - if I&amp;rsquo;d found this when I was searching for something I would have used it as is.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/IanKulin/mdserver/blob/e9a09c4381e9bda373b86701f90cf165ea0d0e7e/server.js"&gt;code&amp;rsquo;s up on github&lt;/a&gt; if you want a look. To make it a finished product it probably needs some hardening. Also, since I need to learn how to build Docker containers, this would be a good project for that, so stand by for a future installment.&lt;/p&gt;</description></item><item><title>Ansible playbook to start Proxmox hosts</title><link>https://blog.iankulin.com/ansible-playbook-to-start-proxmox-hosts/</link><pubDate>Sun, 05 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ansible-playbook-to-start-proxmox-hosts/</guid><description>&lt;img src="https://blog.iankulin.com/images/mick-jagger-start-me-up-video-the-rolling-stones-far-out-magazine-copy.jpg" width="683" alt=""&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/proxmox-tags-to-solve-a-problem/"&gt;In my last post&lt;/a&gt;, I talked about tagging guests in a Proxmox node so I could easily see which VMs and LXCs I needed to manually start before I ran an Ansible script to run all my &lt;code&gt;apt updates&lt;/code&gt;. It would have been reasonable to wonder why I didn&amp;rsquo;t just add things to my playbook to magically do that.&lt;/p&gt;
&lt;p&gt;The answer would be, I haven&amp;rsquo;t gotten around to it yet, so here goes:&lt;/p&gt;
&lt;h3 id="modules"&gt;Modules&lt;/h3&gt;
&lt;p&gt;You might remember we discussed that the various functionalities for Ansible are in &lt;em&gt;modules&lt;/em&gt;. The modules for starting Proxmox guests are &lt;code&gt;[community.general.proxmox_kvm](https://docs.ansible.com/ansible/2.9/modules/proxmox_kvm_module.html)&lt;/code&gt; for VMs, and &lt;code&gt;[community.general.proxmox](https://docs.ansible.com/ansible/2.9/modules/proxmox_module.html)&lt;/code&gt; for LXC containers. If you look at the documentation for either of those, you&amp;rsquo;ll see a couple of prerequisites: &lt;em&gt;proxmoxer&lt;/em&gt; and &lt;em&gt;requests&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-8.18.46-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-8.18.46-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;requests&lt;/em&gt; is a common Python library (Ansible is actually running Python on the machines it&amp;rsquo;s configuring) for HTTP requests. We can ignore it since (a) you probably already have it installed, and (b) if not, when we install &lt;em&gt;proxmoxer&lt;/em&gt;, it will be installed as a dependency. You&amp;rsquo;ve probably already guessed that &lt;em&gt;proxmoxer&lt;/em&gt; is the Python library for interacting with Proxmox through it&amp;rsquo;s API.&lt;/p&gt;
&lt;p&gt;So before we can start any of the guests, we need to ensure proxmoxer is installed:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; tasks:

 - name: Install proxmoxer
 apt:
 name: python3-proxmoxer
 state: latest
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="my-ansible-setup"&gt;My Ansible setup&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s probably worth going over how my Ansible is set up so you can make sense of the rest of this without going back to read earlier posts. In the directory where I&amp;rsquo;m running this playbook, I have an &lt;code&gt;ansible.cfg&lt;/code&gt; file. Here&amp;rsquo;s the entire contents:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[defaults]
INVENTORY = hosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s an INI type file, and in this case it&amp;rsquo;s just saying if I don&amp;rsquo;t specify the name of an inventory file (a list of all my machines and their IP addresses or names), then use the file named &amp;lsquo;hosts&amp;rsquo;. This just saves me specifying the inventory file at the command line with the flag &lt;code&gt;-i&lt;/code&gt; each time.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;hosts&lt;/code&gt; file looks a bit like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[pve_dev1]
pve-dev1
#192.168.100.28

[pve_dev1:vars]
ansible_user=&amp;#39;{{pve_dev1_user}}&amp;#39;
ansible_become_password=&amp;#39;{{pve_dev1_become_pass}}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s a couple of these entries for every &amp;lsquo;machine&amp;rsquo; that I manage. The first bit just gives the address for the machine, and the second the variables for that machine - a sudo user and their password. You could just type those entries in here like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[pve_dev1]
pve-dev1
#192.168.100.28

[pve_dev1:vars]
ansible_user=root
ansible_become_password=password1234
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Instead of putting my credentials in a text file that&amp;rsquo;s pushed up to github, I use another file called a &amp;lsquo;vault&amp;rsquo; which is encrypted to keep them in. I&amp;rsquo;ve explained about &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;that elsewhere,&lt;/a&gt; but to understand what&amp;rsquo;s going on here, you just need to know that &lt;code&gt;'{{pve_dev1_user}}'&lt;/code&gt; gets resolved to &lt;code&gt;root&lt;/code&gt; when the playbook is run.&lt;/p&gt;
&lt;p&gt;You might also be wondering about the IP address that&amp;rsquo;s commented out in the snippets above. I am using the Tailscale MagicDNS on my machines, so I can just refer to this dev Proxmox instance as &lt;code&gt;pve-dev1&lt;/code&gt;, but yours is probably setup with IP address instead- in which case use that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[pve_dev1]
192.168.100.28

[pve_dev1:vars]
ansible_user=root
ansible_become_password=password1234
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So now the name being used in Ansible is pve_dev1, but it&amp;rsquo;s referring to the machine at 192.168.100.28&lt;/p&gt;
&lt;h3 id="starting-a-proxmox-vm"&gt;Starting a Proxmox VM&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; - name: start vm321-deb
 community.general.proxmox_kvm:
 api_user : root@pam
 api_password: &amp;#39;{{pve_dev1_become_pass}}&amp;#39;
 api_host : pve-dev1
 name : vm321-deb
 state : started
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The api_host is the address of the node, and the user and password above it are the same ones you use to log into the web gui of this Proxmox server. name is the you gave the VM in Proxmox when you created it. Note that this is for a stand-alone Proxmox server, not a node that&amp;rsquo;s part of a cluster. If we had a cluster called &amp;lsquo;mycluster&amp;rsquo; and the server/node that vm321-deb was hosted on was called &amp;rsquo;node2&amp;rsquo; the Ansible entry for it would be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; - name: start vm321-deb
 community.general.proxmox_kvm:
 api_user : root@pam
 api_password: &amp;#39;{{pve_dev1_become_pass}}&amp;#39;
 api_host : mycluster
 node : node2
 name : vm321-deb
 state : started
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="starting-an-lxc-container"&gt;Starting an LXC container&lt;/h3&gt;
&lt;p&gt;Increasingly, I run services in their own LXC container. They are quick to create and start, use less resources, but can still be snapshot-ed for easy backups.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; - name: start ct351-go
 community.general.proxmox:
 api_user : root@pam
 api_password: &amp;#39;{{pve_dev1_become_pass}}&amp;#39;
 api_host : pve-dev1
 vmid : 351
 state : started
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So for these containers, we use a different module, and call them by their VMID instead of name.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the full playbook.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Start pve-dev hosts for updating&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ansible-playbook start-apt-dev-vms.yaml --ask-vault-pass &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vars_files&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./vault.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hosts&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;become&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tasks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Install proxmoxer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;apt&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python3-proxmoxer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start babybuntu&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;babybuntu&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start vm321-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vm321-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start vm322-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vm322-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start vm323-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vm323-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start ct351-go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vmid &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;351&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start ct353-omada&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vmid &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;353&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start ct356-proxy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vmid &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;356&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Proxmox tags to solve a problem</title><link>https://blog.iankulin.com/proxmox-tags-to-solve-a-problem/</link><pubDate>Thu, 02 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-tags-to-solve-a-problem/</guid><description>&lt;p&gt;Each weekend I run an Ansible script that updates all my apt based VMs and containers. For the production machines, that&amp;rsquo;s everything, but my dev Proxmox is full of half-finished projects. Some of these have IP addresses reserved and are in the Ansible hosts file (because whatever service they are running is almost ready to move to the production server) others do not.&lt;/p&gt;
&lt;p&gt;Long story short, the dev server has some containers and VM&amp;rsquo;s that need turned on before I run the updates, and some that don&amp;rsquo;t. I could just start them all up, for the ten minutes the updates usually take, but that seems wasteful somehow. If there was only some way to mark the ones I need to turn on in the Proxmox webgui! Well, there is. We can add tags to machines in Proxmox.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-11.23.57-am-copy.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-11.23.57-am-copy.png" width="512" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Proxmox has quite a &lt;a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_tags"&gt;comprehensive tagging system&lt;/a&gt; - there are different display formats, and tags can be limited to a specific set, or completely free form. Also, there&amp;rsquo;s a heap of command line tools to work with them. For this job, I don&amp;rsquo;t really need much of that stuff - I just want to click a few things in the web gui to mark some of my VM&amp;rsquo;s with a coloured marker so I know which ones to start when I&amp;rsquo;m going to run my updates.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the steps.&lt;/p&gt;
&lt;p&gt;Go into &lt;code&gt;DataCenter | Options&lt;/code&gt;. One of the options is &lt;code&gt;Tag Style Override&lt;/code&gt;. It&amp;rsquo;s called &amp;ldquo;Override&amp;rdquo; because by default, the colours are deterministically figured out from the tag text. I want to just have a nice dark blue associated with the tag &lt;code&gt;apt&lt;/code&gt;, so I&amp;rsquo;m going to set it. It turns out I could have just skipped this step and got a nice light blue for &lt;code&gt;apt&lt;/code&gt;. This system (of just figuring out a colour from the text) means in most cases you can completely skip this step. Each machine you tag with a particular tag will be marked with the same colour - it will just work. &lt;code&gt;test&lt;/code&gt; = pink, &lt;code&gt;fred&lt;/code&gt; = green, and so on.&lt;/p&gt;
&lt;p&gt;Back to me being fussy. Opening up the &lt;code&gt;Tag Style Override&lt;/code&gt; I&amp;rsquo;m setting apt to be dark blue with white text.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-2.58.00-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-2.58.00-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To apply these tags, you just click on the machine you want to tag, then notice that up the top of the web gui, next to the machine name, it says &amp;ldquo;No Tags&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.07.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.07.11-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You just click on the pencil, and enter the tag name. If you haven&amp;rsquo;t changed any of the other defaults, a coloured circle will appear next to the machine in the server view.&lt;/p&gt;
&lt;p&gt;There are three display options for the tags - &amp;ldquo;full&amp;rdquo; which is a coloured bar including the text of the tag, &amp;ldquo;circle&amp;rdquo; which is the one shown in the first screenshot above, and &amp;ldquo;dense&amp;rdquo; which is a small rectangular bar - designed for stacking several different tags against each machine. All these options are under &amp;ldquo;tree shape&amp;rdquo; in the &lt;code&gt;Tag Color Override&lt;/code&gt; dialogue we opened earlier.&lt;/p&gt;
&lt;p&gt;As well as being able to see the tag blobs in the tree view, if you look at all your machines on the &lt;code&gt;Datacenter | Search&lt;/code&gt; view, it&amp;rsquo;s possible to sort by tags - which will even further simplify the job for me of starting them all up before I run the updates.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.35.21-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.35.21-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>apt update - BADSIG 871920D1991BC93C</title><link>https://blog.iankulin.com/apt-update-badsig-871920d1991bc93c/</link><pubDate>Mon, 30 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/apt-update-badsig-871920d1991bc93c/</guid><description>&lt;p&gt;I have an ansible script that runs each weekend which basically does an &lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -Y&lt;/code&gt; on every Debian based instance. This weekend it failed on one Ubuntu host. When I went it to try it manually, this was the output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Hit:1 http://au.archive.ubuntu.com/ubuntu jammy InRelease
Hit:2 https://download.docker.com/linux/ubuntu jammy InRelease 
Hit:3 http://au.archive.ubuntu.com/ubuntu jammy-backports InRelease 
Hit:4 http://au.archive.ubuntu.com/ubuntu jammy-security InRelease 
Get:5 http://au.archive.ubuntu.com/ubuntu jammy-updates InRelease [119 kB] 
Err:5 http://au.archive.ubuntu.com/ubuntu jammy-updates InRelease 
 The following signatures were invalid: BADSIG 871920D1991BC93C Ubuntu Archive Automatic Signing Key (2018) &amp;lt;ftpmaster@ubuntu.com&amp;gt;
Get:6 https://pkgs.tailscale.com/stable/ubuntu jammy InRelease
Fetched 125 kB in 1s (125 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
11 packages can be upgraded. Run &amp;#39;apt list --upgradable&amp;#39; to see them.
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://au.archive.ubuntu.com/ubuntu jammy-updates InRelease: The following signatures were invalid: BADSIG 871920D1991BC93C Ubuntu Archive Automatic Signing Key (2018) &amp;lt;ftpmaster@ubuntu.com&amp;gt;
W: Failed to fetch http://au.archive.ubuntu.com/ubuntu/dists/jammy-updates/InRelease The following signatures were invalid: BADSIG 871920D1991BC93C Ubuntu Archive Automatic Signing Key (2018) &amp;lt;ftpmaster@ubuntu.com&amp;gt;
W: Some index files failed to download. They have been ignored, or old ones used instead.
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="solved"&gt;Solved&lt;/h3&gt;
&lt;p&gt;The first &lt;a href="https://ubuntuforums.org/showthread.php?t=2484710"&gt;google result&lt;/a&gt; mentions apt-cache - which &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;I also run&lt;/a&gt;, so a first level debug step is to delete the &lt;code&gt;/etc/apt/apt.conf.d/00aptproxy&lt;/code&gt; file that redirects apt requests to the cache I run in an LXC container. After that, if I re-run the &lt;code&gt;apt update&lt;/code&gt; it works perfectly. Seems like a problem with the cache then. I&amp;rsquo;m not sure why it would only affect this host though - I have other Ubuntu VM&amp;rsquo;s in the fleet that are not getting the original error.&lt;/p&gt;
&lt;p&gt;In any case, adding the conf back to force the server to use the cache made the error reappear - so it&amp;rsquo;s definitely related to the cache. With any type of cache, when there&amp;rsquo;s a problem related to it, deleting the contents is usually a &amp;ldquo;plan A&amp;rdquo; response. Assuming there&amp;rsquo;s some mechanism in &lt;a href="https://wiki.debian.org/AptCacherNg"&gt;Apt Cacher NG&lt;/a&gt; to do this, I went to the little stats/config webpage it serves up.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-08-at-8.41.44-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well &amp;ldquo;Force the download of index files&amp;rdquo; sounds promising, let&amp;rsquo;s try that.&lt;/p&gt;
&lt;p&gt;I ticked the box for Force the download of index files (even having fresh ones), but it wasn&amp;rsquo;t clear to me how to make that change stick. The first button I could click further down the page was &amp;ldquo;Start Scan&amp;rdquo; which was related to some different checkboxes. I tried it anyway, but it didn&amp;rsquo;t force the downloading of index files. Time for some command line comandoing.&lt;/p&gt;
&lt;p&gt;The cache files for &lt;code&gt;aptcacher-ng&lt;/code&gt; are in &lt;code&gt;/var/cache/apt-cacher-ng/&lt;/code&gt; each distro has a directory in there.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-08-at-9.08.48-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Guessing the Ubuntu repository cache is probably stored in &lt;code&gt;uburep&lt;/code&gt;, I deleted that with &lt;code&gt;rm -R /var/cache/apt-cacher-ng/uburep&lt;/code&gt;. When I retried the &lt;code&gt;apt update&lt;/code&gt;, it worked perfectly, and I could see that the &lt;code&gt;/var/cache/apt-cacher-ng/uburep&lt;/code&gt; directory had re-appeared.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the immediate problem fixed. The cause of this problem is unclear. Presumably it related to a package running on this Ubuntu machine (runs docker with a couple of small services) that is not running on my other Ubuntu hosts. It probably falls into the category of &amp;ldquo;don&amp;rsquo;t worry about unless it crops up again&amp;rdquo;.&lt;/p&gt;</description></item><item><title>We need to talk about Bruno</title><link>https://blog.iankulin.com/we-need-to-talk-about-bruno/</link><pubDate>Fri, 27 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/we-need-to-talk-about-bruno/</guid><description>&lt;p&gt;&lt;a href="https://www.usebruno.com/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-6.01.17-pm.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/how-to-deploy-a-node-js-app/"&gt;mentioned before&lt;/a&gt; that I was using Insomnia as a tool to check my REST APIs as I was developing them, and that I was avoiding Postman (which I guess is more widely used since it&amp;rsquo;s worth &lt;a href="https://techcrunch.com/2021/08/18/api-platform-postman-valued-at-5-6-billion-in-225-million-fundraise/"&gt;USD5.6 billion&lt;/a&gt;) because&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The only reason I&amp;rsquo;m using Insomnia instead of Postman is that when I tried Postman, it straight away wanted some of my data to make it work. Insomnia hasn&amp;rsquo;t forced me to do that yet.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sadly that was prophetic. I saw &lt;a href="https://vmst.io/@wtpisaac"&gt;@wtpisaac@vmst.io&lt;/a&gt; &lt;a href="https://vmst.io/@wtpisaac/111150369449470670"&gt;mention this exact thing&lt;/a&gt; had happened.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-5.43.52-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The following day when I opened up Insomnia, it asked to upgrade and I said no, and was able to write and save a handful of new calls to test the API I was building. Sadly, I closed it, and of course on the next run, it demanded an account be created. I skipped that, and it presented me with a &amp;ldquo;sandbox&amp;rdquo; and all my saved requests were gone.&lt;/p&gt;
&lt;p&gt;Luckily, Isaac also suggests a solution - &lt;a href="https://www.usebruno.com/"&gt;Bruno&lt;/a&gt;. This is a 1K star FOSS project by &lt;a href="https://github.com/helloanoop"&gt;helloanoop&lt;/a&gt;. There are Mac, Windows and Linux clients, as well as a CLI and VSCode plugins. One of it&amp;rsquo;s selling points (apart from it&amp;rsquo;s not Postman or Insomnia) is that the collections of requests are saved in very simple human readable text (a language called &lt;em&gt;bru&lt;/em&gt;) so it&amp;rsquo;s straightforward and sensible to commit them to source control along with your code.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-6.14.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-6.14.09-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This video from Anoop (with a very clickbait-y title) does a good job of explaining his project.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/b_ctmKlEOXg?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;I&amp;rsquo;ve only played around with Bruno for an afternoon, but I&amp;rsquo;m loving it so far. Seems like it will do everything I need, and the diffable files for the requests are a bonus. This is a project that deserves to be better known.&lt;/p&gt;</description></item><item><title>Tailscale keys expire</title><link>https://blog.iankulin.com/tailscale-keys-expire/</link><pubDate>Tue, 24 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/tailscale-keys-expire/</guid><description>&lt;p&gt;I have an &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;Ansible playbook&lt;/a&gt; I run each weekend to do all the &lt;code&gt;apt&lt;/code&gt; updates. As well as keeping everything up to date, it&amp;rsquo;s a good check-in that everything&amp;rsquo;s alive and working as expected. I have Uptime Kuma checking the services are alive, and that no one is running out of disk or memory so there shouldn&amp;rsquo;t be any drama right?&lt;/p&gt;
&lt;p&gt;This weekend, three instances (two remote, one local) timed out with &amp;ldquo;unreachable&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-09-30-at-2.53.24-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-30-at-2.53.24-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since Ansible is effectively ssh-ing in, I guess try that from the terminal.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-30-at-2.58.01-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vm100-dockhost&lt;/code&gt; is the &amp;ldquo;magic DNS&amp;rdquo; name for this machine. One of the cool things Tailscale does is to allow these sorts of names. I use them so much, I&amp;rsquo;ve forgotten all their IP addressees. When I look it up and try with the local IP address for this machine, it works fine.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/itcrowd.jpg" width="872" alt=""&gt;
&lt;p&gt;Since it seems like a Tailscale problem, I tried turning it off and on again with &lt;code&gt;sudo tailscale down&lt;/code&gt; and &lt;code&gt;sudo tailscale up&lt;/code&gt;. When it came up, it printed the URL to re-authenticate - so something&amp;rsquo;s happened&amp;hellip;&lt;/p&gt;
&lt;p&gt;It turns out that &lt;a href="https://tailscale.com/kb/1028/key-expiry/"&gt;Tailscale keys expire&lt;/a&gt; for security reasons - by default every 180 days. Once the key is expired, you can&amp;rsquo;t access that machine via the Tailnet. Obviously, this is going to make an issue if you have a remote site and the key expires. So how can we prevent it from happening?&lt;/p&gt;
&lt;p&gt;My first idea was to use the Tailscale CLI to do the re-authentication on each machine &lt;em&gt;before&lt;/em&gt; it expires. And handily, there is a command for this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;tailscale up --force-reauth
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But, small catch (mentioned in the &lt;a href="https://tailscale.com/kb/1028/key-expiry/"&gt;docs&lt;/a&gt;, or in the CLI if you try it) if you are ssh&amp;rsquo;d in over Tailscale, when you run this, it actually drops the ssh link. So you&amp;rsquo;ll never see the URL you need to re-authorise, so now you&amp;rsquo;ve lost access to that machine.&lt;/p&gt;
&lt;p&gt;If a key has expired, it is possible to remotely reauthorise it from your &lt;a href="https://login.tailscale.com/admin/machines"&gt;machines admin page&lt;/a&gt; for a short period it to allow someone with local access to reauthorise it properly. If you don&amp;rsquo;t have local access to it, you&amp;rsquo;re in trouble if you discover this after it&amp;rsquo;s expired. I guess it would be possible to write a script to run the &lt;code&gt;tailscale up&lt;/code&gt; on the remote machine, capture the output and send it to me, but that&amp;rsquo;s starting to sound like more work than I want to do.&lt;/p&gt;
&lt;h3 id="avoiding-the-problem"&gt;Avoiding the problem&lt;/h3&gt;
&lt;p&gt;If you want to avoid the problem of Tailscale keys expiring on remote systems, it&amp;rsquo;s possible to turn it off so they never expire. This option is in the menu for each machine on the &lt;a href="https://login.tailscale.com/admin/machines"&gt;machines admin page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-4.38.33-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-4.38.33-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I guess another way of avoiding this problem, if it&amp;rsquo;s possible, would be to visit your remote sites every six months and do the force update to reset the expiry. For my setup of the remote backup sites that&amp;rsquo;s a reasonable plan.&lt;/p&gt;
&lt;p&gt;One slightly annoying thing is that it&amp;rsquo;s not easy to see the expiry date of each Tailscale instance. I would have thought it would appear on that machines admin page, or in the CLI with &lt;code&gt;tailscale status&lt;/code&gt;. When I was searching for an answer, I see that there is an &lt;a href="https://github.com/tailscale/tailscale/issues/4854"&gt;open github issue&lt;/a&gt; for it, and there&amp;rsquo;s been an update to the JSON version of the &lt;code&gt;tailscale status&lt;/code&gt; command that includes the key expiry date.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-5.33.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-01-at-5.33.54-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>New Project Routine</title><link>https://blog.iankulin.com/new-project-routine/</link><pubDate>Sat, 21 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/new-project-routine/</guid><description>&lt;p&gt;I have a sort of muscle memory for starting little web projects now. I seem to have landed on node/express SSR apps with HTMX sprinkles. So it goes a bit like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a working directory - all lower case with a simple, but unlikely to be duplicated by me, name.&lt;/li&gt;
&lt;li&gt;Open the directory in vscode&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm init&lt;/code&gt; in the directory to create the &lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;create a &lt;code&gt;public&lt;/code&gt; sub directory, and drop &lt;a href="https://htmx.org/docs/#installing"&gt;&lt;code&gt;htmx.min.js&lt;/code&gt;&lt;/a&gt; in there, and create a &lt;code&gt;styles.css&lt;/code&gt; there. I&amp;rsquo;m always conflicted about what to do about this htmx dependency. I&amp;rsquo;d rather host it rather than use their CDN because &lt;a href="https://blog.wesleyac.com/posts/why-not-javascript-cdn"&gt;reasons&lt;/a&gt;. But I also feel bad about committing it on Github. I could .gitignore it, but then when I clone the project on the production server I&amp;rsquo;d need to add another step to download it. HTMX is only 44K, and Microsoft can afford the bandwidth, so for the moment I commit them, but I need a better solution for the future.&lt;/li&gt;
&lt;li&gt;using the git tools in vscode, add &lt;code&gt;.DS_Store&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; (which also creates it), then edit it to also ignore &lt;code&gt;node_modules&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm install express&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm install ejs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;create a server.js, and add the &lt;a href="https://nodejs.org/en/docs/guides/getting-started-guide"&gt;hello world&lt;/a&gt; code&lt;/li&gt;
&lt;li&gt;create a &lt;code&gt;readme.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;commit these files as &amp;ldquo;initial&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Create the repo on github with the same name - no readme and no licence. I do it this way for a couple of reasons - I want to find out at this point if I&amp;rsquo;ve already used this repo name, and I want it to give me the cut and paste commands to push the repository.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-25-at-9.55.46-am.png" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do those in the terminal.&lt;/li&gt;
&lt;li&gt;Refresh the github page, and add the licence by &lt;code&gt;Add File&lt;/code&gt;, name it LICENSE - this lets you choose the template you want. What I&amp;rsquo;d really like here is &amp;ldquo;GPL3 but giant cloud companies can&amp;rsquo;t make money from hosting it&amp;rdquo; - which I guess would be called the MongoDB license or something.&lt;/li&gt;
&lt;li&gt;Do &lt;code&gt;git pull&lt;/code&gt; in the terminal to check that&amp;rsquo;s all working&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nodemon ./server.js&lt;/code&gt; then command click on the link to check everything&amp;rsquo;s working&lt;/li&gt;
&lt;li&gt;profit&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="express-skeleton"&gt;Express Skeleton&lt;/h3&gt;
&lt;p&gt;That&amp;rsquo;s my basic web app setup, but since this is an express app, and we&amp;rsquo;re using some EJS templating, there&amp;rsquo;s some other starter files I like to create. Let&amp;rsquo;s start with our pages. I&amp;rsquo;ll need an index and a 404 page, and my pages are all going to have a header section as well as a nav and a footer. Something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;─── views
 ├── 404.ejs
 ├── index.ejs
 └── partials
    ├── footer.ejs
    ├── head.ejs
   └── nav.ejs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To give you a flavour of how that all works, here&amp;rsquo;s a sample &lt;code&gt;index.ejs&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
&amp;lt;%- include(&amp;#39;./partials/head.ejs&amp;#39;) %&amp;gt;
 &amp;lt;body&amp;gt;
 &amp;lt;%- include(&amp;#39;./partials/nav.ejs&amp;#39;) %&amp;gt;
 &amp;lt;div class=&amp;#34;content&amp;#34;&amp;gt;
 &amp;lt;h2&amp;gt;Hello world&amp;lt;/h3&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;%- include(&amp;#39;./partials/footer.ejs&amp;#39;) %&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we need some basic routing in &lt;code&gt;server.js&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#39;express&amp;#39;);
const app = express();
 
const hostname = &amp;#39;127.0.0.1&amp;#39;;
const port = 3000;

app.set(&amp;#39;view engine&amp;#39;, &amp;#39;ejs&amp;#39;);
app.use(express.static(&amp;#39;public&amp;#39;));

app.get(&amp;#39;/&amp;#39;, (req, res) =&amp;gt; {
 res.render(&amp;#39;index&amp;#39;, { title: &amp;#39;Index&amp;#39;});
 });

//404 handling
app.use(function (req, res, next) {
 res.status(404).render(&amp;#39;404&amp;#39;, { title: &amp;#39;404&amp;#39;, url: req.url });
});

app.listen(port, hostname, () =&amp;gt; {
 console.log(`Server running at http://${hostname}:${port}/`);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And, lastly, a bit of CSS to make it beautiful.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@viewport {
 width: device-width ;
 zoom: 1.0 ;
} 

body{
 max-width: 1200px;
 font-family: Tahoma, Arial, Helvetica, sans-serif;
 margin: 0;
}

nav {
 position: fixed; 
 top: 0; 
 width: 100%; 
 overflow: hidden;
 background-color: #EEE;
}

nav li {
 display: inline-block;
 padding: 0;
}

nav a {
 display: inline;
 color: #333;
 text-align: center;
 padding: 17px 8px;
 text-decoration: none;
}

nav a:hover {
 background: #ddd;
 color: black;
}

nav ul {
 padding-inline-start: 4px;
}

/* push content down below the nav bar */
.content {
 padding: 50px 10px 10px 10px;
}

footer {
 width:100%;
 position:absolute;
 bottom:0;
 left:0;
 color: #757171;
 text-align: center;
 margin: 80px auto 20px;
 background-color: #EEE;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;chefs_kiss.jpg&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-25-at-11.54.42-am.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Getting Tailscale working in LXC containers</title><link>https://blog.iankulin.com/getting-tailscale-working-in-lxc-containers/</link><pubDate>Wed, 18 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-tailscale-working-in-lxc-containers/</guid><description>&lt;p&gt;I&amp;rsquo;ve taken to running lots of my services in LXC containers under Proxmox. I like the feeling of installing in a VM, but it&amp;rsquo;s lightweight. I like the backups, I like things being isolated from each other, I like moving them around between machines easily. I&amp;rsquo;m just a big LXC lover at the moment.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also a Tailscale lover, and the generous number of nodes in the free tier means I now just routinely install them in my VMs and containers without a thought.&lt;/p&gt;
&lt;p&gt;There is an issue with unprivileged LXC containers and Tailscale though. Unprivileged containers have less access to the host system&amp;rsquo;s internals, and are therefore a bit safer, but part of that reduced access includes some of the networking stuff that Tailscale needs. If you try to install Tailscale, it will look fine, until you get to the &lt;code&gt;tailscale up&lt;/code&gt; command, at which point it will say something like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;failed to connect to local tailscaled (which appears to be running as tailscaled, pid 3121). Got error: 503 Service Unavailable: no backend
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There is an easy way to fix this, documented in a &lt;a href="https://tailscale.com/kb/1130/lxc-unprivileged/"&gt;Tailscale how to guide&lt;/a&gt;. Basically you need to stop the container and edit the LXC conf file. These are named by the container number. My container is 354, so the conf file is &lt;code&gt;/etc/pve/lxc/354.conf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Add the lines:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-19-at-8.01.13-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This creates a TUN/TAP device (commonly used for VM networking) and creates a bind point to it inside the container. The effect of this is to enable the container to work with TUN/TAP devices and use them for networking purposes. This can be essential for various networking-related applications or services running within the container - including, in this case, Tailscale.&lt;/p&gt;
&lt;p&gt;Start the container again, redo your &lt;code&gt;tailscale up&lt;/code&gt;, and you should be in business.&lt;/p&gt;</description></item><item><title>Certbot - adding more virtual hosts</title><link>https://blog.iankulin.com/certbot-adding-more-virtual-hosts/</link><pubDate>Sun, 15 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/certbot-adding-more-virtual-hosts/</guid><description>&lt;p&gt;I&amp;rsquo;ve got a domain that&amp;rsquo;s not currently used, so I&amp;rsquo;m going to set it up as a virtual host under NGINX. This server is already serving two domains set up with Certbot for SSL. Is it going to be possible to add another site and have Certbot manage the certificates for it after I&amp;rsquo;ve run Certbot once?&lt;/p&gt;
&lt;p&gt;When I googled around to find out, I didn&amp;rsquo;t find anything - which is usually a sign I&amp;rsquo;m either asking a wrong question, or it&amp;rsquo;s so little drama that no one ever mentions it. I decided just to move the site, check it was all working for the http version, then run Certbot and see what it said.&lt;/p&gt;
&lt;p&gt;Since I already had Certbot installed, I just ran &lt;code&gt;sudo certbot --nginx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-09-03-at-10.03.19-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-03-at-10.03.19-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s probably worth explaining at this point that Certbot does not obtain separate certificates for each domain (which is what I&amp;rsquo;d been doing when I was doing this manually), but instead grabs a single certificate that includes all the domains, and stores it under the the first domain - in the case above, for agnet.&lt;/p&gt;
&lt;p&gt;I hit &amp;ldquo;E&amp;rdquo; for Expand, and Certbot did it&amp;rsquo;s thing by acquiring the new certificate expanded to cover the new domain and installed it. No drama.&lt;/p&gt;
&lt;h3 id="what-if-you-already-have-a-certificate-from-another-provider"&gt;What if you already have a certificate from another provider?&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve got two more domains to move from another server, but both of these already have active SSL certificates that I obtained via Porkbun. Is that going to be a problem? Can Let&amp;rsquo;s Encrypt (who actually does the certificates for Porkbun) include these sites on the combined certificate on my main VPS so I can use Certbot to maintain them? Let&amp;rsquo;s see.&lt;/p&gt;
&lt;p&gt;I went through the same routine - created a nginx conf for the virtual host in &lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;, created a simple index.html in &lt;code&gt;/var/www/drysea.xyz&lt;/code&gt; and then symlinked the conf file into &lt;code&gt;/etc/nginx/sites-enabled&lt;/code&gt;. Then changed the A records for the DNS to point to the server address and waited for them to propagate so I could test the http version of the site.&lt;/p&gt;
&lt;p&gt;After that, I ran the sudo certbot &amp;ndash;nginx command again, and exactly as before, it asked if I wanted to expand the existing certificate. I did that, and the site can now be visited securely with no warning about the incorrect certificate. So that&amp;rsquo;s all worked well.&lt;/p&gt;
&lt;p&gt;It is allowable for a site to have more than one active, valid SSL certificate. This often happens in the exact scenario we&amp;rsquo;ve got here where domains are being moved around. There is a security implication for this though. A &lt;a href="https://www.csoonline.com/article/561111/dns-record-will-help-prevent-unauthorized-ssl-certificates.html"&gt;system&lt;/a&gt; of entering a particular DNS record that would prevent certificates being issued by all but one particular certificate authority exists, but is not widely used.&lt;/p&gt;
&lt;p&gt;It is probably a good idea for my to change my configuration on Porkbun to stop it from going on generating certificates that are not needed though, so I&amp;rsquo;ll go ahead and revoke that.&lt;/p&gt;</description></item><item><title>Certbot &amp; Let's Encrypt are great</title><link>https://blog.iankulin.com/certbot-lets-encrypt-are-great/</link><pubDate>Thu, 12 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/certbot-lets-encrypt-are-great/</guid><description>&lt;img src="https://blog.iankulin.com/images/certbot.png" width="847" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve been managing SSL certificates for my domains purchased from &lt;a href="https://porkbun.com/"&gt;PorkBun&lt;/a&gt; by going there every 90 days downloading the certificates, &lt;a href="https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/"&gt;joining them together&lt;/a&gt; to make the &lt;code&gt;fullchain.pem&lt;/code&gt; then &lt;code&gt;scp&lt;/code&gt;-ing them to my servers. That&amp;rsquo;s been sort of manageable, but less than ideal.&lt;/p&gt;
&lt;p&gt;It also doesn&amp;rsquo;t work for my Australian domains. Since there&amp;rsquo;s strict rules about who can own a domain in the &lt;code&gt;.au&lt;/code&gt; space (&lt;em&gt;you have to have some sort of right to the name - a random person can&amp;rsquo;t obtain the &lt;code&gt;coke.com.au&lt;/code&gt; domain unless that&amp;rsquo;s a trading name, a trademark, or something similar&lt;/em&gt;), they have to be managed by one of about eight organisations, and the offerings are much simpler.&lt;/p&gt;
&lt;p&gt;No problem though for two wonderful reasons - &lt;a href="https://letsencrypt.org/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; and &lt;a href="https://certbot.eff.org/"&gt;Certbot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let’s Encrypt is a free, automated, and open certificate authority (CA), run for the public’s benefit. It is a service provided by the Internet Security Research Group. They provide free TLS certificates to allow websites to use SSL.&lt;/p&gt;
&lt;p&gt;Certbot, managed by the Electronic Frontiers Foundation, is a utility to automatically obtain certificates for a website from Let&amp;rsquo;s Encrypt, and change the server configuration files to use them.&lt;/p&gt;
&lt;p&gt;This makes this whole process amazingly painless. There&amp;rsquo;s really no excuse for not adding this to your websites, and I&amp;rsquo;d highly encourage you to donate to both projects if you use Certbot.&lt;/p&gt;
&lt;h2 id="certbot"&gt;Certbot&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m running NGINX on Ubuntu LTS on my VPS&amp;rsquo;s, so installation was a snap (pun intended). I just followed the &lt;a href="https://certbot.eff.org/instructions?ws=nginx&amp;amp;os=ubuntufocal"&gt;instructions&lt;/a&gt; which involved installing the snap, adding a symlink to ensure it was in my path, then running the bot passing it a flag to say I was using NGINX.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-09-02-at-4.35.25-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-02-at-4.35.25-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It asks you a couple of questions, intelligently (by reading all the nginx conf files) then downloads the certificates and edits the nginx site conf files to use them. It also adds a systemd timer command to automate checking to see if they need renewed every couple of hours.&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done, you just go back to your website and you&amp;rsquo;ve got the magical padlock, and won&amp;rsquo;t have to worry about it again due to the automatic renewal.&lt;/p&gt;</description></item><item><title>BOINC in an LXC container</title><link>https://blog.iankulin.com/boinc-in-an-lxc-container/</link><pubDate>Mon, 09 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/boinc-in-an-lxc-container/</guid><description>&lt;img src="https://blog.iankulin.com/images/boinc_logo.png" width="900" alt=""&gt;
&lt;p&gt;Years ago, I was very keen on the &lt;a href="https://youtu.be/WwxTc6pFOcU"&gt;SETI@home&lt;/a&gt; project that used a distributed computing model whereby packets of digitized received radio data were farmed out to individuals&amp;rsquo; computers to be processed to look for any unusual signals that could potentially be from an intelligent extra-terrestrial source.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s long since defunct, but the idea lives on with &lt;a href="https://boinc.berkeley.edu/"&gt;BOINC&lt;/a&gt; - a system run out of Berkley that allows different science organisations to offer projects to run on individuals&amp;rsquo; computers.&lt;/p&gt;
&lt;p&gt;I thought that figuring out how to get all that running in an LXC container would make a good blog post, and wasted about a day fiddling around with it, with limited success. I forget the exact details, but I think the projects I&amp;rsquo;d subscribed to via the &lt;a href="https://www.worldcommunitygrid.org/"&gt;World Community Grid&lt;/a&gt; might have wanted serious GPU power which my container does not have - but I wasn&amp;rsquo;t 100% sure I&amp;rsquo;d set everything up correctly. There was so many fiddly variables I wasn&amp;rsquo;t confident to commit to posting about it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s my custom on the weekends to turn all my nodes on, and start every VM and container, even the testing ones on the dev node, then run the &lt;a href="https://blog.iankulin.com/tags/ansible/"&gt;Ansible playbook&lt;/a&gt; to do all of the &lt;code&gt;apt&lt;/code&gt; updates. When I did that today, I noticed this CPU pulsing:&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/WwxTc6pFOcU?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;Well, that seems like it&amp;rsquo;s doing some serious work. Either I&amp;rsquo;ve been hacked and someone&amp;rsquo;s mining crypto, or BOINC is working.&lt;/p&gt;
&lt;p&gt;Each of the organisations enrolled in BOINC have a community page where you sign up and get an API key that identifies your computers to the project, and you can head there to see your contributions. Sure enough, I&amp;rsquo;ve been receiving, processing and returning packets.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-27-at-7.12.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-27-at-7.12.05-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is another thing I&amp;rsquo;d like to return to later - I don&amp;rsquo;t think it was as simple as following the &lt;a href="https://boinc.berkeley.edu/wiki/Installing_BOINC_on_Debian_or_Ubuntu"&gt;instructions&lt;/a&gt; because I&amp;rsquo;d made my life a bit more complicated by running it in an LXC. It also occurs to me that this might be a good workload to use an orchestration tool like Kubernetes for - since I don&amp;rsquo;t really have any actual need (excuse) to play with those.&lt;/p&gt;</description></item><item><title>Solved DNS Issues - Proxmox, LXC, Ubuntu, Tailscale</title><link>https://blog.iankulin.com/solved-dns-issues-proxmox-lxc-ubuntu-tailscale/</link><pubDate>Fri, 06 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/solved-dns-issues-proxmox-lxc-ubuntu-tailscale/</guid><description>&lt;p&gt;&lt;a href="https://i.imgur.com/WmRbmf5.png"&gt;&lt;img src="https://blog.iankulin.com/images/wmrbmf5.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve picked up an new TP-Link WAP with Omada, so I wanted to spin up an Ubuntu 20.04 LXC to run the controller software in, and ended up spending a couple of hours figuring out why things where not working.&lt;/p&gt;
&lt;p&gt;The initial problem was I was having connectivity issues pulling down the updates for all the packages required. I went down a bit of a tangent because I installed an apt cache the other day, so I was looking for problems there. Eventually I narrowed it down to DNS not working and started A/B testing like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-26-at-4.49.24-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A more seasoned sysadmin probably would have been looking at the &lt;code&gt;/etc/resolv.conf&lt;/code&gt; a bit earlier where the glaring hint was. I&amp;rsquo;ll get to that in a second, but first a bit about my setup.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running Proxmox 8.0.4 on one of my HP G2 800 Minis (love these little power-frugal &lt;a href="https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/"&gt;gems&lt;/a&gt;) and I use Tailscale to tie all my network (my homelab here, and two remote locations) together. The Tailscale version on this node is 1.48.1&lt;/p&gt;
&lt;p&gt;You can see in the table above, that a LXC using the Ubuntu 20.04 template had no domain name resolution, but the Debian 12 (and Debian 11 I tried earlier did). The &lt;code&gt;/etc/resolv.conf&lt;/code&gt; on the Debian containers looked like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nameserver 192.168.100.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And on the Ubuntu container&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# --- BEGIN PVE ---
search tailaf96a.ts.net
nameserver 100.100.100.100
# --- END PVE ---
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;192.168.100.1&lt;/code&gt; is my local DNS which is provided from the DHCP, but clearly Ubuntu is not using that. The &lt;code&gt;PVE&lt;/code&gt; comments tells me it&amp;rsquo;s Proxmox messing with my container, and that&amp;rsquo;s the Tailscale DNS server number in there. The container does not have a route to &lt;code&gt;100.100.100.100&lt;/code&gt; so that DNS is not going to be able to resolved anything.&lt;/p&gt;
&lt;p&gt;So, that&amp;rsquo;s a bit weird, but easily fixed by just editing this back to set the nameserver to &lt;code&gt;192.160.100.1&lt;/code&gt; right? Well, yes - if you do that, it works, but then as soon as the container is rebooted, the Tailnet DNS gets written back in. Those blocky PVE comments are probably part of the automated system for doing that. So, what&amp;rsquo;s going on here?&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s two screens for network configuration when you&amp;rsquo;re creating an &lt;a href="https://en.wikipedia.org/wiki/RAS_syndrome"&gt;LXC container&lt;/a&gt; in the Proxmox GUI.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-26-at-4.55.54-pm-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-26-at-4.56.03-pm-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no option in the GUI to just say &lt;em&gt;&amp;ldquo;Use the DNS settings provided by the DHCP server&amp;rdquo;&lt;/em&gt;, although we&amp;rsquo;ll see later, there is a work around for this.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;d been leaving the &lt;code&gt;DNS domain:&lt;/code&gt; set to &lt;code&gt;use host settings&lt;/code&gt;. You might reasonably wonder what the Proxmox node /etc/resolv.conf looks like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# resolv.conf(5) file generated by tailscale
# For more info, see https://tailscale.com/s/resolvconf-overwrite
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN

nameserver 100.100.100.100
search tailaf96a.ts.net local
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So actually, although I was thinking there must be some bug with Ubuntu since Debian was working how I expected, it&amp;rsquo;s the other way around - Ubuntu and Proxmox are working together to do exactly what the settings have told it to - to use the host settings. And actually, the Debian containers are not working correctly (although they were working how I expected). The process of Proxmox making these types of changes is documented in the &lt;a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_guest_operating_system_configuration"&gt;Admin Guide&lt;/a&gt;. I&amp;rsquo;d actually never seen that guide till today (although there is a large &amp;ldquo;Documentation&amp;rdquo; button in the top right of the web GUI), but it looks pretty great so I&amp;rsquo;ll be revisiting it.&lt;/p&gt;
&lt;h3 id="solution-1"&gt;Solution 1&lt;/h3&gt;
&lt;p&gt;The first solution is just to specify the DNS address in the GUI - then our container works exactly as the PVE developers intended. A slight downside is that if I change the network configuration in future and update the DNS address in the DHCP server (which is the logical way to do that) then it won&amp;rsquo;t update for this container and domain name resolution will stop working for it.&lt;/p&gt;
&lt;p&gt;If I do that, the &lt;code&gt;/etc/resolv.conf&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# --- BEGIN PVE ---
search tailaf96a.ts.net
nameserver 192.168.100.1
# --- END PVE ---
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And it all works fine.&lt;/p&gt;
&lt;h3 id="solution-2"&gt;Solution 2&lt;/h3&gt;
&lt;p&gt;This &lt;a href="https://forum.proxmox.com/threads/lxc-dns-from-dhcp.36200/"&gt;post on the Proxmox Forums&lt;/a&gt; lead me to a second solution. It&amp;rsquo;s possible to stop Proxmox from adding the host by adding a little signal file with&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;touch /etc/.pve-ignore.resolv.conf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When Proxmox sees that. it won&amp;rsquo;t mess with the &lt;code&gt;/etc/resolv.conf&lt;/code&gt; file, so if that&amp;rsquo;s been edited to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nameserver 192.168.100.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It will be left alone, and things will work fine. This is not quite what I&amp;rsquo;d like - I&amp;rsquo;d really prefer it picks everything up from DHCP, but I don&amp;rsquo;t know enough about how that works in Linux to fix it, yet.&lt;/p&gt;</description></item><item><title>Caching APT updates</title><link>https://blog.iankulin.com/caching-apt-updates/</link><pubDate>Tue, 03 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/caching-apt-updates/</guid><description>&lt;p&gt;It&amp;rsquo;s bothered me for a while that all these VM&amp;rsquo;s are pulling down a lot of the same updates. As well as needlessly using some bandwidth, I&amp;rsquo;m hammering the update servers (that I don&amp;rsquo;t pay for) with the same requests over and over. I did briefly consider running my own mirror, but that&amp;rsquo;s not simple, plus I&amp;rsquo;d then be mirroring a heap of files in a complete repository that I&amp;rsquo;d never use. What I really needed was some sort of cache so once I&amp;rsquo;ll pulled down an update, it would hang around for a few days being available to other machines on the local network. Luckily, that exact thing exists - &lt;a href="https://www.unix-ag.uni-kl.de/~bloch/acng/html/index.html"&gt;APT Cacher NG&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It works pretty much as described above - all of the machines on the LAN have their APT calls proxied through a little server. If the server doesn&amp;rsquo;t have a copy of the appropriate package, it pulls it down and delivers it. If it&amp;rsquo;s got a good copy already, it just provides that.&lt;/p&gt;
&lt;h3 id="installing-the-server"&gt;Installing the server&lt;/h3&gt;
&lt;p&gt;I decided an unprivileged LXC container would be the perfect base for this service. I created one from the Debian 12 image with 1MB RAM but a largish 30GB drive. I don&amp;rsquo;t really have any feel for how big the cache will get under normal use so I erred on the large side. It&amp;rsquo;s not doing any computationally expensive work, so one CPU is plenty.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.42.06-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.42.06-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then we just install it, and start and enable it as a service.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt install apt-cacher-ng
systemctl start apt-cacher-ng
systemctl enable apt-cacher-ng
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;During the install, it asked me about https:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.46.32-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.46.32-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I said no, but then to enable it, I had to (after the installation) edit the config file at &lt;code&gt;/etc/apt-cacher-ng/acng.conf&lt;/code&gt; to uncomment the line&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PassThroughPattern: .* # this would allow CONNECT to everything&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With that done, and the service restarted, we&amp;rsquo;re now serving proxies at localhost:3142, and also a little web page with some advice.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-2.37.46-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="installing-the-client"&gt;Installing the client&lt;/h3&gt;
&lt;p&gt;The two bits of information I&amp;rsquo;ve put red boxes around are the things we need to do to enable &lt;code&gt;apt&lt;/code&gt; on the client machines to use the cache. We need to create a file called &lt;code&gt;/etc/apt/apt.conf.d/00aptproxy&lt;/code&gt; and add the single line to it of:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Acquire::http::Proxy &amp;quot;http://192.168.100.37:3142&amp;quot;;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Note that the ip address will be different on yours, just copy it off the little web page. Since I&amp;rsquo;ve got a heap of machines to do this do, I made the &lt;code&gt;conf&lt;/code&gt; file once and pushed out out with Ansible.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.00.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.00.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;hosts: local&lt;/code&gt; in the pkaybook refers to the &lt;code&gt;local: children&lt;/code&gt; group in my hosts ini file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.14.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="statistics"&gt;Statistics&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re curious to see what the savings are, there&amp;rsquo;s another web page served by the cache at &lt;code&gt;&amp;lt;server ip&amp;gt;:3142/acng-report.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.16.27-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Further down on that page are some options that can be changed as well.&lt;/p&gt;
&lt;h3 id="resources"&gt;Resources&lt;/h3&gt;
&lt;p&gt;I learned most of this from &lt;a href="https://www.youtube.com/watch?v=t8kI4YwdvRA"&gt;this video by RickMakes&lt;/a&gt;, and &lt;a href="https://www.linuxhelp.com/how-to-set-up-apt-caching-server-using-apt-cacher-ng-on-debian-11-3"&gt;this Linux Help page&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Installing service with Ansible</title><link>https://blog.iankulin.com/installing-service-with-ansible/</link><pubDate>Sat, 30 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/installing-service-with-ansible/</guid><description>&lt;p&gt;Having written my little monitoring endpoint in Go, it needs pushed out to all my servers and VM&amp;rsquo;s. Clearly this is a job for Ansible which I&amp;rsquo;ve already &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;dabbled my toes in&lt;/a&gt;. Before we get onto doing that though, we need to have a think about how to make it a service.&lt;/p&gt;
&lt;h3 id="linux-services"&gt;Linux Services&lt;/h3&gt;
&lt;p&gt;A service in Linux is just a program, but one that&amp;rsquo;s usually required to be running all the time to provide some piece of functionality. The &amp;ldquo;program&amp;rdquo; can be any executable, but to allow systemd to manage it, we need to tell it a bit about what we want in a &lt;code&gt;.service&lt;/code&gt; file. This file is used by &lt;code&gt;systemd&lt;/code&gt; to know how to manage the service. They can get quite complex, but here&amp;rsquo;s the simple one for &lt;code&gt;vitals-glimpse&lt;/code&gt; - my little monitoring API endpoint.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-19-at-11.23.21-am.png" alt="[Unit]
Description=Memory and Disk statistics server on port 10321
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/vitals-glimpse
[Install]
WantedBy=default.target"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ExecStart&lt;/code&gt; is just saying what executable file is to be run. In this case it&amp;rsquo;s my compiled Go program. It&amp;rsquo;s a whopping 6MB so I&amp;rsquo;m assuming it&amp;rsquo;s all statically linked and standalone, so to run it we just copy it into &lt;code&gt;/usr/local/bin&lt;/code&gt; and run it from there.&lt;/p&gt;
&lt;p&gt;The two lines mentioning &lt;code&gt;.target&lt;/code&gt;s might not be obvious. These refer to the different times things happen in the machine startup sequence. &lt;code&gt;After=network.target&lt;/code&gt; means &amp;ldquo;don&amp;rsquo;t start this until the network is up and running&amp;rdquo;. You can see how it would be pointless to start a server that&amp;rsquo;s listening on a network port before networking is live. &lt;code&gt;default.target&lt;/code&gt; is just the system state when everything is going and ready for the users to interact with things, so when we specify &lt;code&gt;WantedBy=default.target&lt;/code&gt; we&amp;rsquo;re just saying &amp;ldquo;this service needs to be running by the time we are ready for user interactions&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;I already have my hosts file listing every machine, and an encrypted vault for my secrets (we&amp;rsquo;ve discussed those before), so the installation Ansible playbook just needs to copy the executable file into place in &lt;code&gt;/usr/local/bin&lt;/code&gt;, mark it as executable, copy the service file into place, and then start the service.&lt;/p&gt;
&lt;p&gt;If the files are already up to date and we don&amp;rsquo;t copy anything, then there&amp;rsquo;s no need touch the service, but if we have copied a new file, then we want to restart the service to pick up the change. Here&amp;rsquo;s how that all looks.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Install vitals-glimpse to a Debian based server
 # ansible-playbook vg-install.yml --ask-vault-pass 
 vars_files: ./vault.yml
 hosts: vm100-dockhost
 become: true

 tasks:
 - name: Copy service file
 ansible.builtin.copy:
 src: files/vitals-glimpse.service
 dest: /etc/systemd/system/vitals-glimpse.service
 notify: Restart vitals-glimpse

 - name: Copy executable
 ansible.builtin.copy:
 src: files/vitals-glimpse
 dest: /usr/local/bin/vitals-glimpse
 mode: &amp;#39;0755&amp;#39; # Set the executable permissions
 notify: Restart vitals-glimpse

 handlers:
 - name: Restart vitals-glimpse
 ansible.builtin.service:
 name: vitals-glimpse
 state: restarted
 enabled: yes
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing to know is that I have a hosts inventory file in my Ansible config, and &lt;code&gt;vm100-dockhost&lt;/code&gt; is just one of those hosts. The sudo credentials for that host are in the &lt;code&gt;vault.yml&lt;/code&gt; file mentioned in the code as &lt;code&gt;vars_file&lt;/code&gt;. I&amp;rsquo;ve started putting the command I need to run each playbook in a comment in the file so I don&amp;rsquo;t have to remember them, the command for this one: &lt;code&gt;ansible-playbook vg-install.yml --ask-vault-pass&lt;/code&gt; tells Ansible to run this playbook, and ask me for the password to decrypt the vault file.&lt;/p&gt;
&lt;p&gt;The if/then mechanism to only do something based on something earlier happening in Ansible is usually achieved with notify/handles. We put the declarative block which is optionally executed in the &lt;code&gt;handlers:&lt;/code&gt; block. The name of this block (in the case above it is &lt;code&gt;Restart vitals-glimpse&lt;/code&gt;) is specified with the &lt;code&gt;notify&lt;/code&gt; key. If either of the files are copied in, then the notify flag is set and the service is restarted.&lt;/p&gt;</description></item><item><title>Simple API endpoint in Go</title><link>https://blog.iankulin.com/simple-api-endpoint-in-go/</link><pubDate>Wed, 27 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/simple-api-endpoint-in-go/</guid><description>&lt;img src="https://blog.iankulin.com/images/gopher.png" width="219" alt=""&gt;
&lt;p&gt;I&amp;rsquo;d like a small, quick, low load endpoint on all my nodes and VM&amp;rsquo;s that exposes a text keyword indicating if that machine is okay for RAM and disk space. I&amp;rsquo;m currently using &lt;a href="https://blog.iankulin.com/tags/uptime-kuma/"&gt;Uptime Kuma&lt;/a&gt; to monitor if these machines are pingable, but I&amp;rsquo;d love a tiny bit more information from them so I&amp;rsquo;d get a &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Ntfy&lt;/a&gt; buzz on my phone if a machine is in trouble.&lt;/p&gt;
&lt;p&gt;I mentioned a couple of weeks ago that the benefit of doing it in C rather than Node.js was probably not worth the trouble, but then being a fickle developer, decided to write it in Go.&lt;/p&gt;
&lt;p&gt;This was a pretty sweet experience, it&amp;rsquo;s a nice language and the ecosystem is good. When writing such a small utility, you don&amp;rsquo;t really get a full appreciation for a language, but there is a couple of nice things going on - one I appreciated was that unused code - for example an import that&amp;rsquo;s not used, or a variable declared but not accessed is a compiler error and flagged by the intellisense as you type.&lt;/p&gt;
&lt;p&gt;In terms of the language as written, it&amp;rsquo;s fair to say C-like - there&amp;rsquo;s no weirdness like the formatting being semantic. It&amp;rsquo;s statically typed, but has good inference.&lt;/p&gt;
&lt;p&gt;The code is up on &lt;a href="https://github.com/IanKulin/vitals-glimpse"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s hard-coded to port 10321 and the route is &lt;code&gt;/vitals.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-9.37.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-9.37.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You get back this JSON. In my Uptime Kuma system, I search for the keywords &lt;code&gt;mem_okay&lt;/code&gt; and &lt;code&gt;disk_okay&lt;/code&gt; - no need to parse the JSON, it&amp;rsquo;s just an on/off status check that will show up in red on the page if there&amp;rsquo;s trouble, and ping my phone using &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;ntfy.sh&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In Uptime Kuma, there&amp;rsquo;s an option when setting up a new monitor for &lt;code&gt;Http(s) Keyword&lt;/code&gt;. How this works is that it will scrape that web address and look to see if a particular keyword exists. If the keyword is present on the page, that site is marked as up, if not, it&amp;rsquo;s marked as down.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-7.47.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-7.47.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Testing the memory threshold for the screenshot above was fun:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;stress-ng --vm-bytes $(awk &amp;#39;/MemAvailable/{printf &amp;#34;%d\n&amp;#34;, $2 * 0.9;}&amp;#39; &amp;lt; /proc/meminfo)k --vm-keep -m 1
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Problems backing up LXC to NFS in Proxmox</title><link>https://blog.iankulin.com/problems-backing-up-lxc-to-nfs-in-proxmox/</link><pubDate>Sun, 24 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/problems-backing-up-lxc-to-nfs-in-proxmox/</guid><description>&lt;p&gt;If you create an unprivileged LXC container on Proxmox, then try to back it up to an NFS share, for example on a NAS, you&amp;rsquo;ll get an error when it tries to build the temporary file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.29-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.29-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The clue is in the &lt;code&gt;Permission denied&lt;/code&gt; line. It is trying to create a temporary file on my NAS, and failing because of a &lt;a href="https://blog.iankulin.com/could-it-be-a-permissions-problem/"&gt;permissions&lt;/a&gt; problem. If I try the same backup to the local storage, it works fine.&lt;/p&gt;
&lt;p&gt;The solution is to build the temporary file in the local storage. To do this, you need to edit the &lt;code&gt;/etc/vzdump.conf&lt;/code&gt; on the Proxmox node to set the &lt;code&gt;tmpdir: /tmp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.16.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.16.14-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then if you run the backup again, it will be able to create the temporary file, and successfully copy it to the share.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.20-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.20-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t make sense to me how it has the permissions to copy the finished backup file to the share, but not create a temporary file there - but I&amp;rsquo;m not curious enough today to find out. Shout out to user &lt;a href="https://forum.proxmox.com/members/dunuin.96080/"&gt;Dunuin&lt;/a&gt; in the Proxmox &lt;a href="https://forum.proxmox.com/threads/cannot-backup-only-lxc-to-nfs-vm-works.90797/"&gt;forums&lt;/a&gt; for the suggestion to change the &lt;code&gt;tmpdir&lt;/code&gt; in &lt;code&gt;/etc/vzdump.conf&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Use VS Code to work on remote files</title><link>https://blog.iankulin.com/use-vs-code-to-work-on-remote-files/</link><pubDate>Thu, 21 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/use-vs-code-to-work-on-remote-files/</guid><description>&lt;p&gt;If you&amp;rsquo;ve got a script, or some code to work on, and it&amp;rsquo;s on a VM somewhere, you can always &lt;code&gt;ssh&lt;/code&gt; in and use &lt;code&gt;nano&lt;/code&gt; or &lt;a href="https://blog.iankulin.com/bloody-vim/"&gt;&lt;code&gt;vim&lt;/code&gt;&lt;/a&gt; to make your edits. Like a caveman. With an archaic editor, no intellisense, and no spell checking.&lt;/p&gt;
&lt;p&gt;Or&amp;hellip;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-3.50.15-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-3.50.15-pm.png" width="900" alt="VS Code connected to a remote server over SSH"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This magic - of editing a files on a remote server over SSH is achieved by using a Microsoft plugin for VS Code - &amp;ldquo;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"&gt;Remote - SSH&lt;/a&gt;&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"&gt;&lt;img src="https://blog.iankulin.com/images/untitled.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;How the plugin works is that it installs a copy of &lt;a href="https://code.visualstudio.com/docs/remote/vscode-server"&gt;VS Code Server&lt;/a&gt; on the remote machine, then connects to it over SSH.&lt;/p&gt;
&lt;p&gt;The experience is pretty great, once it&amp;rsquo;s installed (which I&amp;rsquo;ll run through below) it&amp;rsquo;s as if you are natively on the remote machine - the terminal is in the current directory on the remote machine, you can navigate around your files, edit them, use git, drag local files in and drop them in your remote folder and run your code.&lt;/p&gt;
&lt;h3 id="setup"&gt;Setup&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You need to be able to SSH into the remote machine, preferably with keys if you want a smooth experience. I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/ssh-key-login-on-vps/"&gt;talked about this&lt;/a&gt; before if that&amp;rsquo;s new to you.&lt;/li&gt;
&lt;li&gt;Install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"&gt;Remote-SSH&lt;/a&gt; plugin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-3.29.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-3.29.05-pm.png" width="825" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Once that&amp;rsquo;s done, there will be a &amp;ldquo;Remote Explorer&amp;rdquo; icon over on the left edge of the VS Code window. If you click on that, the explorer area will show a list of machines you&amp;rsquo;ve configured. There&amp;rsquo;s a + to add a new one.&lt;/li&gt;
&lt;li&gt;If you click on that, it will ask you to enter the SSH command to access a machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-3.31.49-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-3.31.49-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When you hit enter on that, it will ask you where to save the config file - I just chose the top one since that&amp;rsquo;s where I usually go to edit &lt;code&gt;known_hosts&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;You can get back to this config file later by clicking on the gear icon next to SSH in the remote explorer. That&amp;rsquo;s what I&amp;rsquo;ve done in the screenshot below to change the server name to something a bit more memorable.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;With that all set up, just click on the &amp;ldquo;Connect in New Window&amp;rdquo; icon next to the server you want to work on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-4.59.30-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-4.59.30-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Once the connection is established, new VS Code window will open up, with the files in the remote directory loaded ready for work. The status of the connection is shown in the bottom left corner of the window.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-5.04.18-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-13-at-5.04.18-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If anything here didn&amp;rsquo;t make sense, there&amp;rsquo;s a &lt;a href="https://code.visualstudio.com/docs/remote/ssh-tutorial"&gt;good tutorial on the Visual Studio Code website&lt;/a&gt;, or if you&amp;rsquo;re more of a video person, this overenthusiastic guy has a &lt;a href="https://www.youtube.com/watch?v=7kum46SFIaY"&gt;good quick summary&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Disable SSH root logins</title><link>https://blog.iankulin.com/disable-ssh-root-logins/</link><pubDate>Mon, 18 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/disable-ssh-root-logins/</guid><description>&lt;p&gt;This always makes me laugh:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-03-at-8.01.20-pm.jpg" alt="Screenshot of terminal output full of lines saying &amp;ldquo;Failed password for root&amp;rdquo;"&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s like half the traffic on the internet is &lt;a href="https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/"&gt;bots&lt;/a&gt; trying random passwords on root accounts over ssh. This is on an Ubuntu VPS on BinaryLane that had only been spun up five minutes or so. Looks like about one attempt every 10 seconds.&lt;/p&gt;
&lt;p&gt;This is why the number three thing on my new install list is to disable root access via ssh. Here&amp;rsquo;s my system - possibly just for Ubuntu and related systems:&lt;/p&gt;
&lt;p&gt;Add a new user, and put them in the sudo group&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;add user fred
usermod -aG sudo fred
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then log out, and ssh back in with the user you just created. Now we want to edit the config file for the ssh daemon. Since we&amp;rsquo;re not logged in as root now, we&amp;rsquo;ll have to use &lt;code&gt;sudo&lt;/code&gt;, so we&amp;rsquo;ll also find out if that&amp;rsquo;s working.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If &lt;code&gt;sudo&lt;/code&gt; doesn&amp;rsquo;t work, either you stuffed up adding the new username to the sudo group, or you don&amp;rsquo;t have sudo installed. If the problem is the latter, log out, and ssh back in as root and install it with &lt;code&gt;apt install sudo&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There is probably a line with &lt;code&gt;PermitRootLogin&lt;/code&gt; in it. It may be commented out, or set to &lt;code&gt;yes&lt;/code&gt;. But we want it to end up looking like this&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PermitRootLogin no
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;ll need to restart the daemon to pick up the config changes.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now if you log out, and try to ssh back in as root, it should fail. If it doesn&amp;rsquo;t, a likely issue is that there&amp;rsquo;s other configuration files being included. I feel I&amp;rsquo;ve mentioned before that a common pattern with Linux config files, at least on the Debian based systems I use, is that there&amp;rsquo;s a main config file that you probably shouldn&amp;rsquo;t mess with, but it pulls in subsidiary config files, often in a subdirectory called &lt;code&gt;conf.d&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In the case of this file, there&amp;rsquo;s a line up the top saying&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Include /etc/ssh/sshd_config.d/*.conf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;which does exactly that.&lt;/p&gt;
&lt;h3 id="spy-vs-spy"&gt;Spy vs Spy&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;d love to know what passwords these bots are trying. I was thinking it wouldn&amp;rsquo;t be all that hard to write something that would face the password login process and run it on port 22 to see. I asked ChatGPT about this, but goodie-goddie that it is, all I got was a warning about ethics and some ssh security tips.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-04-at-6.04.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-04-at-6.04.02-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not the first person to &lt;a href="https://www.darkreading.com/endpoint/a-common-password-list-accounts-for-nearly-all-cyberattacks"&gt;think of this&lt;/a&gt;, so I might come back to this idea later. If I was running brute force ssh I guess I&amp;rsquo;d use one of the common password lists from one of the big leaks, so it might not be that exciting. It would also be interesting to see what the first command they tried to run is as well.&lt;/p&gt;</description></item><item><title>Lightweight Web Servers</title><link>https://blog.iankulin.com/lightweight-web-servers/</link><pubDate>Fri, 15 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/lightweight-web-servers/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-02-at-9.09.48-pm-2.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-02-at-9.09.48-pm-2.png" width="300" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been using the excellent &lt;a href="https://github.com/louislam/uptime-kuma"&gt;Uptime Kuma&lt;/a&gt; for my monitoring, but a couple of recent incidents - an external USB mount disappeared on a remote machine, an NVME drive filled up on a different node and stopped backups working because of a configuration error - have made me start to think about more robust monitoring.&lt;/p&gt;
&lt;p&gt;The are many great tools for this - &lt;a href="https://www.nagios.org/"&gt;Nagios&lt;/a&gt;, &lt;a href="https://prometheus.io/"&gt;Prometheus&lt;/a&gt; etc. but they are pretty substantial time investments for the excellent power. They can save time series data and display them beautifully. However, all I really want is to add some extra ability to Uptime Kuma.&lt;/p&gt;
&lt;p&gt;Uptime Kuma is already pretty great - it can parse a webpage to search for a particular phrase, it can execute searches in popular databases, it can ping, check a docker container is running and all sorts of other tricks - but it can&amp;rsquo;t check memory use of a service, or if a machine is running out of disk space. Uptime Kuma works in binary - things either pass a check, or they don&amp;rsquo;t. It does do some nice graphs of ping times, but that&amp;rsquo;s about all.&lt;/p&gt;
&lt;p&gt;I could expose some of this data - disk space free, CPU temp, checking a mount is working - pretty easily in a little Node endpoint. But it thinking about this, it made me wonder what the overhead of running Node (probably with Express) to carry out this menial task might be. I was thinking that the alternatives would be to use python/flask, or just to write it in C or Golang.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://dev.to/wickdchromosome/is-the-pain-worth-the-gain-writing-webapps-in-c-benchmarks-vs-flask-and-nodejs-14l0"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-02-at-9.34.50-pm.png" width="129" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Whilst searching for answers about this, I found this excellent article from Bence Cotis. It turns out, that for very low loads (I&amp;rsquo;ll probably hit these endpoints once every five minutes) C is a bit better, but probably not (in my opinion) worth the hassle. I&amp;rsquo;ll stick to Node.&lt;/p&gt;</description></item><item><title>Cookies, Sessions &amp; Tokens</title><link>https://blog.iankulin.com/cookies-sessions-tokens/</link><pubDate>Tue, 12 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/cookies-sessions-tokens/</guid><description>&lt;p&gt;I&amp;rsquo;m up to the point in a web app where it needs to come off my lan and into the hands of a couple of users for alpha feedback. Before that happens, I have to add some sort of login/authentication system since it I want to use real, sensitive data. There&amp;rsquo;s lots of detailed blog posts and videos of how to implement this in an Express app with passport, but what I was missing was the big picture of what actually needs to happen.&lt;/p&gt;
&lt;p&gt;This clear video from Valetin Despa provides that.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/GhrvZ5nUWNg?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Basic VPS disk speed</title><link>https://blog.iankulin.com/basic-vps-disk-speed/</link><pubDate>Sat, 09 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/basic-vps-disk-speed/</guid><description>&lt;p&gt;I couldn&amp;rsquo;t help but measure some VPS disk speeds while I was busting out the &lt;code&gt;fio&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/vps.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Binary Lane only claims &amp;ldquo;pure SSD drives&amp;rdquo; but seems pretty great. The difference between Digital Ocean SSD and NVME is disappointing. Obviously you&amp;rsquo;re sharing a drive with other users, so perhaps this depends on what else is going on.&lt;/p&gt;</description></item><item><title>Sorting out Node package dependencies when cloning old repos</title><link>https://blog.iankulin.com/sorting-out-node-package-dependencies-when-cloning-old-repos/</link><pubDate>Wed, 06 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sorting-out-node-package-dependencies-when-cloning-old-repos/</guid><description>&lt;p&gt;If you clone an old node project and &lt;code&gt;npm install&lt;/code&gt; it, you&amp;rsquo;ll most likely get a bunch of errors and warning messages. If you just decide to yolo it and run the project, you&amp;rsquo;ll get a bunch more.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been doing this exact thing. I want to add some auth to my app, and I&amp;rsquo;ve been following &lt;a href="https://github.com/WebDevSimplified"&gt;WebDevSimplified&lt;/a&gt;&amp;rsquo;s &lt;a href="https://www.youtube.com/watch?v=-RCnNyD0L-s"&gt;video&lt;/a&gt; about using &lt;a href="https://www.passportjs.org/packages/passport-npm/"&gt;passport&lt;/a&gt;. I was building into my app without really understanding what I was doing, ran into problems and decided just to clone his repo and integrate the code into my app. The repo is four years old.&lt;/p&gt;
&lt;p&gt;The reason this is a problem is that &lt;code&gt;npm&lt;/code&gt; uses &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; to specify the versions of different packages. This is great, since it means you can clone a repo and know you are using the exact same versions of each package that everyone else using the repo is using. However, given enough time it also becomes a problem - packages are updated to address security vulnerabilities in their own code, or in their dependencies all the time.&lt;/p&gt;
&lt;p&gt;To untangle this mess, it&amp;rsquo;s worth understanding what&amp;rsquo;s going on with these two files.&lt;/p&gt;
&lt;h3 id="packagejson"&gt;package.json&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;package.json&lt;/code&gt; file doesn&amp;rsquo;t just store package versions, it has a heap of other project configuration stuff - like the starts script, project name and other meta data that we&amp;rsquo;re not really interested in here. What we&amp;rsquo;re interested in is the dependancies, so let&amp;rsquo;s have a look at a sample.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;dependencies&amp;#34;: {
 &amp;#34;lodash&amp;#34;: &amp;#34;^4.17.21&amp;#34;,
 &amp;#34;express&amp;#34;: &amp;#34;~4.17.1&amp;#34;,
 &amp;#34;axios&amp;#34;: &amp;#34;2.6.0&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Unsurprisingly, we&amp;rsquo;re looking at some JSON. In this case, key value pairs consisting of the package name, and then a version, but the version sometimes has some punctuation in front of it. The actual number is the version that was pulled down when we said something like &lt;code&gt;npm install lodash&lt;/code&gt;. In the case above, that was version 4.17.21&lt;/p&gt;
&lt;p&gt;The caret ^ in front of it, means this version, or any future version up to but not including the next major version. So &lt;code&gt;npm install&lt;/code&gt; is free to grab whatever the current version is - maybe 4.18.34 or 4.99.99 - but not 5.0.0 or anything after that.&lt;/p&gt;
&lt;p&gt;This is a sensible restriction. In most projects a major version denotes a breaking (not backward compatible) change, so it makes sense to allow any future improvements and bug fixes, but to not allow breaking changes. For this reason, this is the default, so if you don&amp;rsquo;t manually edit your dependencies, this is what they will be set to.&lt;/p&gt;
&lt;p&gt;If you want to be slightly stricter, you use the tilde ~ in front of the version number as shown above for the &lt;code&gt;express&lt;/code&gt; package. In this case, you&amp;rsquo;re specifying the minimum version of the package, but allowing only patches, and not minor version changes. So in the case of ~4.17.1 it would be fine to install 4.17.2 or 4.17.9 but not 4.18.0&lt;/p&gt;
&lt;p&gt;The last case is no punctuation in front of the version number, in which case we are locked into that version. This is what&amp;rsquo;s happening with the axios package above. &lt;code&gt;npm install&lt;/code&gt; will only fetch 2.6.0, even if there&amp;rsquo;s a bug fix 2.6.1 available.&lt;/p&gt;
&lt;p&gt;For a long time, &lt;code&gt;package.json&lt;/code&gt; was all that was available, and beautiful thing that it is, there was still an issue.&lt;/p&gt;
&lt;h3 id="package-lockjson"&gt;package-lock.json&lt;/h3&gt;
&lt;p&gt;Even though, in the example of &lt;code&gt;&amp;quot;axios&amp;quot;: &amp;quot;2.6.0&amp;quot;&lt;/code&gt; we&amp;rsquo;ve firmly locked axios to version 2.6.0 by putting it in the package.json file with no prefix on the version number, some changes are still possible - how so?&lt;/p&gt;
&lt;p&gt;Most non-trivial packages you use will themselves depend on other packages. These are called transitional dependancies. In the case of axios (which is a http client to pull web pages into node) it depends on seven other packages that do more specialised things such as handling streams, understanding mime types and so on.&lt;/p&gt;
&lt;p&gt;If you want code bases to be completely reproducible, then we also need to lock all the versions of the transitive dependencies. To do this, &lt;code&gt;package-lock.json&lt;/code&gt; was introduced in &lt;a href="https://github.com/npm/npm/releases/tag/v5.0.0"&gt;Node v5.0&lt;/a&gt; in 2017. Here&amp;rsquo;s a snippet out of the file for an app using axios.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; &amp;#34;node_modules/mime-types&amp;#34;: {
 &amp;#34;version&amp;#34;: &amp;#34;2.1.35&amp;#34;,
 &amp;#34;resolved&amp;#34;: &amp;#34;https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz&amp;#34;,
 &amp;#34;integrity&amp;#34;: &amp;#34;sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==&amp;#34;,
 &amp;#34;dependencies&amp;#34;: {
 &amp;#34;mime-db&amp;#34;: &amp;#34;1.52.0&amp;#34;
 },
 &amp;#34;engines&amp;#34;: {
 &amp;#34;node&amp;#34;: &amp;#34;&amp;gt;= 0.6&amp;#34;
 }
 },
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="the-mess"&gt;The Mess&lt;/h3&gt;
&lt;p&gt;Running &lt;code&gt;npm install&lt;/code&gt; causes &lt;code&gt;npm&lt;/code&gt; to look at both of those files to work out what packages to download. It creates the &lt;code&gt;node_modules&lt;/code&gt; folder and puts all those packages in there so we can &lt;code&gt;require&lt;/code&gt; them. When I tried that with this four year old project, this is the first of three pages of error messages I got.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-06-at-6.54.03-pm.jpg" alt="Screenshot full of warning messages for deprecated code"&gt;&lt;/p&gt;
&lt;p&gt;Most of the rest were errors from bcrypt - and you don&amp;rsquo;t really want to run old cryptology code.&lt;/p&gt;
&lt;p&gt;So, we&amp;rsquo;re in a bit of a bind here. The package version specified by the package developers doesn&amp;rsquo;t work any more. We can (and will shortly) ignore those, but of course then we&amp;rsquo;re risking that some breaking change in one of the packages will break the app code in some other way. Nevertheless, that&amp;rsquo;s what we need to do, but we&amp;rsquo;ll do it starting from the least risky to the most risky.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Delete &lt;code&gt;package-lock.json&lt;/code&gt; - if you trust the developers of the packages being used (and you shouldn&amp;rsquo;t be running them if you do not) then letting them decide the relative risks with updating the transitive dependencies is probably a reasonable bet. We can achieve that by deleting or renaming &lt;code&gt;package-lock.json&lt;/code&gt; and re-running &lt;code&gt;npm install&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;package.json&lt;/code&gt; to allow minor version changes - maybe all of the package versions in here have the caret ^ in front of their version number to allow them to be updated to the latest minor version and patch without changing the major version. If they do not, then try adding the caret to each one.&lt;/li&gt;
&lt;li&gt;Allow the latest version - an option we didn&amp;rsquo;t talk about when adding carets or tildes is that we can actually tell npm to just download the latest version. You don&amp;rsquo;t often see this, but if you put in a wildcard it will just grab the most recent. This is a bit more of a nuclear option, so it&amp;rsquo;s probably worth having a look at the 2000 lines of errors I had, and seeing if you can make an intelligent guess about which package is troublesome, and starting from there, doing them one at a time.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;dependencies&amp;#34;: {
 &amp;#34;axios&amp;#34;: &amp;#34;*&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In my case there was lots of bcrypt sounding errors, so that was my first try - I set that to the wildcard version, and the number of lines of warning/error output dropped from 2249 to 14. Also the process actually completed this time - I had a &lt;code&gt;node_modules&lt;/code&gt; folder and a new &lt;code&gt;package-lock.json&lt;/code&gt;. Included in the 14 lines of output was advice that there was a number of security vulnerabilities that could be fixed by running &lt;code&gt;npm audit fix --force&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; will let you know of any known security vulnerabilities in your installed packages. If you add the &lt;code&gt;fix --force&lt;/code&gt; option it will update them to the minimum version to address those vulnerabilities. This is basically just doing what we did in the previous step, but a bit smarter. In general, it&amp;rsquo;s going to be safer to have to fix some code than to ship code with known vulnerabilities, so if you are offered this choice, go ahead and run that.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test everything. You&amp;rsquo;ve just pulled a heap of new code in this project. It needs tested.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Testing Storage Speed</title><link>https://blog.iankulin.com/testing-storage-speed/</link><pubDate>Sun, 03 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/testing-storage-speed/</guid><description>&lt;p&gt;Now I&amp;rsquo;ve added NVME drives to my nodes, plus added an external NMVE RAID, I&amp;rsquo;ve got quite the collection of storage options. For one of my nodes, it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-23-at-1.20.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-23-at-1.20.34-pm.png" width="979" alt="Screenshot of Proxmox GUI showing 5 storage options"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The 256GB NVME the OS is installed to&lt;/li&gt;
&lt;li&gt;The 512GB SSD, currently running ZFS&lt;/li&gt;
&lt;li&gt;The Synology NAS - 4 x 6TB drives in RAID 5 on a 1GB switch&lt;/li&gt;
&lt;li&gt;A pair of 256GB NVME sticks in an external USB3 enclosure set up as a mirrored ZFS pool.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my dev VM&amp;rsquo;s I often set them up to have their storage on the NAS - it&amp;rsquo;s just super easy to move them around then. The production VM&amp;rsquo;s currently have their storage on the SSD (that machine hasn&amp;rsquo;t had the NVME upgrade yet), but obviously with all these options, it&amp;rsquo;d be interesting to think about what goes where.&lt;/p&gt;
&lt;p&gt;The biggest lots of files - media and distro ISO&amp;rsquo;s are clearly going to be on the NAS. No thinking to do there. Production VM backups also go there, and now I&amp;rsquo;ve got room they might also go cross-node as an extra layer of redundancy.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s really the live VM hard disks that need a decision. So I need to do some measuring.&lt;/p&gt;
&lt;p&gt;Jim Salter - one of the ZFS kings on the &lt;a href="https://2.5admins.com/"&gt;2.5 Admins&lt;/a&gt; podcast has an &lt;a href="https://arstechnica.com/gadgets/2020/02/how-fast-are-your-disks-find-out-the-open-source-way-with-fio/"&gt;article on drive speed testing&lt;/a&gt; that&amp;rsquo;s worth reading even just for the good descriptions of different drives and the range of workloads to consider.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://arstechnica.com/gadgets/2020/02/how-fast-are-your-disks-find-out-the-open-source-way-with-fio/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-23-at-4.42.31-pm.jpg" alt="Article headline: How fast are your disks? Find out the open source way,
with fio."&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;He ends up recommending three tests using &lt;code&gt;fio&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing 4K blocks randomly - disks do not enjoy this&lt;/li&gt;
&lt;li&gt;Having 16 parallel processes write 64K blocks to random locations in 256MB files&lt;/li&gt;
&lt;li&gt;Writing 1MB blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I ran all of those tests once on each of my storage options and this is what we got.&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;NVME&lt;/td&gt;&lt;td&gt;NAS/NFS/RAID 5&lt;/td&gt;&lt;td&gt;SDD/ZFS&lt;/td&gt;&lt;td&gt;External NVME RAID 1 ZFS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Single 4KiB random write process&lt;/td&gt;&lt;td&gt;430&lt;/td&gt;&lt;td&gt;17.9&lt;/td&gt;&lt;td&gt;80.6&lt;/td&gt;&lt;td&gt;75.7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;16 parallel 64KiB random write processes&lt;/td&gt;&lt;td&gt;2328&lt;/td&gt;&lt;td&gt;4897&lt;/td&gt;&lt;td&gt;267&lt;/td&gt;&lt;td&gt;135&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Single 1MiB random write process&lt;/td&gt;&lt;td&gt;651&lt;/td&gt;&lt;td&gt;70.7&lt;/td&gt;&lt;td&gt;379&lt;/td&gt;&lt;td&gt;195&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Speeds in MB/s&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You can better see how crazy this is in a graph.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/picture-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What is up with the figure for the NAS 16 x 64K?&lt;/p&gt;
&lt;p&gt;Probably worth talking about what I was expecting. I thought the internal NVME would be quickest, then the internal SDD, then the external NVME RAID then the NAS since it&amp;rsquo;s over the network.&lt;/p&gt;
&lt;p&gt;Given the excellent speed of the internal NVME, the external NMVE was a d=bit disappointing, but it&amp;rsquo;s got a few factors going against it. One is that it&amp;rsquo;s talking over USB 3 which has a theoretical 600MB/s limit, but we&amp;rsquo;re well under that. More likely it&amp;rsquo;s the ZFS - a system that is focused on data integrity rather than speed. But then the external drive is worse that the internal SATA SSD - and they both have ZFS with compression turned on. The enclosure is a no-name brand one, so we don&amp;rsquo;t really know the quality of it&amp;rsquo;s USB 3.0 implementation.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m dubious about the data for the NAS. When I first ran the 4K test it took so long, I killed the process a couple of times thinking it was frozen. The test command limits all three tests to a minute, so you&amp;rsquo;d expect them to take just a few seconds more than that, but all the NAS tests seemed to hang on the very last piece of output for minutes. I&amp;rsquo;m wondering is there was some smart caching going on, but then it waited in an async way to complete. Jim actually discusses that this is the purpose of the &lt;code&gt;--end_fsync=1&lt;/code&gt; in the commands so likely that&amp;rsquo;s it. I&amp;rsquo;ll have a think about how to adjust for this for a future post.&lt;/p&gt;
&lt;h3 id="cost-of-zfs"&gt;Cost of ZFS&lt;/h3&gt;
&lt;p&gt;What would the story be on the internal SSD if we turn the ZFS compression off, or just use ext4?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/ssd-speed.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It looks like compression has quite a cost on the little writes. It&amp;rsquo;s not a simple matter though - you could expect a speed improvement in many situations if the data compresses well. I had a look at the data in some of the test files that were generated, and it seemed quite random which is probably the worst case scenario for compression. Even so, in the graphs above the compressed looks like it did better than the uncompressed for the 64K random writes. I didn&amp;rsquo;t bother to replicate the tests, so it&amp;rsquo;s possible that&amp;rsquo;s just noise.&lt;/p&gt;
&lt;p&gt;Looking at the external dual NVME USB 3 drive with compression on/off is a similar story.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/external.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A big increase for the small writes, slight decrease in the parallel ones.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s really a bit hard to come to any conclusions from all this, but let&amp;rsquo;s have a go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There&amp;rsquo;s a speed penalty for using ZFS at all&lt;/li&gt;
&lt;li&gt;ZFS compression is expensive for many small writes of uncompressible data&lt;/li&gt;
&lt;li&gt;I need to know more about fio to figure out what was going on with that near impossible speed&lt;/li&gt;
&lt;li&gt;NVME is really fast connected to the bus, plugged in through USB, not so much&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Based on this, I wish I&amp;rsquo;d bought bigger NVME drives - that might be a future plan when I&amp;rsquo;ve run these for a while. These 256GB ones were $20 each which is just about a disposable price. They&amp;rsquo;ll probably end up as cache for a NAS or something in the future. In the mean time, I&amp;rsquo;ll format my SATA SSDs with ZFS with no compression and the VM disks will live on them. The benefits I get from that (100% data integrity checks with scrubs, and easy transfer of snapshots) seem like a reasonable tradeoff for a little speed.&lt;/p&gt;</description></item><item><title>Error wiping old drive in Proxmox</title><link>https://blog.iankulin.com/error-wiping-old-drive-in-proxmox/</link><pubDate>Thu, 31 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/error-wiping-old-drive-in-proxmox/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.19.42-pm-copy.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.19.42-pm-copy.png" width="568" alt="Error: disk/partition '/dev/sda3' has a holder (500)"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I popped in an NVME drive and freshly installed Proxmox to it, I assumed I&amp;rsquo;d just be able to wipe the SDD that had previously been the boot drive to set it up as a ZFS pool. However, when I tried to do the wipe, I was greeted with the error:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;disk/partition &amp;#39;/dev/sda3&amp;#39; has a holder (500)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I assume this means there&amp;rsquo;s a flag set on one of the Proxmox partitions to prevent accidental deletion or Proxmox thought that&amp;rsquo;s where it was running from. It&amp;rsquo;s likely that it&amp;rsquo;s related to this message I had during installation that I haven&amp;rsquo;t seen before:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_5830.jpg" alt="Detected existing &amp;lsquo;pve&amp;rsquo; Volume Group(s)! Do you want to: rename VG backed by PV &amp;lsquo;/dev/sda3&amp;rsquo; to &amp;lsquo;pve-OLD-D4DDE7DC&amp;rsquo; or cancel the installation?"&gt;&lt;/p&gt;
&lt;p&gt;Since I didn&amp;rsquo;t want to cancel the installation, I went ahead and told it okay. On the non-graphical &amp;lsquo;console&amp;rsquo; version of the installer, this message is truncated, and the only option available is abort. I guess that&amp;rsquo;s an installer bug. So if you are adding a extra boot drive to an existing Proxmox node, I suggest using the graphical installer.&lt;/p&gt;
&lt;p&gt;When I Googled around for the &amp;ldquo;has a holder&amp;rdquo; error, there were several unanswered requests for help for this, several speculative answers, and &lt;a href="https://www.reddit.com/r/Proxmox/comments/xff5ri/how_do_i_wipe_an_old_drive/"&gt;one that worked&lt;/a&gt;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/66d29d7d-bc29-4747-b92a-7fc7c790227f_text.gif" width="400" alt=""&gt;
&lt;p&gt;You need to use &lt;code&gt;fdisk&lt;/code&gt; to remove each partition. Take a note of the drive name - I could see in the Proxmox GUI that mine was sda, so the command to run was:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fdisk /dev/sda&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You probably need to have a &lt;a href="https://www.howtogeek.com/106873/how-to-use-fdisk-to-manage-partitions-on-linux/"&gt;read-up on&lt;/a&gt; &lt;code&gt;[fdisk](https://www.howtogeek.com/106873/how-to-use-fdisk-to-manage-partitions-on-linux/)&lt;/code&gt; if you&amp;rsquo;re not familiar with it, but basically, you&amp;rsquo;re in the command mode, for one of the partitions (my &lt;code&gt;sda&lt;/code&gt; had three) if you press the &lt;code&gt;d&lt;/code&gt; key here it marks that partition for deletion. Even though the error message had said it was the last partition that was causing the headache, I just went ahead and deleted all of them. There&amp;rsquo;s no warnings as you do this, and actually no changes have been made yet, that happens when you press &lt;code&gt;w&lt;/code&gt; to write the changes. No warning here either. 🙂&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.29.16-pm.png" alt="fdisk screenshot"&gt;&lt;/p&gt;
&lt;p&gt;That gave an error saying the third partition was still in use by the kernel, so I followed the advice to reboot, then I was able to wipe the drive in the Proxmox web GUI.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.30.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.30.09-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>How to install M.2 SSD in HP G2 800 Mini</title><link>https://blog.iankulin.com/how-to-install-m-2-ssd-in-hp-g2-800-mini/</link><pubDate>Mon, 28 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-install-m-2-ssd-in-hp-g2-800-mini/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_5821-copy.jpg" width="512" alt=""&gt;
&lt;p&gt;As part of my strategy to not worry about the &lt;a href="https://blog.iankulin.com/sdd-wearout-numbers/"&gt;slightly dodgy SMART reporting&lt;/a&gt; on the SDD&amp;rsquo;s in my HP Elitedesk G2 800 Mini Proxmox nodes, I&amp;rsquo;d decided to make use of the full sized &lt;a href="https://en.wikipedia.org/wiki/M.2"&gt;M.2&lt;/a&gt; slot to install 256GB NVME drives. That way I can boot from those, and have the SSD&amp;rsquo;s running &lt;a href="https://arstechnica.com/information-technology/2020/05/zfs-101-understanding-zfs-storage-and-performance/"&gt;ZFS&lt;/a&gt; which allows &lt;em&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/8/zpool-scrub.8.html"&gt;scrubbing&lt;/a&gt;&lt;/em&gt; to check the integrity of all the data. My VM disks can live on this drive.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://support.hp.com/au-en/product/hp-elitedesk-800-35w-g2-desktop-mini-pc/7633266"&gt;G2 800 Mini&lt;/a&gt; has two M.2 slots, a 2230 (M.2 sizes are &lt;code&gt;wwll&lt;/code&gt; where &lt;code&gt;ww&lt;/code&gt; is width in mm, and &lt;code&gt;ll&lt;/code&gt; is length in mm) for the wireless/bluetooth adaptor and a 2280 for storage. These slots are under the SSD drive cage.&lt;/p&gt;
&lt;h2 id="steps"&gt;Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Undo the large finger-operable screw on the back of the case, then slide the case off in the direction of the front of the unit.&lt;/li&gt;
&lt;li&gt;Unplug the drive SATA connector&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="https://blog.iankulin.com/images/img_5818.jpg" width="800" alt=""&gt;
&lt;ul&gt;
&lt;li&gt;At the right side of the SSD (when the machine is orientated per the photo above) is a lever that can be pushed a little to the right to allow the drive to slide back and be lifted out.&lt;/li&gt;
&lt;li&gt;There&amp;rsquo;s three giant screws holding the drive cage in numbered 1, 2 &amp;amp; 3. There&amp;rsquo;s also several smaller screws with numbers - ignore them. The ones you are looking for have a torx in the middle, but also a slot for an ordinary flat blade screwdriver. If you can only find two, that&amp;rsquo;s probably because the drive&amp;rsquo;s SATA connector is covering it up.&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="https://blog.iankulin.com/images/img_5829.jpg" width="800" alt=""&gt;
&lt;ul&gt;
&lt;li&gt;Once the drive cage is removed and set aside, you&amp;rsquo;ll be able to see the two M.2 slots. The NVME drive slots in like SODIMM memory - sort of sprung up on the end away from the connector. I didn&amp;rsquo;t like the look of those lose wires - but I assume they are for the wifi or bluetooth antennas.&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="https://blog.iankulin.com/images/img_5821-copy.jpg" width="512" alt=""&gt;
&lt;ul&gt;
&lt;li&gt;Wriggle it in, then push the end down and secure it with the little M.2 screws. You did remember to &lt;a href="https://www.ebay.com.au/itm/254101897159?hash=item3b29a73fc7:g:Wi4AAOSw6JpfdRiw&amp;amp;amdata=enc%3AAQAIAAABAFQS9v%2BRrt%2FNj4OpgTFaOvObhlzxvwZi%2BTxcYYqqbid7A6%2BkHvM6T3%2BDJ%2FegE3E9k3OH8bnHIBDJATYnIeJb9db%2FcKPWZP%2FAeNLDhwPi%2FDebbCZOJmhrSd3j0GRYLzE03YK%2F8DvMMAeLjPWLUO6mqZSUv%2FB7%2FuOs4Yz%2F5%2Bj6atvgCb0afWi9igSdklHlr6N1gqWN7DSb9WrCi2Dx62LQdasjvyrTNm%2BeDGzRj1ADzEJTG1oyJkOto6DOY2cUiGM5gLssMknszOh25RhBgXrNLf%2BUFnzUI2%2BOr5fvcamWs7zxKJJcndcMYOzbm3v%2B243SsWoGymttCsbsWi%2FLRekQRpQ%3D%7Ctkp%3ABFBMvrXWjLBi"&gt;order those screws&lt;/a&gt;, right? My $20 Samsung PM981a 256GB drives didn&amp;rsquo;t come with any, but perhaps fancy ones do.&lt;/li&gt;
&lt;li&gt;Then, as 1970 &lt;a href="https://haynes.com/en-au/holden/kingswood/1968-1971?part=04085&amp;amp;selector=print&amp;amp;gclid=EAIaIQobChMI3u6DqfKjgAMVLtcWBR2U7gT2EAQYBiABEgIgmfD_BwE"&gt;Greggory&amp;rsquo;s workshop manuals&lt;/a&gt; used to say, &amp;ldquo;&lt;em&gt;Assembly is the reverse of the disassembly steps with attention to the following:&lt;/em&gt;&amp;rdquo;. In this case, the attention would be towards being gentle with that SSD ribbon connector.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Hide 'Problems' for a file in VS Code</title><link>https://blog.iankulin.com/hide-problems-for-a-file-in-vs-code/</link><pubDate>Fri, 25 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hide-problems-for-a-file-in-vs-code/</guid><description>&lt;p&gt;I&amp;rsquo;m interested in trying out &lt;a href="https://picocss.com/"&gt;Pico CSS&lt;/a&gt; - a lightweight CSS library, but when I tossed it into my project, the linter found and reported 29 problems. One of my processes is to just keep that problems tab clear as I work, so I&amp;rsquo;d like that to go away.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-20-at-6.54.06-am.jpg" alt="Screenshot of VS Code showing 29 problems detected."&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible, but only by &amp;rsquo;excluding&amp;rsquo; the file from your project - it won&amp;rsquo;t show up in the file view either. That&amp;rsquo;s fine with me, I never want to deal with the file so we&amp;rsquo;ll do that, although it might confuse me in seven years if I come back to this project, so I&amp;rsquo;ll drop a link in my .git_ignore as a clue for future me (excluding the file in VS Code doesn&amp;rsquo;t affect git finding it).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-20-at-7.03.25-am.png" alt="screenshot of .gitignore file including public/pico.min.css"&gt;&lt;/p&gt;
&lt;p&gt;Workspace settings (such as excluding a file) are stored in &lt;code&gt;./vscode/settings.json&lt;/code&gt; - this has some other bits and pieces such as spelling corrections etc. It&amp;rsquo;s worth letting this into your repository so your workspace is recreated when you clone the repo. The fragment you need to add is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; &amp;#34;files.exclude&amp;#34;: {
 &amp;#34;**/pico.min.css&amp;#34;: true
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will remove it from the files, and it will stop it being processed by your extensions including any linters. If the problems don&amp;rsquo;t disappear instantly, click into a different file for a second and they should go, or occasionally, you&amp;rsquo;ll need to close and open VSCode.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-20-at-6.54.47-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-20-at-6.54.47-am.png" width="800" alt="Screenshot of VS Code showing settings.json and zero problems in the problems tab."&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Installing a Node app on a server</title><link>https://blog.iankulin.com/installing-a-node-app-on-a-server/</link><pubDate>Tue, 22 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/installing-a-node-app-on-a-server/</guid><description>&lt;p&gt;Before I write a fancy Ansible playbook to automatically set up the Nginx/Node combo on my web servers, it might be worth going through how to deploy a Node app so it can run on a server without you being logged in.&lt;/p&gt;
&lt;p&gt;Until now, I&amp;rsquo;ve been running my tests on my laptop, or in a server logged in as myself - sometimes detaching from tmux. But we need a bit more professional set up than that. The process will look something like this:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/hqdefault.jpg" width="150" alt=""&gt;
&lt;ul&gt;
&lt;li&gt;Install Node and npm (I&amp;rsquo;m assuming we&amp;rsquo;ve done that since I&amp;rsquo;ve covered the playbook for it before).&lt;/li&gt;
&lt;li&gt;Copy the app files over&lt;/li&gt;
&lt;li&gt;Install the dependencies&lt;/li&gt;
&lt;li&gt;Write the systemd config file&lt;/li&gt;
&lt;li&gt;Start it up&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="app-files"&gt;App files&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ll use the very simple server (&lt;code&gt;index.js&lt;/code&gt;) I&amp;rsquo;ve written for the future Ansible post. All it does is listen on port 3000 to serve a tiny piece of text if someone hits the &lt;code&gt;/api&lt;/code&gt; route.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#39;express&amp;#39;);
const app = express();

const PORT = 3000;

app.get(&amp;#34;/api&amp;#34;, (req, res) =&amp;gt; {
 res.status(200).send(&amp;#39;Success - from /api route via node.js&amp;#39;);
});

app.listen(PORT, () =&amp;gt; {console.log(`Listening on port ${PORT}`)});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;ll also have our &lt;code&gt;package.json&lt;/code&gt;, of which the only interesting thing to notice is that we&amp;rsquo;ve got a dependency on the &lt;code&gt;express&lt;/code&gt; package which I originally installed with &lt;code&gt;npm&lt;/code&gt;. All the files for our dependencies are stored in the &lt;code&gt;./node_modules&lt;/code&gt; directory, but we don&amp;rsquo;t need to copy them to the server.&lt;/p&gt;
&lt;h3 id="where-to-put-the-app-files"&gt;Where to put the app files&lt;/h3&gt;
&lt;p&gt;If I was doing this for a commercial app, I might store the app under &lt;code&gt;/var/www/&amp;lt;app name&amp;gt;&lt;/code&gt; since that&amp;rsquo;s where a future sysadmin might look for it if they don&amp;rsquo;t have access to the playbooks. Another good place might be the home directory of the ansible/node user. Since that&amp;rsquo;s me in this case, they&amp;rsquo;re just going to go in my home directory - it makes the playbook commands shorter. We can use &lt;code&gt;scp&lt;/code&gt; to copy the files in.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-16-at-5.50.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once the files are there, we can install the dependencies with &lt;code&gt;npm install&lt;/code&gt;. This looks at the &lt;code&gt;package.json&lt;/code&gt;, then grabs them down.&lt;/p&gt;
&lt;h3 id="systemd"&gt;systemd&lt;/h3&gt;
&lt;p&gt;systemd manages the init and daemon processes in most Linux distros, so we&amp;rsquo;ll be using that to get our node app running as a service. It was &lt;a href="https://en.wikipedia.org/wiki/Systemd#History"&gt;a present from Red Hat&lt;/a&gt;. Processes that run like this need a configuration file in &lt;code&gt;/lib/systemd/system&lt;/code&gt;, ours will be called&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/lib/systemd/system/test-server.service&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Unit]
Description=index.js - test server 
After=network.target

[Service]
Type=simple
User=ian 
ExecStart=/usr/bin/node /home/ian/index.js
Restart=on-failure

[Install]
WantedBy=multi-user.target 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This file needs to say that we should wait for the network to come up before starting, what we&amp;rsquo;re running and what to do if it dies. &amp;lsquo;on-failure&amp;rsquo; means it will be restarted in pretty much any case but us stopping it cleanly. The &lt;code&gt;[multi-user.target](https://unix.stackexchange.com/questions/506347/why-do-most-systemd-examples-contain-wantedby-multi-user-target)&lt;/code&gt; bit is saying we want this service up and running for the system to be considered ready as a server.&lt;/p&gt;
&lt;p&gt;Once that file is in place, we can reload the configs, and start the service, this can be from anywhere, including a user home directory, and check it&amp;rsquo;s status.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl start test-server
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-16-at-8.10.32-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That all looks good, and if I visit the endpoint, there&amp;rsquo;s the expected response, even after we&amp;rsquo;ve logged out of the server.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-16-at-8.13.04-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Digital Ocean first impressions</title><link>https://blog.iankulin.com/digital-ocean-first-impressions/</link><pubDate>Sat, 19 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/digital-ocean-first-impressions/</guid><description>&lt;p&gt;I&amp;rsquo;ve been thinking about the time it takes me to provision a guest VM in Proxmox. I seem to remember on &lt;a href="https://www.binarylane.com.au/"&gt;BinaryLane&lt;/a&gt; it was seconds rather than minutes. This seemed to be a good excuse to use the free credit I&amp;rsquo;ve heard about for &lt;a href="https://www.linode.com/lp/free-credit-100/?promo=sitelin100-02162023&amp;amp;promo_value=100&amp;amp;promo_length=60&amp;amp;utm_source=google&amp;amp;utm_medium=cpc&amp;amp;utm_campaign=11178784684_109179223363&amp;amp;utm_term=g_kwd-2629795801_e_linode&amp;amp;utm_content=466889596558&amp;amp;locationid=1000676&amp;amp;device=c_c&amp;amp;gclid=CjwKCAjw-7OlBhB8EiwAnoOEk9lQtzb_l17rAJmoU1KzhTUcWc6TF6C8KBTZU3j6tJ3d1qLWqqiRgxoC6qUQAvD_BwE"&gt;Linode&lt;/a&gt; or Digital Ocean hundreds of times in podcast adverts, so I claimed the &lt;a href="http://do.co/lnl"&gt;$200 credit for being a Late Night Linux listener&lt;/a&gt; at Digital Ocean. They extracted $5 out of me in the process, so I guess they are in front on that transaction. $200 would run a little VM for a couple of years at their rates, but of course it&amp;rsquo;s limited to two months, at the end of which I will have an account sitting there, with my credit card already recorded - so all the friction is gone if I need an internet facing machine for some purpose - which is clearly their dastardly plan&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-11-at-7.50.07-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-11-at-7.50.07-pm.png" width="351" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The process of creating a &amp;lsquo;droplet&amp;rsquo; (that&amp;rsquo;s what they call their VM&amp;rsquo;s) was straightforward - select the datacentre, machine size etc You can upload your SSH key which is a nice touch.&lt;/p&gt;
&lt;p&gt;When I got to the end of all that, I hit create and timed the boot up of the Debian 12 system I&amp;rsquo;d chosen - 42.13 seconds.&lt;/p&gt;
&lt;p&gt;I could ping the public IP, so it existed, but couldn&amp;rsquo;t ssh in as root, and didn&amp;rsquo;t know my user name. After trawling through their Getting Started docs, I found one that said to use your email that you signed up with. That didn&amp;rsquo;t make sense or work. I &lt;a href="https://www.youtube.com/watch?v=kzThZOZj1S4&amp;amp;t=417"&gt;watched a video&lt;/a&gt;, then searched further and found I should have gone into the advanced options and written a script to add a user - a sample one was provided.&lt;/p&gt;
&lt;p&gt;I destroyed the first machine and created a second one with the sample user script (which I&amp;rsquo;ve since gone back and searched for but could not find) which basically adds the user and assigns the ssh key. Once that was booted I could ssh in, but not sudo since I didn&amp;rsquo;t know the password.&lt;/p&gt;
&lt;p&gt;There is a &amp;lsquo;console&amp;rsquo; so I used that to set a password for the user the script had created, then was able to both ssh in and use sudo. I guess the idea of the script is great if you know what you&amp;rsquo;re doing and going to be creating a lot of VM&amp;rsquo;s, but this was a painful start compared to &lt;a href="https://www.binarylane.com.au/"&gt;BinaryLane&lt;/a&gt; or my homelab. I figured out afterwards, this was because I&amp;rsquo;d chosen Debian for the distro - you can&amp;rsquo;t ssh in as root. If I choose a more relaxed distro, I could do that, and create my user then patch up the root access.&lt;/p&gt;
&lt;p&gt;The rest of the experience was fine - the web interface is clear enough apart from my initial grumble. I couldn&amp;rsquo;t paste into the web console, and I&amp;rsquo;ve noticed that in Proxmox as well so I guess that&amp;rsquo;s some sort of limitation. In any case, once you&amp;rsquo;ve set up your ssh user properly you never need use it again.&lt;/p&gt;</description></item><item><title>Nginx config on Debian/Ubuntu</title><link>https://blog.iankulin.com/nginx-config-on-debian-ubuntu/</link><pubDate>Wed, 16 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-config-on-debian-ubuntu/</guid><description>&lt;p&gt;A quick look at the arrangements around the config settings for nginx. This is based on what I can see in Debian and Ubuntu, but likely it will be most &lt;code&gt;apt&lt;/code&gt; flavoured distros. Others may well be different, I know CentOS is.&lt;/p&gt;
&lt;h3 id="context"&gt;Context&lt;/h3&gt;
&lt;p&gt;If the way the configs for nginx are arranged seems a little complicated, it&amp;rsquo;s helpful to keep in mind there&amp;rsquo;s a couple of challenges that are being addressed with that complexity.&lt;/p&gt;
&lt;h4 id="separating-concerns"&gt;Separating concerns&lt;/h4&gt;
&lt;p&gt;Separating out distro, and sysadmin concerns. - A very common pattern in Linux configurations is that the main config (often a .conf file) is kept with the application&amp;rsquo;s other files in an /etc/&lt;app name&gt; directory, and a line in that configuration file includes all the configuration files in a subsidiary directory.&lt;/p&gt;
&lt;p&gt;For example, here&amp;rsquo;s a tree of &lt;code&gt;/etc/ssh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-8.35.03-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If we look in the &lt;code&gt;ssh_config&lt;/code&gt; file, there will be a line including all the configs found in the &lt;code&gt;ssh_config.d&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-8.42.11-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The benefit of this pattern (having a main config file including all the config files in a sub-directory) is that the distro maintainers can make changes (in future updates) to the main config file to make everything in their distro work together, and the system administrator can make their changes for the local system in the sub-directory without them being written over during an update.&lt;/p&gt;
&lt;p&gt;Nginx configuration follows this pattern. The main configuration file &lt;code&gt;nginx.conf&lt;/code&gt;, includes the &lt;code&gt;*.conf&lt;/code&gt; files in the &lt;code&gt;conf.d&lt;/code&gt; sub-directory.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-8.48.19-am.png" alt=""&gt;&lt;/p&gt;
&lt;h4 id="maintaining-sites"&gt;Maintaining Sites&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s common for web servers running on an individual machine, to be serving content for more than one domain or web site. For example you might be running apache2 on a VPS that is serving your business web site, as well as the web sites of a couple of clients. This would be achieved by having the DNS records for, say itfreaks.com, realfakedoors.ca, and joesusedcars.com all pointing to the same IP address of your server.&lt;/p&gt;
&lt;p&gt;Since each of these sites may need slightly different setups, it would make sense for them to have separate config files as well. If you look at the screenshot above, you can see that nginx is going to look in the &lt;code&gt;/etc/nginx/sites-enabled/&lt;/code&gt; sub-directory for these, but that&amp;rsquo;s not quite the whole story. Let&amp;rsquo;s look at the tree of a fresh install of niginx.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-9.03.01-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see that the &lt;code&gt;sites-enabled&lt;/code&gt; sub-directory includes a symlink to a config file called default in the &lt;code&gt;sites-available&lt;/code&gt; sub-directory. The intention here is that you put your config files for each site in /&lt;code&gt;etc/nginx/sites-available&lt;/code&gt;, and link to them from &lt;code&gt;/etc/nginx/sites-available&lt;/code&gt;. Then when you want to activate a site, you put a link in sites-enable, or delete the link when you want to disable it, but this is done without losing the work in the site config file.&lt;/p&gt;
&lt;p&gt;Usually, you use the site name for these files - there&amp;rsquo;s no need for the &lt;code&gt;.conf&lt;/code&gt; extension. So if we look at an installation that has a live site with an IP address, it could look like this.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-9.16.36-am.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Ansible with Secrets</title><link>https://blog.iankulin.com/ansible-with-secrets/</link><pubDate>Sun, 13 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ansible-with-secrets/</guid><description>&lt;p&gt;We wrote a nice &lt;a href="https://blog.iankulin.com/first-ansible-playbook/"&gt;little Ansible playbook&lt;/a&gt; the other day to install nginx on our web servers and ensure it was running. We were able to store the usernames in the &lt;code&gt;hosts&lt;/code&gt; inventory file using the a&lt;code&gt;nsible_ssh_user&lt;/code&gt; variable. Then, we ran the playbook with the command:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ansible-playbook web_installs.yaml --ask-become-pass&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This asked us the password to use with the usernames in the &lt;code&gt;hosts&lt;/code&gt; file. Luckily that day, it was the same username/password combo to use for sudo on every server. What happens if that&amp;rsquo;s not the case? Here&amp;rsquo;s our new hosts file for today. There&amp;rsquo;s a cool new sysadmin in town - Jane.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[vm323_deb]
100.108.154.133

[vm323_deb:vars]
ansible_ssh_user=ian

[vm324-deb]
100.77.75.14

[vm324_deb:vars]
ansible_ssh_user=jane
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We could still use &lt;code&gt;--ask-become-pass&lt;/code&gt; but it only asks us one password, and it&amp;rsquo;s highly unlikely (we hope) that Jane and Ian have chosen the same password.&lt;/p&gt;
&lt;p&gt;If you look at the inventory file above, you can see how the variables work - it&amp;rsquo;s the same variable name - Ansible swaps the correct value in for each server as it accesses them. There&amp;rsquo;s many of these variables in addition to &lt;code&gt;ansible_ssh_user&lt;/code&gt;, including &lt;code&gt;ansible_ssh_pass&lt;/code&gt;, so maybe we can do something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[vm323_deb]
100.108.154.133

[vm323_deb:vars]
ansible_ssh_user=ian
ansible_become_password=mittens96

[vm324_deb]
100.77.75.14

[vm324_deb:vars]
ansible_ssh_user=jane
ansible_become_password=GBLEzrvc8rnUFruVrCwm
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you try that, it will work. However &lt;strong&gt;it is a terrible idea to store our ssh passwords in that inventory file in plaintext.&lt;/strong&gt; They would be available to anyone who gets access to my workstation, AND when I commit my work to git, it&amp;rsquo;s getting copied somewhere, probably including github. This is such a common problem there&amp;rsquo;s &lt;a href="https://www.gitguardian.com/solutions/scan-github-for-passwords"&gt;some business&lt;/a&gt; that have come into being to scan for passwords and API keys in people&amp;rsquo;s repos.&lt;/p&gt;
&lt;p&gt;There &lt;em&gt;is&lt;/em&gt; a better way. Ansible will allow us to store the secrets in a separate file as variables, and that separate file can be encrypted while it&amp;rsquo;s on disk, and Ansible will decrypt it to use from memory then clean up after itself. There&amp;rsquo;s a couple of new (to us) things here - variables, and the encryption. Let&amp;rsquo;s look at them separately.&lt;/p&gt;
&lt;h3 id="variables"&gt;Variables&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;[Ansible variables](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html)&lt;/code&gt; is a whole subject, we&amp;rsquo;re just going to look at the minimum we need to solve out problem.&lt;/p&gt;
&lt;p&gt;We can create another file in our project, let&amp;rsquo;s call it plaintext.yaml and store our usernames and passwords in there as key: value pairs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-11.43.01-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then we need to tell our playbook to import that &lt;code&gt;plaintext.yaml&lt;/code&gt; file with &lt;code&gt;vars_files&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-11.53.25-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then in the inventory file, we can substitute the usernames and passwords with our variables.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-11.58.55-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now, when the playbook runs, it will substitute the real values into the &lt;code&gt;host&lt;/code&gt; inventory file for us. Let&amp;rsquo;s check that.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-12.03.09-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Bingo bongo. But we haven&amp;rsquo;t actually solved our security problem yet. The ssh passwords are still dangerously stored in plaintext in our file system. What we have done is learned how to have variables in an external file, pull that into the playbook and have the values substituted in. Now we need the next step - keeping those passwords safe.&lt;/p&gt;
&lt;h3 id="ansible-vault"&gt;Ansible Vault&lt;/h3&gt;
&lt;p&gt;Clearly, every serious use of Ansible is going to have this issue (of needing ssh credentials, but not wanting to give them away to hackers) so of course, there is an elegant solution for it.&lt;/p&gt;
&lt;p&gt;What if the file with all the passwords was stored encrypted, then only decrypted for use by our playbook, and it was never saved anywhere as plaintext? That solves the problem.&lt;/p&gt;
&lt;p&gt;Ansible has a tool for this called &lt;a href="https://docs.ansible.com/ansible/2.8/user_guide/vault.html"&gt;Ansible Vault&lt;/a&gt;. We&amp;rsquo;ll create the yaml file with our variables with that tool, and it will be saved encrypted. When we run the playbook we&amp;rsquo;ll get it to ask us for the password to decrypt the file. It will do that in memory and run the playbook.&lt;/p&gt;
&lt;p&gt;The command to create our file, which we&amp;rsquo;ll call &lt;code&gt;vault.yaml&lt;/code&gt; will be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible-vault create vault.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will ask us to enter the password we want to use. The strength of this password needs to be good. If it&amp;rsquo;s crackable, you are giving away root access to your systems. And it&amp;rsquo;s in a file, not in a login situation where you can time out after three logins. Whoever has the file has the time to brute force password of the the &lt;a href="https://www.ipswitch.com/blog/use-aes-256-encryption-secure-data"&gt;AES 256&lt;/a&gt; encryption.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/h4c7m5z2a2b71-copy.jpg" width="460" alt=""&gt;
&lt;p&gt;Once you&amp;rsquo;ve entered your strong password twice, it will open up the new file in the default editor - probably vim. You may need help to use this editor.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-12.46.35-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re done, save the file and exit. What does this file look like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-12.51.38-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Yep. That should do it. We still need to tell our playbook to use this file instead of the other one.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;--- 
- name: nginx for all web servers
 vars_files: ./vault.yaml
 hosts: all
 become: yes
 tasks: 
 - name: nginx installed
 apt:
 name: nginx
 state: latest
 - name: nginx running
 service: 
 name: nginx
 state: started
 enabled: yes
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And when we run the playbook, we need to let it know to ask us the vault password.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible-playbook web_installs.yaml --ask-vault-pass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-1.00.16-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you need to edit the secrets file in the future, the command for that would be&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ansible-vault edit vault.yaml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once again it will ask the password, and once again it will open up in vim.&lt;/p&gt;</description></item><item><title>Bloody VIM</title><link>https://blog.iankulin.com/bloody-vim/</link><pubDate>Thu, 10 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bloody-vim/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Vim is a highly configurable text editor built to make creating and changing any kind of text very efficient. It is included as &amp;ldquo;vi&amp;rdquo; with most UNIX systems and with Apple OS X.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.vim.org/"&gt;vim.org&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You will encounter vi/vim as the incomprehensible text editor that pops up by default when you need to edit something in your sysadmin journey. Perhaps you issued the command to edit your Ansible vault, perhaps you forgot to add a message to a commit. It&amp;rsquo;s going to be unavoidable.&lt;/p&gt;
&lt;p&gt;Here is the minimum knowledge you need to survive these encounters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It has modes. When it opens up, you are in &amp;lsquo;moving around mode&amp;rsquo;. Use the cursor keys to get to where you need to.&lt;/li&gt;
&lt;li&gt;When you actually want to edit something, press &lt;code&gt;i&lt;/code&gt; now you&amp;rsquo;re in &lt;em&gt;INSERT&lt;/em&gt; mode and it&amp;rsquo;s behaving how 99% of the world expect an editor to behave. When you type things, they go into the document where the cursor is.&lt;/li&gt;
&lt;li&gt;When you are done, press &lt;em&gt;escape&lt;/em&gt; to get out of INSERT mode, then these keys one at a time &lt;code&gt;: w q&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The underlying theory of Vim - that you spend more time moving around in text than editing it, is true of most sysadmin and programming tasks. So Vim has powerful, non-intuitive commands to do that efficiently. It&amp;rsquo;s impressive to watch people who have learned these arcane ways move around a file. They don&amp;rsquo;t have girlfriends. There are also powerful, non-intuitive commands for editing.&lt;/p&gt;</description></item><item><title>Finding the host IP from inside a Docker container</title><link>https://blog.iankulin.com/finding-the-host-ip-from-inside-a-docker-container/</link><pubDate>Mon, 07 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/finding-the-host-ip-from-inside-a-docker-container/</guid><description>&lt;p&gt;Having successfully set up and tested my node.js api handling app behind nginx on a development VM in the homelab, I decided to move it to my VPS so I could start using it for real. I had a bit of trouble finding the nginx.conf files on the VPS, until I remembered I was running nginx in a docker container on this machine!&lt;/p&gt;
&lt;p&gt;I got everything set up, I could hit the domain in a web browser and get served the static page, and I could &amp;lt;domain_name&amp;gt;:3000/api/gnp_temp.txt and get the file delivered by the node script, but if I tried &amp;lt;domain_name&amp;gt;/api/gnp_temp.txt - &amp;ldquo;Bad Gateway&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I looked at the nginx.conf over and over - it seemed fine. The location block was clearly being triggered, otherwise I&amp;rsquo;d be getting a 404. I dived into stack overflow to no avail. It was if http://localhost:3000 just wasn&amp;rsquo;t working. But it definitely was when I &lt;code&gt;curl&lt;/code&gt;ed it from the command line.&lt;/p&gt;
&lt;p&gt;In desperation I started writing out an explanation to ChatGPT about the setup and my problem, and before I pressed enter realised - from nginx&amp;rsquo;s point of view, http://localhost:3000 was an address &lt;em&gt;inside&lt;/em&gt; the container 🤦, what I needed was the address of the host, from the point of view of the docker container. Surely that must be a common requirement that&amp;rsquo;s been solved.&lt;/p&gt;
&lt;p&gt;From reading around, it seems like I should be able to just substitute &lt;code&gt;host.docker.internal&lt;/code&gt; but that didn&amp;rsquo;t seem to work. I opened a shell into the container to look at &lt;code&gt;ip a&lt;/code&gt; or &lt;code&gt;/sbin/ip route|awk '/default/ { print $3 }'&lt;/code&gt; but of course these containers are slim installs without the general tools you need.&lt;/p&gt;
&lt;p&gt;Docker networking is a whole thing that I should learn, but I haven&amp;rsquo;t yet, I just start up containers with whatever the defaults for networking are. But I figured the host must be part of the docker network, and a quick look in &lt;code&gt;ip a | grep docker&lt;/code&gt; produced this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;3: docker0: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc noqueue state DOWN group default 
 inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Bingo. I popped that IP address into my nginx.conf and everything started working perfectly. That was forty minutes of learning I wouldn&amp;rsquo;t have had to live through if I could have just turned around to a work colleague and asked.&lt;/p&gt;</description></item><item><title>nginx in Front of a node.js app</title><link>https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/</link><pubDate>Fri, 04 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/</guid><description>&lt;p&gt;NGINX is a great webserver and reverse proxy - as in it can hand off requests to other web-servers. That&amp;rsquo;s the situation I want to have set up on my VPS. I want NGINX to handle incoming requests - some of them will just be sorted out by returning static HTML, others (like the weather api I&amp;rsquo;ve been playing with) need to be handed off to other services to respond to.&lt;/p&gt;
&lt;p&gt;In the situation I&amp;rsquo;m looking at, I want requests that have the route /api (eg example.com/api/weather) to be passed to a node.js program I&amp;rsquo;ve written. All the other http requests should just be treated as requests for static pages and dealt with by NGINX.&lt;/p&gt;
&lt;p&gt;So I guess is part V of my adventures in the weather API, if you just want to know how to set up NGINX to serve static pages AND pass some routes off to node, you don&amp;rsquo;t need to be up to date on these.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/"&gt;Outside Temperature From an API in a Shell Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/complicating-the-temperature-api/"&gt;Complicating the Temperature API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/how-to-deploy-a-node-js-app/"&gt;Using Node.js to serve a static file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/how-to-deploy-a-node-js-app/"&gt;How to Deploy a Node.js App&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nginx-configuration"&gt;nginx Configuration&lt;/h3&gt;
&lt;p&gt;Once nginx and node.js are installed (with &lt;a href="https://gist.github.com/IanKulin/bd6d1a78f9a9fa9a859384a26ca95235"&gt;the Ansible script&lt;/a&gt; if you want to rock the dev ops tattoo) you&amp;rsquo;ll need to configure nginx. On my Debian systems, the config file &lt;code&gt;nginx.conf&lt;/code&gt; is in &lt;code&gt;/etc/nginx/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a line in that file that includes all the &lt;code&gt;*.conf&lt;/code&gt; files in &lt;code&gt;/etc/nginx/conf.d&lt;/code&gt;. This is a common pattern I see in some distros - the main config files are not really meant to be messed with, but then there&amp;rsquo;s a directory to add config files to whihc are included. The theoretical advantage of this is that the distro maintainers can roll out a new version of a package and change the main config file, and your stuff will still work.&lt;/p&gt;
&lt;p&gt;The way they have done this with the nginx.conf means that the only changes we can make in the &lt;code&gt;conf.d&lt;/code&gt; directory are to do with virtual hosts, but that&amp;rsquo;s going to be 99% of the things we would want to change. So much so, I&amp;rsquo;m not even going to show you the &lt;code&gt;nginx.conf&lt;/code&gt; file, just our little &lt;code&gt;/etc/nginx/conf.d/nodeapi.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; server {
 listen 80;
 server_name 192.168.100.40;

 # Serve static files
 root /var/www;

 # pass api requests to node
 location /api {
 proxy_pass http://localhost:3000;
 proxy_set_header Host $host;
 }
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Okay, we are listening on port 80, and this &amp;ldquo;server block&amp;rdquo; is only for requests like http://192.168.100.40. The purpose of &lt;code&gt;server_name&lt;/code&gt; is that we might run the websites for several domains from one nginx installation. For example, we might be serving example.com and otherexample.com from the same VPS.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;root /var/www;&lt;/code&gt; - tells nginx to grab the files from that directory, so that&amp;rsquo;s the static web server part.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;location /api {&lt;/code&gt; - is telling nginx &amp;ldquo;all the requests with /api on the end are dealt with differently, look in this block for instructions&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy_pass http://localhost:3000;&lt;/code&gt; - send them all to a server on this machine listening on port 3000. This is where the node/express server is running.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy_set_header Host $host;&lt;/code&gt; - it doesn&amp;rsquo;t matter for the purposes of this api, but it&amp;rsquo;s often nice to tell the server being proxied who the real host receiving the request is. If we didn&amp;rsquo;t do this, the node app would only be able to see that &amp;ldquo;localhost&amp;rdquo; was making a request. By doing this, it knows the request was to the server running nginx, in this case 192.168.100.40, but usually a real domain name. This might be needed if our node app was also servicing more than one domain.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. We&amp;rsquo;re done. You do need to have your node app running on port 3000 on the same machine, but as long as that&amp;rsquo;s happening this should all be working. Do remember to restart nginx each time you make a config changes with &lt;code&gt;sudo service nginx restart&lt;/code&gt;, and it would also be good practice to check the config files with &lt;code&gt;sudo nginx&lt;/code&gt; -t before that.&lt;/p&gt;</description></item><item><title>Where to go after Reddit</title><link>https://blog.iankulin.com/where-to-go-after-reddit/</link><pubDate>Tue, 01 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/where-to-go-after-reddit/</guid><description>&lt;p&gt;A big chunk of my mindless doomscrolling used to go to Reddit, but also, Reddit posts from the various communities were frequently the useful results when googling error messages. I lurked in many a sub-reddit, but only posted in a couple - usually r/self-hosted or r/Homelab.&lt;/p&gt;
&lt;p&gt;The problematic treatment of the communities in the leadup to their IPO has been well publicised, and the short blackout by some subreddits seemed to have zero effect on the company&amp;rsquo;s approach to it&amp;rsquo;s users (which is in fact what they have to sell). Those subreddits, and many others are still working, but (and perhaps I&amp;rsquo;m imagining this) seem somehow thinner. Additionally, I feel like it&amp;rsquo;s a fragile arrangement - the company has shown how they will deal with their communities, so depending on them in the long term does not seem wise, or even, somehow, ethical - like I&amp;rsquo;m crossing a picket line.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a great pity. The format of asynchronous chats around bite-size topics that are promoted or demoted based on user interests is a great format for technology questions.&lt;/p&gt;
&lt;p&gt;In any case, if we are to abandon Reddit, in some sort of fuck-you to u/spez, where to go?&lt;/p&gt;
&lt;p&gt;Currently for me, the answer is probably Lemmy. This is a &amp;lsquo;federated&amp;rsquo; system like Masterdon. You can spin up your own instance, and other instances can decide to block you if they are not happy with your moderation policies - sort of the same as email. Communities (ie subreddits) live on a particular server, but you can subscribe to them from whichever server you have your account on. For example, I&amp;rsquo;m a member of the lemmy.world server where a lot of subreddits and redditors have washed up, but there&amp;rsquo;s a homelab community over at &lt;a href="https://lemmy.ml/c/homelab"&gt;lemmy.ml/c/homelab&lt;/a&gt;. When I&amp;rsquo;m signed in at &lt;a href="https://lemmy.world/"&gt;lemmy.world&lt;/a&gt;, I can access the homelab community - reading and posting - just as if it was hosted where I&amp;rsquo;m logged in.&lt;/p&gt;
&lt;p&gt;The Lemmy system will be familiar to ex-redditors. There&amp;rsquo;s upvotes and downvotes, mods, a sidebar with rules etc. I have noticed that many Lemmy communities have decided to try and use the change to make things a bit more civil. Stackoverflow behavior was never an issue in the selfhosted or homelab subreddits, but it certainly has been an issue elsewhere on the site. This slightly-nicer-on-Lemmy phenomenon is interesting, perhaps &lt;a href="https://www.garbageday.email/"&gt;Ryan Broderick&lt;/a&gt; will make sense of it. An extreme example might be that lemmynsfw.com has decide their rule number two is &amp;lsquo;respect and consent&amp;rsquo;. I don&amp;rsquo;t know (but it&amp;rsquo;s easy to imagine) this might not have been the case for NSFW subreddits.&lt;/p&gt;
&lt;p&gt;Not all communities are translated directly across to Lemmy, an example of that, that I&amp;rsquo;ve followed is that Jim Salter (who was a r/ZFS mod) has set up a Discourse server for &lt;a href="https://discourse.practicalzfs.com/c/openzfs/4"&gt;ZFS&lt;/a&gt; and &lt;a href="https://discourse.practicalzfs.com/c/openzfs/proxmox/7"&gt;Proxmox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I mentioned internet technology journalist Ryan Broderick earlier. One of his theories is that the internet is going through a period where we will not really be having a &lt;a href="https://threadreaderapp.com/thread/1174694510527488000.html"&gt;common internet experience&lt;/a&gt;. With Twitter and Reddit being fractured like they have been in the recent past, that seems like a supportable theory. If it means the little different islands of, say, self-hosted fans are each smaller numbers of people, it does devalue the experience a little - expose each group of users to a smaller range of ideas and thoughts. The upside might be this opportunity to re-think what each community should feel like to be part of.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s been some speculation that the &lt;a href="https://www.youtube.com/watch?v=cXlxMP9PU8I"&gt;sudden decline in Stack Overflow&lt;/a&gt; is because ChatGPT has gobbled up all of it&amp;rsquo;s content, so it&amp;rsquo;s better to just ask ChatGPT your web development questions. It&amp;rsquo;s certainly the case that when I&amp;rsquo;ve been talking to it, ChatGPT has always been present to deal with, and never treated me like an idiot regardless of the noob mistakes I&amp;rsquo;m making. If new Lemmy (or other versions of) subreddits are going to be nicer, I&amp;rsquo;m all for it.&lt;/p&gt;
&lt;p&gt;Doubtless users will slowly vote with their feet, and the situation will evolve over time, but for the moment, I&amp;rsquo;ll be at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lemmy.world/"&gt;lemmy.world&lt;/a&gt; - general stuff&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lemmy.world/c/selfhosted"&gt;lemmy.world/c/selfhosted&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lemmy.ml/c/webdev"&gt;lemmy.ml/c/webdev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lemmy.ml/c/homelab"&gt;lemmy.ml/c/homelab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discourse.practicalzfs.com/c/openzfs/proxmox/"&gt;discourse.practicalzfs.com/c/openzfs/proxmox/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m still reading Twitter/X - not so much for the tech stuff - most of the tech folk I follow have moved to Masterdon, but for a couple of sassy-quipping influencers who haven&amp;rsquo;t made the leap, and for the education accounts I follow who don&amp;rsquo;t seemed to have migrated at all.&lt;/p&gt;</description></item><item><title>ZFS Basics on Proxmox</title><link>https://blog.iankulin.com/zfs-basics-on-proxmox/</link><pubDate>Sat, 29 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/zfs-basics-on-proxmox/</guid><description>&lt;p&gt;I&amp;rsquo;m a keen listener of the &lt;a href="https://2.5admins.com/"&gt;2.5 Admins&lt;/a&gt; podcast in which there&amp;rsquo;s frequent enumeration of the advantages of &lt;a href="https://itsfoss.com/what-is-zfs/"&gt;ZFS&lt;/a&gt; as a file system. So much so, that I&amp;rsquo;ve had occasional twinges or regret about the money I spent on the Synology - although it has been boringly reliable and does everything I need.&lt;/p&gt;
&lt;p&gt;Proxmox has some built in support for ZFS, including through the web GUI. So I&amp;rsquo;ve been itching to give it a try.&lt;/p&gt;
&lt;p&gt;I had a 256GB M2 NVME sitting around - I bought it with the plan to try it as the root drive in one of the servers. That was when I was worried that one of the servers&amp;rsquo; drives was about dead because the SMART data said it was at 100% use. I&amp;rsquo;ve since discovered that various companies interpret that different ways, so probably it&amp;rsquo;s 100% okay.&lt;/p&gt;
&lt;p&gt;I started to think a little &lt;a href="https://www.techtarget.com/searchstorage/definition/JBOD"&gt;JBOD&lt;/a&gt; with a couple of NVME SSD mirrored drives would be a fun project. There&amp;rsquo;s no way I could do that inside the case to get the proper PCI access, but the my HP 800 G2&amp;rsquo;s all have USB 3 so it shouldn&amp;rsquo;t be terrible - probably a lot better than the spinning rust NAS over 1GB Ethernet.&lt;/p&gt;
&lt;p&gt;I purchased this little UNITEK S1206A dual bay enclosure and another stick of 256GB Samsung SSD.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5742.jpg" width="600" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/s-l960.jpg" width="600" alt=""&gt;
&lt;p&gt;The instructions for the unit show sticking a layer of silicon over the top of the gum sticks, and then a thin piece of aluminum. I&amp;rsquo;ve heard these get hot, but it wasn&amp;rsquo;t clear to me if I should peel that paper off first. So I&amp;rsquo;ve done nothing for the moment while I do some more research.&lt;/p&gt;
&lt;p&gt;The process of getting it set up in Proxmox was simple. If you select the node in the web interface, and go in to &lt;em&gt;Disks&lt;/em&gt;, you can see a list of the physical disks attached. The NVME drives showed up as NTFS so I wiped them by selecting the drive and pressing the &lt;em&gt;Wipe Disk&lt;/em&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-6.19.00-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see on the screenshot above, that further down in the Disks list it says ZFS. That&amp;rsquo;s where you go to create the ZFS pool. I probably need to pause here, and go over some of the ZFS terminology.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.victormendonca.com/2020/11/03/zfs-for-dummies/"&gt;&lt;img src="https://blog.iankulin.com/images/zfs-components-1.png" width="706" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To start in the middle, we have the concept of a ZFS Pool. This is, well, a pool of storage that&amp;rsquo;s available to be used. It has a size, and we can see how much space is available. The pool is made up of vdev (virtual devices). A vdev could be a single physical drive, or multiple drives in some kind of RAID arrangement.&lt;/p&gt;
&lt;p&gt;In my situation, with the two NVME drives, my zpool will be made up of a single vdev comprising two physical drives which have been mirrored.&lt;/p&gt;
&lt;p&gt;In the zpool, we can create &lt;em&gt;datasets&lt;/em&gt; where we can actually put some data. You can think of these as directories in the sense they have a name and we can create directories and store data inside them, but in ZFS, the datasets in a zpool can have different settings (such as compression, de-duplication) applied to them. This is also the level where snapshots can be taken for backups.&lt;/p&gt;
&lt;p&gt;To create the ZFS pool in Proxmox, again select the node, then select ZFS in the list under &lt;em&gt;Disks&lt;/em&gt;. At the top is a button for &lt;em&gt;Create ZFS&lt;/em&gt;. Select the wiped drives, chose your RAID and give it a name. By tradition the pools are usually called &amp;rsquo;tank&amp;rsquo; - if you look at a few tutorials you&amp;rsquo;ll see that all over the place.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.26.42-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once that was done, tank appeared as storage in the list under my node. I moved the drives of these dev guests across to it so the zpool would have something to do. I did notice that this process would rush through, then pause for a few seconds - something I haven&amp;rsquo;t noticed when moving guest droves between the NAS and internal SSDs. Early reviews of Samsung pm981 NVME SSD &lt;a href="https://www.tomshardware.com/reviews/samsung-pm981-980-nvme-ssd,5323.html"&gt;noted a sustained write dropoff&lt;/a&gt;, so this might be something to come back and have a look at later.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.23.10-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If we drop into the shell now, we can have a look at the datasets.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve-prod1:/# zfs list
NAME USED AVAIL REFER MOUNTPOINT
tank 32.0G 199G 96K /tank
tank/vm-300-disk-0 16.5G 209G 6.04G -
tank/vm-321-disk-0 5.16G 202G 1.67G -
tank/vm-322-disk-0 5.16G 202G 1.62G -
tank/vm-323-disk-0 5.16G 202G 1.60G -
root@pve-prod1:/# 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So my pool is &lt;code&gt;tank&lt;/code&gt;, and there&amp;rsquo;s been datasets created for each of the VM guests&amp;rsquo; disks. We can create a data set to start using the pool as well.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;zfs create tank/temp_set
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That creates a dataset called &lt;code&gt;temp_set&lt;/code&gt; in the &lt;code&gt;tank&lt;/code&gt; zpool. It will have been mounted for us too. Let&amp;rsquo;s create a 1 GB file in there.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve-prod1:/# cd /tank/temp_set
root@pve-prod1:/tank/temp_set# head -c 1G &amp;lt;/dev/urandom &amp;gt;myfile
root@pve-prod1:/tank/temp_set# ls
myfile
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then if we list the datasets again.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve-prod1:/tank/temp_set# zfs list
NAME USED AVAIL REFER MOUNTPOINT
tank 33.0G 198G 104K /tank
tank/temp_set 1.00G 198G 1.00G /tank/temp_set
tank/vm-300-disk-0 16.5G 208G 6.04G -
tank/vm-321-disk-0 5.16G 201G 1.67G -
tank/vm-322-disk-0 5.16G 201G 1.62G -
tank/vm-323-disk-0 5.16G 201G 1.60G -
root@pve-prod1:/tank/temp_set# 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you&amp;rsquo;ve created a dataset, you can just use it as a regular place to store stuff. ZFS will go on doing it&amp;rsquo;s magic in the background to keep your data safe with copy-on-write and other magic. It&amp;rsquo;s good ZFS practice to do a &lt;em&gt;scrub&lt;/em&gt; every now and then. This causes ZFS to use whatever information it&amp;rsquo;s got to check the integrity of all your data.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve-prod1:/# zpool scrub tank

root@pve-prod1:/# zpool status -v tank
 pool: tank
 state: ONLINE
 scan: scrub repaired 0B in 00:00:53 with 0 errors on Tue Jul 4 20:58:16 2023
config:

 NAME STATE READ WRITE CKSUM
 tank ONLINE 0 0 0
 mirror-0 ONLINE 0 0 0
 sdb ONLINE 0 0 0
 sdc ONLINE 0 0 0

errors: No known data errors
root@pve-prod1:/# 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;While I&amp;rsquo;ve been writing this post, I&amp;rsquo;ve been copying data to and fro (I&amp;rsquo;d try things out, then have to delete and repeat to get the screen shot I wanted, and at one stage I decided to change the name of the zpool so all the disk images had to be moved off then back on after I&amp;rsquo;d recreated it etc) for about 90 minutes, and I&amp;rsquo;ve just been in the server rooms to see if the external NVME enclosure is hot. It&amp;rsquo;s warm to the touch, I&amp;rsquo;d guess 40° - so not alarming for this level of use. That box is pretty well ventilated.&lt;/p&gt;
&lt;p&gt;If you want a good summary of ZFS, particularly the thinking behind it, this is a great overview.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/lsFDp-W1Ks0?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>First Ansible Playbook</title><link>https://blog.iankulin.com/first-ansible-playbook/</link><pubDate>Wed, 26 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/first-ansible-playbook/</guid><description>&lt;p&gt;In the &lt;a href="https://blog.iankulin.com/getting-started-with-ansible/"&gt;previous post&lt;/a&gt;, we looked at getting up and running with Ansible, including using the ad-hoc mode to send commands to our servers. We had a inventory file called hosts that had groups of server IP addresses and a simple &lt;code&gt;ansible.cfg&lt;/code&gt; file that pointed to our inventory file.&lt;/p&gt;
&lt;h3 id="playbooks"&gt;Playbooks&lt;/h3&gt;
&lt;p&gt;Ansible playbooks are used to collect together a description of the state we want in a server. When the playbook is executed, Ansible figures out what things need need changed, and changes them. If you&amp;rsquo;re used to the procedural nature of a bash script, where things proceed from one step to the next, and there might be decision branches, this requires an adjustment in your thinking. This is similar to the adjustment I had getting my head around &lt;a href="https://betterprogramming.pub/swiftui-understanding-declarative-programming-aaf05b2383bd"&gt;SwiftUI&lt;/a&gt;, and moving from JS to &lt;a href="https://levelup.gitconnected.com/why-react-is-declarative-a300d1e930b7?gi=3d11485226b4"&gt;React&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Before we dive in and look at a playbook, I should probably say a couple of things about the &lt;a href="https://www.tutorialspoint.com/yaml/yaml_basics.htm"&gt;YAML&lt;/a&gt; format used for these files. It&amp;rsquo;s yet another attempt to strike a compromise between human readable and machine processable files. Spacing is important, it doesn&amp;rsquo;t like tabs, it&amp;rsquo;s case sensitive, and begins with three hyphens. The rest, you&amp;rsquo;ll figure out.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the files we currently have in our working directory:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.16-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.16-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.02-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The config file just specifies our inventory file, and the inventory file (named &lt;code&gt;hosts&lt;/code&gt;) lists the servers in groups, and provides some &lt;em&gt;variables&lt;/em&gt; for the servers.&lt;/p&gt;
&lt;p&gt;Our web servers are going to need something to serve web pages. Let&amp;rsquo;s write a playbook to ensure they have NGINX installed. If you don&amp;rsquo;t know what &lt;a href="https://www.nginx.com/"&gt;NGINX&lt;/a&gt; is, don&amp;rsquo;t worry about it, it&amp;rsquo;s a web server.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.38.27-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.38.27-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;- name:&lt;/code&gt; - YAML files are hierarchical. Ansible YAML files are a collection of &lt;em&gt;plays&lt;/em&gt;. This file only has one play, named &amp;ldquo;nginx for all web servers&amp;rdquo;. All the plays will be at the top level like this starting with a single hyphen in column 1. Names are great; pick good ones and you won&amp;rsquo;t need much in the way of comments. These names also appear in the output, helping anyone using the playbook to understand what&amp;rsquo;s happening.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hosts: web&lt;/code&gt; - tells Ansible that we are only running this play against our &lt;code&gt;web&lt;/code&gt; servers. These are defined in the &lt;code&gt;hosts&lt;/code&gt; file that we kept from the last post. If you look back up at the top for that file, you can see we&amp;rsquo;re specifying it for 192.168.100.37 and 192.168.100.38&lt;/p&gt;
&lt;p&gt;&lt;code&gt;become: yes&lt;/code&gt; - To install packages with &lt;code&gt;apt&lt;/code&gt;, we need to &lt;code&gt;sudo&lt;/code&gt;. &lt;code&gt;become yes&lt;/code&gt; is telling Ansible that we need to do this. I guess if we were already the &lt;code&gt;root&lt;/code&gt; user we wouldn&amp;rsquo;t have to do that, but in our &lt;code&gt;hosts&lt;/code&gt; file, we&amp;rsquo;ve said to use the user &lt;code&gt;ian&lt;/code&gt; so &lt;code&gt;ssh&lt;/code&gt; in. We&amp;rsquo;ll see later how to deal with needing the password for this escalation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tasks:&lt;/code&gt; - In our hierarchy, each &lt;em&gt;play&lt;/em&gt; consists of a number of &lt;em&gt;tasks&lt;/em&gt;. Here&amp;rsquo;s our list of tasks. Because we&amp;rsquo;re changing levels, they&amp;rsquo;ll be indented.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;- name:&lt;/code&gt; - This time, it&amp;rsquo;s the name of the task. Again, pick good ones.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt:&lt;/code&gt; Ansible functionality is organised according to modules. Here we are saying we are going to use the &lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html"&gt;apt module&lt;/a&gt;. &lt;code&gt;apt&lt;/code&gt; is the command to install packages on Debian flavoured systems&lt;/p&gt;
&lt;p&gt;&lt;code&gt;name : nginx&lt;/code&gt; - This time, it&amp;rsquo;s the name of the package we want installed. It&amp;rsquo;s the same name as you would have used if using &lt;code&gt;apt&lt;/code&gt; manually.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;state: latest&lt;/code&gt; - we&amp;rsquo;re saying we want the nginx package to be installed, and we want it to be the latest one. This is were you should really be noticing the declarative nature of the playbook. We could also say &lt;code&gt;state: absent&lt;/code&gt; and Ansible would uninstall it if it was installed, or &lt;code&gt;state: present&lt;/code&gt; in which case Ansible would just check it&amp;rsquo;s there but not worry about the version.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;- name:&lt;/code&gt; - Okay, we&amp;rsquo;re back up at the lists of tasks level. Here&amp;rsquo;s the name of a new task.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;service:&lt;/code&gt; In the previous task we were using the apt module. This task is going to use the &lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/service_module.html"&gt;service module&lt;/a&gt;. If you&amp;rsquo;re wondering how you get to know what things are in each module, the &lt;a href="https://docs.ansible.com/ansible/latest/module_plugin_guide/index.html#"&gt;documentation at Ansible&lt;/a&gt; is pretty great. Sometimes you&amp;rsquo;ll get pretty close by thinking of what you&amp;rsquo;re doing. In this case, we want to check if the NGINX service is running, and if not start it - so it&amp;rsquo;s logical that the module we want is going to be &lt;code&gt;service&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;name : nginx&lt;/code&gt; - This time, it&amp;rsquo;s the name of the service.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;state: started&lt;/code&gt; - declaratively saying what we want the state of the NGINX service to be.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;enabled: yes&lt;/code&gt; - it will also be started on a reboot.&lt;/p&gt;
&lt;p&gt;Phew. Okay. We want NGINX to be installed on these machines, and for the service to be running and for that to still be the case after a reboot. Let&amp;rsquo;s run this playbook and see what happens. Here&amp;rsquo;s the command we&amp;rsquo;re going to do that with.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible-playbook web_installs.yaml --ask-become-pass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;--ask-become-pass&lt;/code&gt; piece of this command is telling Ansible to ask us for the password for this user so it can have sudo privileges to install things. We could have just added the password in the hosts file like we have the user name, but that would be quite insecure. Especially when we push our code up to github. Scanning pubic github commits for passwords and API keys is a popular pastime.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.38.19-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;After asking me for the password, Ansible has correctly identified the two servers and gathered facts from them. The facts are a lot of information that&amp;rsquo;s then stored in variables that we can then use in our playbooks. For example this playbook is assuming a Linux distro that uses the apt package manager. If we wanted to check for that, one of the facts variables would contain the distro name and we could use that to conditionally use apt or some other package manager.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ve probably noticed the colours. Green messages mean something&amp;rsquo;s in the correct state, yellow means it wasn&amp;rsquo;t in the correct state before, but is now, and red means it&amp;rsquo;s not in the correct state, and couldn&amp;rsquo;t be made to be for some reason.&lt;/p&gt;
&lt;p&gt;Since this is the first time this playbook&amp;rsquo;s been run against these servers, we expected the &amp;rsquo;nginx installed&amp;rsquo; tasks to be yellow for both servers. The highlighted IP address under &amp;rsquo;nginx running&amp;rsquo; is just because I was copying it to go and check the web server was working. Let&amp;rsquo;s have a look.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-3.36.22-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well done Ansible.&lt;/p&gt;
&lt;p&gt;In regard to those yellow messages where Ansible found that NGINX wasn&amp;rsquo;t installed, so it went ahead and installed them, you might be thinking &amp;ldquo;if we run the playbook again, shouldn&amp;rsquo;t they be green?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.50.42-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s our first playbook done. We&amp;rsquo;ve only learned the commands for installing packages and working with services, but Ansible can do pretty much anything. Certainly anything you can do by sshing in and running a script of some kind. I don&amp;rsquo;t think I want to go any further with trying to show the range of things that can be accomplished (although it is tempting to now install a web page into our servers) - it makes more sense for you to just find what you need as you need it.&lt;/p&gt;
&lt;p&gt;There is however one problem I ran into almost immediately and couldn&amp;rsquo;t find a simple description of that I&amp;rsquo;ll cover in the next post. Every Saturday morning, I ssh into my local and remote servers (15 of them) and run &lt;code&gt;apt update&lt;/code&gt; and &lt;code&gt;apt upgrade&lt;/code&gt;. You can see from the yaml above, that&amp;rsquo;s going to be quite easy to automate with Ansible and save me heaps of time and effort. My problem is - all my servers have unique user names and passwords. It&amp;rsquo;s not possible to just add a &amp;ndash;ask-become-pass to my command; that would only work for the first one.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll look at how to solve that securely in a future post.&lt;/p&gt;</description></item><item><title>Proxmox 8.0 Install</title><link>https://blog.iankulin.com/proxmox-8-0-install/</link><pubDate>Sun, 23 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-8-0-install/</guid><description>&lt;p&gt;I&amp;rsquo;m normally a x.1 release type of sysadmin, but the increasing temptation of installing Proxmox 8.0 while I&amp;rsquo;ve got some time off, and the fact that I&amp;rsquo;ve got a cluster, so I can just move the VM&amp;rsquo;s around all adds up to thinking I&amp;rsquo;ll do that today.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/cluster-2.png" width="328" alt=""&gt;
&lt;p&gt;Here&amp;rsquo;s how my system works. It consists of three HP-800 mini G2&amp;rsquo;s. &lt;code&gt;pve-prod1&lt;/code&gt; is a bit fancier - i7 6700T and 32GB, the other two are i5 6500T and 16GB. The production VM&amp;rsquo;s use the local SSD but backups go to the NAS. All the machines are currently running Proxmox 7.4. They are not clustered in the proper sense - I don&amp;rsquo;t need high availability, and I don&amp;rsquo;t want to run them all the time. &lt;code&gt;pve-prod1&lt;/code&gt; runs 24/7 and I just power up &lt;code&gt;pve-dev1&lt;/code&gt; when I&amp;rsquo;m working on something.&lt;/p&gt;
&lt;p&gt;The intention is that although I&amp;rsquo;m not on high availability, I can quickly come back from a machine failure by powering &lt;code&gt;pve-prod2&lt;/code&gt; up and restoring from the latest VM backup from the NAS. &lt;code&gt;pve-prod1&lt;/code&gt; does not have a full load yet (I&amp;rsquo;m slowly cancelling cloud services and moving them in-house) but once it does, I&amp;rsquo;d have the capacity to fully replace it by sharing any guests between &lt;code&gt;pve-prod2&lt;/code&gt; and &lt;code&gt;pve-dev1&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="migration-plan"&gt;Migration plan&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/migration-1.png" width="273" alt=""&gt;
&lt;p&gt;Currently &lt;code&gt;pve-prod1&lt;/code&gt; is only running two guests, jellyfin, and a docker host with a collection of smallish services. The plan is to move those to &lt;code&gt;pve-prod2&lt;/code&gt;, check everything is working, then install the new Proxmox 8 onto &lt;code&gt;pve-prod1&lt;/code&gt;. Apart from giving me the opportunity to do that, it&amp;rsquo;s a good test of the plan for recovering from a &lt;code&gt;pve-prod1&lt;/code&gt; failure. I&amp;rsquo;ll live off it for a few days to ensure that it&amp;rsquo;s a viable process.&lt;/p&gt;
&lt;p&gt;A small hitch with this is that the RAM in &lt;code&gt;pve-prod1&lt;/code&gt; cost me $100, and I didn&amp;rsquo;t want to not use it, so I created the jellyfin VM with 16GB RAM. It&amp;rsquo;s a simple matter to stop it, give it less, and restart it - except it seems to be using it all.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-7.31.59-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see from this, I tried shutting it down and restarting - thinking that the memory use might climb up slowly as the app was used, but it just went straight back to 15GB. In a way, I approve of a VM using the memory I&amp;rsquo;ve given it - presumably it is caching or something. Jellyfin should certainly be able to run on a machine with much less memory, so I suppose I&amp;rsquo;ll stop it, back it up, and try it in a smaller VM.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-7.42.58-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Yep, that works fine. And I can&amp;rsquo;t notice any difference in the app performance. So I stopped it, backed it up, and restored onto prod2. And immediately bumped into a couple of problems when I tried to start it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.52.34-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There was two hardware incompatibilities - the first was that on prod1 I had passed through the GPU from the host (in an unsuccessful attempt to use quicksync hardware transcoding for video). I don&amp;rsquo;t need that, so that gets deleted out of the &amp;lsquo;hardware&amp;rsquo; for the VM.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.47.00-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And the second was that I still had the Debian 11 ISO mounted in the &amp;lsquo;cd-rom&amp;rsquo;. Lol - the Debian installer specifically tells you to remove this before it reboots. That can be removed exactly as I had done for the GPU pass through, and the VM boots fine, and the app tests out ok.&lt;/p&gt;
&lt;p&gt;The first time I ever did this - move a guest VM from one lot of hardware to another, then boot it up and all my apps are working perfectly on their old IP addresses - I was amazed and danced around in excitement. I didn&amp;rsquo;t dance today, but it is so cool.&lt;/p&gt;
&lt;p&gt;Interestingly, it&amp;rsquo;s decided to use much less RAM now. I caused that increase at the end of the graph by rescanning the media library, then browsing through all the titles so the cover images would have to be loaded - so perhaps it&amp;rsquo;s the web server caching them all. It&amp;rsquo;s hard to know for sure without some objective measurements, but I suspect the app was crisper and more responsive than before. In any case, it certainly wasn&amp;rsquo;t any worse.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-9.02.56-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Moving the docker host over was straightforward and only took five minutes of downtime as it&amp;rsquo;s a smaller image. I guess a lot of that time is just my 1GB network limitation or the spinning disk transfer speed from the NAS - the docker hoats was 4GB and Jellyfin 14GB.&lt;/p&gt;
&lt;h3 id="nuke-and-pave"&gt;Nuke and pave&lt;/h3&gt;
&lt;p&gt;I try and keep my hosts very clean, so wiping them and starting over is no biggie, but since this node has been up I have installed a chron job for &lt;a href="https://blog.iankulin.com/linux-shell-script-for-temperature-logging/"&gt;temperature logging&lt;/a&gt;. I&amp;rsquo;ve documented that in a blog post so I&amp;rsquo;ll be able to recreate it, but this sort of thing is the reason I&amp;rsquo;m interested in &lt;a href="https://blog.iankulin.com/getting-started-with-ansible/"&gt;Ansible&lt;/a&gt;. Another project while I&amp;rsquo;ve got some time will be to recreate that on the new machine with Ansible so it&amp;rsquo;s trivial to restore in future. I pulled the temperature log file down though - because who doesn&amp;rsquo;t like eighty thousand data points.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/temp1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There is a &lt;a href="https://pve.proxmox.com/wiki/Upgrade_from_7_to_8"&gt;published process to upgrade Proxmox&lt;/a&gt; from 7.x to 8, so I briefly considered it, but fresh installs are generally less likely to lead to drama, especially this early in the major release cycle. Plus, I keep my installs clean to allow it - this is a freedom allowed by my sysadmin discipline along with the investment in redundant hardware so there&amp;rsquo;s zero time pressure while I&amp;rsquo;m doing it.&lt;/p&gt;
&lt;h3 id="run-book-for-new-proxmox-install"&gt;Run Book for New Proxmox Install&lt;/h3&gt;
&lt;p&gt;My install process for Proxmox goes something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flash the ISO onto a USB drive with &lt;a href="https://etcher.balena.io/"&gt;Balena Etcher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Plug in the USB drive, my bluetooth keyboard/mouse USB, and the screen - I&amp;rsquo;ve got a special long HDMI cord that reaches from my desk to the servers&lt;/li&gt;
&lt;li&gt;Boot up, mashing the boot menu key (F9 on my G2&amp;rsquo;s)&lt;/li&gt;
&lt;li&gt;Follow my nose through the prompts - since this is an existing server, the DHCP serves up the correct IP address&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh&lt;/code&gt; into it to check everything&amp;rsquo;s fine. Since this IP was already in my known hosts file, I had to go an delete it out&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh-copy-id&lt;/code&gt; to get my ssh keys across&lt;/li&gt;
&lt;li&gt;Update the repositories - by default, Proxmox comes set up to use with a subscription. I wish they had a lower tier and I&amp;rsquo;d by one since it gives me so much joy - even if it didn&amp;rsquo;t remove the nags. In the meantime, you can follow the instructions &lt;a href="https://pve.proxmox.com/wiki/Package_Repositories#sysadmin_no_subscription_repo"&gt;here&lt;/a&gt; to set it up to use the non-subscription repoistories:
&lt;ul&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list&lt;/code&gt; to add &lt;code&gt;deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list.d/pve-enterprise.list&lt;/code&gt; to comment out the line in there&lt;/li&gt;
&lt;li&gt;and a new one that&amp;rsquo;s not mentioned on that wiki page, edit &lt;code&gt;/etc/apt/sources.list.d/ceph.list&lt;/code&gt; to comment out the line in there. I don&amp;rsquo;t know where that leaves you if you are using Ceph (which is a cool file system if you&amp;rsquo;re using high availability) but I&amp;rsquo;m not, so all good. If you don&amp;rsquo;t do this, you&amp;rsquo;ll get errors like &lt;code&gt;E: Failed to fetch https://enterprise.proxmox.com/debian/ceph-quincy/dists/bookw orm/InRelease 401 Unauthorized IP: 103.76.41.50 4431 E: The repository &amp;quot;https://enterprise.proxmox.com/debian/ceph-quincy bookworm In Release' is not signed.&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Run the updates with &lt;code&gt;apt update&lt;/code&gt; &amp;amp;&amp;amp; &lt;code&gt;apt upgrade&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install the certificate - you need SSL setup for the web interface if you want Chrome to let it save your password, which I do. Also the red &lt;em&gt;insecure&lt;/em&gt; message bugs me
&lt;ul&gt;
&lt;li&gt;Log into the web interface at https://&lt;ip address&gt;:8006 - you&amp;rsquo;ll need to jump through all those hoops to take on the responsibility of opening an unsecured site&lt;/li&gt;
&lt;li&gt;If you click on the node, then certificates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-12.08.29-pm.png" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;You can open up that certificate, and copy out the raw certificate, paste it into a text editor and save it somewhere. I drag that into my macOS keychain app. It shows up with a red cross, but if you open it up you can mark it as &amp;ldquo;always trust&amp;rdquo;&lt;/li&gt;
&lt;li&gt;We&amp;rsquo;re not done yet, now back in Chrome, click on the &lt;em&gt;insecure&lt;/em&gt; message next to the URL. Go into &lt;em&gt;Site Settings&lt;/em&gt; | &lt;em&gt;Insecure Content&lt;/em&gt; and change it to &lt;em&gt;Allow&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Almost there - at the top of those settings is a button to clear the cache, do that&lt;/li&gt;
&lt;li&gt;Reload the page. Profit.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Then I &lt;a href="https://tailscale.com/kb/1031/install-linux/"&gt;install Tailscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Last of all, add my NAS to the storage. I use NFS. The only trick here is to go into the dropdown of what type of content is on that storage, and select everything&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-12.17.35-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. Nice new Proxmox. I&amp;rsquo;ll leave my production VM&amp;rsquo;s on pve-prod2 for a week, and move all of my dev work over to this machine so it gets some exercise before I upgrade the other machines.&lt;/p&gt;
&lt;h3 id="tailscale"&gt;Tailscale&lt;/h3&gt;
&lt;p&gt;The only small issue I ran into (apart from the Ceph repository) was I couldn&amp;rsquo;t access the machine via it&amp;rsquo;s &amp;ldquo;magic DNS&amp;rdquo; Tailscale name. Since it was going to be the same name as a machine in my existing network, I&amp;rsquo;d thought ahead and deleted the old one out via the &lt;a href="https://login.tailscale.com/admin/machines"&gt;Tailscale machines&lt;/a&gt; page, but even so, it wouldn&amp;rsquo;t connect from my laptop.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-11.45.38-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I assume the old Tailscale IP address was cached somewhere, and fixed it by turning Tailscale off and on again on my laptop.&lt;/p&gt;</description></item><item><title>Getting Started with Ansible</title><link>https://blog.iankulin.com/getting-started-with-ansible/</link><pubDate>Wed, 19 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-started-with-ansible/</guid><description>&lt;p&gt;Ansible is a system for executing commands on remote systems. It allows a declarative approach - so if you run a playbook (the system configuration files are called playbooks) that says a system has a Docker container running Jellyfin, Ansible will check if that&amp;rsquo;s true, and if not, make it so. Ansible is best used when you have a large number of systems to maintain, but even with a small number, it serves to document systems as well as to automate their creation.&lt;/p&gt;
&lt;p&gt;Since, with Ansible, system configurations can be completely described, it&amp;rsquo;s a step in the journey to &amp;ldquo;infrastructure as code&amp;rdquo; and allows infrastructure to be version controlled, and lends itself to Git-Ops where you push a change to a playbook file, and it&amp;rsquo;s executed to make that description of the configuration reality on your servers. The list of servers is stored in a file called the inventory.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=wgQ3rHFTM4E"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-02-at-11.28.10-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve considered implementing it a couple of times, but put it off as soon as I started looking at these complicated yaml files. Jeff Geerling&amp;rsquo;s &lt;a href="https://www.ansiblefordevops.com/"&gt;&amp;ldquo;Ansible for DevOps&amp;rdquo;&lt;/a&gt; seemed like the perfect place to start, but then he uses Vagrant and VirtualBox in his early examples, and Vagrant&amp;rsquo;s integration with Ansible means things are not being done in a standard way and I couldn&amp;rsquo;t follow along without mirroring his setup. I don&amp;rsquo;t want to run VM&amp;rsquo;s on my laptop, I want to use my homelab VMs or a VPS - both of which I think would be a more common setup.&lt;/p&gt;
&lt;p&gt;This mini guide is just a start. I&amp;rsquo;ll step through to the point where you have a yaml file describing a system configuration that can be applied to a VM to install some software. After that, you probably want to buy Jeff&amp;rsquo;s book, hit up some &lt;a href="https://www.youtube.com/watch?v=wgQ3rHFTM4E"&gt;good v&lt;/a&gt;ideos, or head to the &lt;a href="https://docs.ansible.com/"&gt;Ansible documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-02-at-11.16.33-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-02-at-11.16.33-am.png" width="118" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For this to be helpful to you, you probably need to have been mucking about running Linux servers. You know how to ssh into them and have set up key pairs to allow that without typing your password each time. You can write a bash script (but don&amp;rsquo;t want to), You know how to install software with apt/yum/pip/homebrew etc. You should go and install it now. Note that you also need python (preferably 3) on the host.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve saved a run book of the things you need to do to recreate particular setups or deal with common issues, then you are at the exact point that Ansible is going to make your life better.&lt;/p&gt;
&lt;p&gt;Get &lt;a href="https://docs.ansible.com/ansible/latest/installation_guide/installation_distros.html"&gt;Ansible installed&lt;/a&gt;, you do need an up to date Python. You also need to have ssh set up for each of the nodes (servers) you are going to manage, preferably including using keys rather than passwords.&lt;/p&gt;
&lt;h3 id="starting-concepts"&gt;Starting Concepts&lt;/h3&gt;
&lt;p&gt;Ansible can execute &lt;em&gt;playbooks&lt;/em&gt; which are yaml files setting out the actions needed or final state of the node to be achieved. Alternatively, single commands can be executed from the command line in &amp;lsquo;ad-hoc mode&amp;rsquo;. When setting things up, ad-hoc mode is a good starting place to check you&amp;rsquo;ve installed everything correctly since it&amp;rsquo;s simpler.&lt;/p&gt;
&lt;p&gt;Ansible &lt;em&gt;modules&lt;/em&gt; are bits of code to support particular pieces of functionality. You could think of them as code libraries. For example, there&amp;rsquo;s an &lt;code&gt;apt&lt;/code&gt; module that enables Ansible to execute commands related to package management on the Debian family of Linux distros. Similar to code libraries, you&amp;rsquo;ll need to know which library is needed for the functionality you want to use. Luckily, Ansible&amp;rsquo;s documentation is excellent, and as with your programming, you&amp;rsquo;ll soon become familiar with the ones you use all the time.&lt;/p&gt;
&lt;h3 id="demo-environment"&gt;Demo Environment&lt;/h3&gt;
&lt;p&gt;For the following examples, I&amp;rsquo;ve set up three virtual machines (VM&amp;rsquo;s) 192.168.100.37 - 192.168.100.39 running Debian. I use Proxmox on my servers, so it looks a bit like this.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-6.46.26-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re trying things from a single machine, you could install something like &lt;a href="https://www.virtualbox.org/"&gt;VirtualBox&lt;/a&gt; to create VM&amp;rsquo;s, or I&amp;rsquo;d probably recommend just commissioning a VPS on &lt;a href="https://www.linode.com/lp/podcasts/?ifso=ssh&amp;amp;utm_source=podcast&amp;amp;utm_medium=audio&amp;amp;utm_campaign=ssh"&gt;Linode&lt;/a&gt; or &lt;a href="https://cloud.digitalocean.com/registrations/new"&gt;Digital Ocean&lt;/a&gt;. They both have deals whereby you get a dollar amount credit for signing up, for the minimal machine you need to try these things out, you&amp;rsquo;re probably looking at a cost of $0.30 an hour. I&amp;rsquo;m in Australia, so my VPS&amp;rsquo;s are on &lt;a href="https://www.binarylane.com.au/register"&gt;Binary Lane&lt;/a&gt; which costs less that AUD5 a month for a low-end instance.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running against multiple machines, you&amp;rsquo;ll make your life easier by having the same user name on each one. For example, the commands I use to &lt;code&gt;ssh&lt;/code&gt; into mine are:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh ian@192.168.100.37
ssh ian@192.168.100.38
ssh ian@192.168.100.39
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="inventory"&gt;Inventory&lt;/h3&gt;
&lt;p&gt;Ansible has the concept of an &lt;em&gt;Inventory&lt;/em&gt;. The Inventory is a text file of the servers/nodes (I&amp;rsquo;m just going to say nodes from now on). We need this inventory whether using playbooks or ad-hoc commands. Here&amp;rsquo;s mine, which I&amp;rsquo;ve saved in the directory I&amp;rsquo;m working from as &lt;code&gt;hosts&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;192.168.100.37
192.168.100.38
192.168.100.39
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that these could also be domain names if your nodes are set up on DNS.&lt;/p&gt;
&lt;h3 id="first-command"&gt;First Command&lt;/h3&gt;
&lt;p&gt;Finally, we&amp;rsquo;re at the point we can run something. Let&amp;rsquo;s try this command to find the host name of each node. There&amp;rsquo;s a lot going on, so we&amp;rsquo;ll break it down after we&amp;rsquo;ve looked at the output.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible -i hosts all -u ian -a &amp;#34;hostname&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-7.41.00-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s break down all those arguments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-i hosts&lt;/code&gt; - the inventory flag points to the inventory file. In my example the file is named &amp;ldquo;hosts&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;all&lt;/code&gt; - we&amp;rsquo;re saying to execute this against all of the nodes in the &lt;code&gt;hosts&lt;/code&gt; file. Later on we&amp;rsquo;ll see how to separate the nodes into groups inside the inventory file and this will make more sense.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-u ian&lt;/code&gt; - the ssh user name for each node&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-a &amp;quot;hostname&amp;quot;&lt;/code&gt; - the command to run&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What Ansible has actually done here is ssh into each node and use python to execute the command. Collected the output, then formatted that for us to see. Here it is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;192.168.100.37 | CHANGED | rc=0 &amp;gt;&amp;gt;
vm321-deb
192.168.100.38 | CHANGED | rc=0 &amp;gt;&amp;gt;
vm322-deb
192.168.100.39 | CHANGED | rc=0 &amp;gt;&amp;gt;
vm323-deb
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s our node IP addresses. The &lt;code&gt;rc=0&lt;/code&gt; is the successful return code, then there&amp;rsquo;s the actual host names - &lt;code&gt;vm321-deb&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;But, what&amp;rsquo;s going on with &lt;code&gt;CHANGED&lt;/code&gt;? Ansible always indicates some sort of status - things like &lt;code&gt;CHANGED&lt;/code&gt;, &lt;code&gt;SUCCESS&lt;/code&gt;, &lt;code&gt;FAILED&lt;/code&gt; etc. In this case, there should not have been any change - we were just retrieving the hose names, not altering them. The best answer is just ignore this for now. The long answer is that when we&amp;rsquo;re using &lt;code&gt;-a&lt;/code&gt; to run commands on a node, Ansible&amp;rsquo;s &lt;code&gt;command&lt;/code&gt; module isn&amp;rsquo;t able to tell if there have been changes or not, so it reports &lt;code&gt;CHANGED&lt;/code&gt; as a better safe than sorry approach.&lt;/p&gt;
&lt;p&gt;Even though it&amp;rsquo;s possible to use Ansible to run native commands, when there is an equivalent Ansible module that can carry out the same action, it&amp;rsquo;s always better to use that. The reason is that that module code is smart enough to see if something needs done or not. If it does not need done, it will just return &lt;code&gt;SUCCESS&lt;/code&gt;, if it needs done, it will carry out what&amp;rsquo;s needed and return &lt;code&gt;CHANGED&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="idempotence"&gt;Idempotence&lt;/h3&gt;
&lt;p&gt;Every Ansible tutorial includes this word, which I have never encountered anywhere else. A command is idempotent if the result is the same no matter how many times it is executed. In the case of Ansible, this is because it checks if something is needed before it does it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at an example. If I wanted to create a test directory in the home folder of each of my machines, the Ansible module for this is the file module. I could use this command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible -i hosts all -u ian -m file -a &amp;#34;path=test state=directory&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;-m&lt;/code&gt; tells Ansible with module to use, and our arguments after the &lt;code&gt;-a&lt;/code&gt; flag tell Ansible that the state we want to achieve is a directory named &lt;code&gt;test&lt;/code&gt;. Let&amp;rsquo;s run that and have a look at the output:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.03.12-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That makes sense, each one is CHANGED because we needed to create the directory. Let&amp;rsquo;s run it again and see what happens.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.03.25-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This time, since the directory is there, there&amp;rsquo;s no need to change it. Ansible checks for the directories existence before it bothers to create it - because it is idempotent.&lt;/p&gt;
&lt;h3 id="ansiblecfg"&gt;ansible.cfg&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m getting a bit sick of this long command. We can move the inventory file name to a config file to save the typing. Create an ansible.cfg file in your working directory like this.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[defaults]
inventory = hosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we can eliminate that from our command line input.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.14.25-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d also like to get rid of the &lt;code&gt;-u ian&lt;/code&gt; from each command. That&amp;rsquo;s not stored in the .cfg file. Since it&amp;rsquo;s likely that your nodes will have different user names in a real situation, they can be stored in the inventory file.&lt;/p&gt;
&lt;h3 id="inventory-file"&gt;Inventory file&lt;/h3&gt;
&lt;p&gt;We started off with a very simple inventory file - literally just a list of IP addresses. let&amp;rsquo;s revisit that to add the ssh user, and while we&amp;rsquo;re there, we can group the nodes according to their functions - this will come in handy later.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[web]
192.168.100.37
192.168.100.38

[db]
192.168.100.39

[web:vars]
ansible_ssh_user=ian

[db:vars]
ansible_ssh_user=ian
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here I&amp;rsquo;ve created two groups for my nodes, a &lt;code&gt;web&lt;/code&gt; group and a &lt;code&gt;db&lt;/code&gt; group. I&amp;rsquo;ve also set the ssh_user for each group. Now that argument can be left out of out commands. So to get the hostnames now, we can just say:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible all -a &amp;#34;hostname&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So much neater! Additionally, since our nodes are in groups now, we can specify the group if we don&amp;rsquo;t want to execute the command on all nodes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.38.41-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.38.41-am.png" width="472" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s probably as far as I want to go in this post. We&amp;rsquo;ve got our heads around some early Ansible concepts, learned how to use the Ad-Hoc commands to do things to our nodes, learned a big word that won&amp;rsquo;t ever come up again except in coding interviews, and seen how to set up the ansible.cfg and inventory files.&lt;/p&gt;
&lt;p&gt;The real power to be unleashed is using Ansible playbooks. We&amp;rsquo;ll look at them next.&lt;/p&gt;</description></item><item><title>How to recover a docker run command</title><link>https://blog.iankulin.com/how-to-recover-a-docker-run-command/</link><pubDate>Sun, 16 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-recover-a-docker-run-command/</guid><description>&lt;p&gt;Imagine if, lets say hypothetically, you&amp;rsquo;d set up an application months ago with a &lt;code&gt;docker run&lt;/code&gt; command. Then you&amp;rsquo;d heard there had been an update to the app because of a security update. So you need to stop/remove the container, pull a new image and restart it, trouble is, you don&amp;rsquo;t remember the exact &lt;code&gt;run&lt;/code&gt; command you used to start it.&lt;/p&gt;
&lt;p&gt;This didn&amp;rsquo;t happen to me, since all my vm setups are in git as markdown (I&amp;rsquo;m pre-Ansible), but I did google how to do this thinking that there would be an easy way before I bothered to look through my config files.&lt;/p&gt;
&lt;h3 id="short-answer"&gt;Short answer&lt;/h3&gt;
&lt;p&gt;There isn&amp;rsquo;t a docker command that will retrieve your run command for you.&lt;/p&gt;
&lt;h3 id="long-answer"&gt;Long answer&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s probably still in your bash history, try:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;history | grep &amp;#34;docker run&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If that doesn&amp;rsquo;t work, it must have been a long time ago.&lt;/p&gt;
&lt;p&gt;Most likely, the crucial information you want to know will be the ports you specified, the network setup, and any directories you&amp;rsquo;ve bound or volumes used. All of this information is available from the &lt;code&gt;docker inspect&lt;/code&gt; command, but you&amp;rsquo;re going to have to trawl through it a bit. Search for &lt;code&gt;Mounts&lt;/code&gt; to see what you did there:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;Mounts&amp;#34;: [
 {
 &amp;#34;Type&amp;#34;: &amp;#34;volume&amp;#34;,
 &amp;#34;Name&amp;#34;: &amp;#34;jellyfin-config&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/var/lib/docker/volumes/jellyfin-config/_data&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/config&amp;#34;,
 &amp;#34;Driver&amp;#34;: &amp;#34;local&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;z&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;&amp;#34;
 },
 {
 &amp;#34;Type&amp;#34;: &amp;#34;bind&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/mnt/media/video&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/media&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;rprivate&amp;#34;
 },
 {
 &amp;#34;Type&amp;#34;: &amp;#34;volume&amp;#34;,
 &amp;#34;Name&amp;#34;: &amp;#34;jellyfin-cache&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/var/lib/docker/volumes/jellyfin-cache/_data&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/cache&amp;#34;,
 &amp;#34;Driver&amp;#34;: &amp;#34;local&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;z&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;&amp;#34;
 }
 ],
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="better-answer"&gt;Better Answer&lt;/h3&gt;
&lt;p&gt;Investigate &lt;code&gt;[docker compose](https://docs.docker.com/compose/compose-file/)&lt;/code&gt; to save some effort next time.&lt;/p&gt;</description></item><item><title>Updating SSL Certificates</title><link>https://blog.iankulin.com/updating-ssl-certificates/</link><pubDate>Wed, 12 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/updating-ssl-certificates/</guid><description>&lt;p&gt;When I first installed my SSL certificates, &lt;a href="https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/"&gt;I mentioned&lt;/a&gt; it&amp;rsquo;s a process I need to automate before they came up for expiry, but here we are ten days out, and I haven&amp;rsquo;t done that yet, but I have been keeping an eye on it though the excellent display and notifications set up in &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.36.01-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.36.01-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Updating the certificates is easy. When I went into the site at PorkBun (where I purchased the domain and who do the primary DNS for the site, the next certificates were sitting there to be downloaded. My existing certificates were due to expire on 30th July, and these had been generated on 3rd July.&lt;/p&gt;
&lt;p&gt;The bundle included the same files as last time. You might remember from last &lt;a href="https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/"&gt;time&lt;/a&gt; that we need to join the &lt;code&gt;domain.cert.pem&lt;/code&gt; and &lt;code&gt;intermediate.cert.pem&lt;/code&gt; to make the &lt;code&gt;fullchain.pem&lt;/code&gt; file. I had just &lt;code&gt;cat&lt;/code&gt;&amp;rsquo;d them together and this had caused an issue as there&amp;rsquo;s no newline character at the end of the first file. I got smarter this time and googled up this &lt;a href="https://stackoverflow.com/questions/8183191/concatenating-files-and-insert-new-line-in-between-files/23549826#23549826"&gt;solution&lt;/a&gt; which did the trick by using echo to insert the newline:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.57.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.57.44-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once that was done, I uploaded them to the nginx directory where I stored them last time. Nginx reloads the config on restart, although there&amp;rsquo;s probably a neater way as well, so I just restarted the container with Docker compose to pick up the new certificates. While I was doing that I got the ping from Uptime Kuma via &lt;a href="https://ntfy.sh/"&gt;ntfy&lt;/a&gt; to say it was down, then up. I had a look at the display, and it&amp;rsquo;s showing I&amp;rsquo;ve got another 84 days left on the cert.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-6.10.32-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-6.10.32-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, 84 days for me to get around to automating this.&lt;/p&gt;</description></item><item><title>Unlearning Relational DB</title><link>https://blog.iankulin.com/unlearning-relational-db/</link><pubDate>Sun, 09 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/unlearning-relational-db/</guid><description>&lt;p&gt;Some of my first university programming was writing &lt;a href="https://en.wikipedia.org/wiki/CICS"&gt;CICS&lt;/a&gt; COBOL transactions against IBM&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/IBM_Db2"&gt;DB2&lt;/a&gt; relational database. My commercial work after uni was mostly written in Clipper which was a sort of compiled version of &lt;a href="https://en.wikipedia.org/wiki/DBase"&gt;dBase&lt;/a&gt; and used the same data file format. The minimal web work I did in the before times relied on &lt;a href="https://en.wikipedia.org/wiki/MySQL"&gt;MySQL&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All of which is to say, I&amp;rsquo;m very comfortable designing relational database schema, and I understand what&amp;rsquo;s going on at the disk level when they are being accessed and written to.&lt;/p&gt;
&lt;p&gt;But the world moves on, and now developers have some amazing &lt;a href="https://en.wikipedia.org/wiki/NoSQL"&gt;NoSQL&lt;/a&gt; databases. Of these, the document store types such as MongoDB, CouchDB, and Firebase are of the most interest to me. My stack for my first couple of realistic scale web-apps is likely to be Node/Express/EJS/MongoDB.&lt;/p&gt;
&lt;p&gt;In a document database, each collection contains a number of documents. In the ones I&amp;rsquo;m looking at these are basically JSON objects, so this could be a document:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.mongodb.com/docs/manual/introduction/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-01-at-8.14.06-am.png" width="402" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this example (from the MongoDB manual) &lt;code&gt;groups&lt;/code&gt; is an array of string, but it could just as easily be an array of objects. Documents can be big (&lt;a href="https://www.youtube.com/watch?v=jm66TSlVtcc"&gt;Fireship says try to keep them under 1MB&lt;/a&gt;) so a large collection of child objects is a realistic possibility.&lt;/p&gt;
&lt;p&gt;This all raises a couple of questions for me. What are the factors to consider when designing a database schema for a document store database?, and (probably less importantly) what are the implementation details? If we add an extra group to the example above, does a new document get written and this one voided?&lt;/p&gt;
&lt;h3 id="designing-database-schema"&gt;Designing Database Schema&lt;/h3&gt;
&lt;p&gt;Lots of good stuff available about this. I started here:&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/3GHZd0zv170?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;h3 id="implementation"&gt;Implementation&lt;/h3&gt;
&lt;p&gt;In a fixed-sized record database (most relational databases) you can edit records in place since you know the offsets. Or if you want to ensure the changes are atomic, write the edited record to a blank slot, and mark the old one as available to be reused. By necessity, each document in a nosql database can be any size, so they can&amp;rsquo;t be edited in place. As well as that, it&amp;rsquo;s possible for a document to grow during an edit (for example if we added a new group to the array in the example above).&lt;/p&gt;
&lt;p&gt;The best (from a dev point of view) answer to this is don&amp;rsquo;t worry about it. It&amp;rsquo;s abstracted away so you don&amp;rsquo;t have to think about it. Of course there are edge cases, usually involving large scale implementations, where these things would start to affect database design decisions, but for most cases not.&lt;/p&gt;
&lt;p&gt;The actuall answer is going to be complex, but it is going to involve a couple of things for sure.&lt;/p&gt;
&lt;p&gt;The first is that modern situations make good use of memory caches so nearly all edits occur in memory rather than disk. Often this is paralleled with writing a journal to disk to allow for a recovery in an unexpected power down situation.&lt;/p&gt;
&lt;p&gt;Another strategy is to write deltas for documents, such that a complete up to date document might consist of the original document, then a list of changes to it that can be used to recreate the document when it is retrieved.&lt;/p&gt;
&lt;p&gt;Both of these strategies suggest that some periodic maintenance is required - rewriting the full documents to disk (often called snapshots). Implementations will be doing parts this maintenance in spare cycles as they go along, on a time basis, or manually - for example the &lt;a href="https://www.mongodb.com/docs/manual/core/wiredtiger/"&gt;WiredTiger&lt;/a&gt; storage engine from MongoDB has a &lt;code&gt;[compact](https://www.mongodb.com/docs/manual/reference/command/compact/)&lt;/code&gt; command that goes through, writing all the documents in full and rebuilding all the indexes to save disk space and speed up operations.&lt;/p&gt;
&lt;p&gt;But again, best not to worry about all that, and to rather focus on how your app needs to access and use the data, and build your schema with those factors in mind.&lt;/p&gt;</description></item><item><title>How to deploy a Node.js app</title><link>https://blog.iankulin.com/how-to-deploy-a-node-js-app/</link><pubDate>Wed, 05 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-deploy-a-node-js-app/</guid><description>&lt;p&gt;This is one of those things that is simple once you know it. I had my &lt;a href="https://blog.iankulin.com/using-node-js-to-return-a-static-file/"&gt;tiny Node service working&lt;/a&gt; on my MacBook, but how do I run it on the server?&lt;/p&gt;
&lt;h3 id="native-or-container"&gt;Native or Container&lt;/h3&gt;
&lt;p&gt;Obviously I need Node.js installed on the server, should I have it in a Docker container, or native on the machine. There&amp;rsquo;s no clear answer here - in a container set up with Docker Compose might be more in line with my ideology of treating machines as disposable, but a native install is simpler, and I probably want to make life simpler at this stage when I&amp;rsquo;m learning everything.&lt;/p&gt;
&lt;h3 id="installing-node"&gt;Installing Node&lt;/h3&gt;
&lt;p&gt;This took me down a bigger rabbit hole than I was expecting. My VPS is Unbuntu LTS 22.04.2, so I spun one of those up in a VM on the homelab to try things out.&lt;/p&gt;
&lt;p&gt;A quick google search suggested the &lt;a href="https://github.com/nodesource/distributions"&gt;NodeSource binary distributions&lt;/a&gt;. That involves curling a big script (when I pasted the script into ChatGPT it said it wasn&amp;rsquo;t malicious). I could chose the Node version, so I grabbed 20.x That was as painless as you&amp;rsquo;d expect.&lt;/p&gt;
&lt;p&gt;Then I started wondering why I couldn&amp;rsquo;t just &lt;code&gt;apt install&lt;/code&gt; it. If I could do that, it would reduce the chance of a supply chain attack since I&amp;rsquo;d have the power of Canonical on my side. So I rolled the previous install back (thank you Proxmox backups of VMs), and tried:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt install nodejs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That worked fine - Node is in the Ubuntu packages, but the version is &lt;a href="https://nodejs.dev/en/about/releases/"&gt;quite old&lt;/a&gt; - v12.22.9. This is on the current Ubuntu LTS 22.04.2. I don&amp;rsquo;t think it will matter for my purposes, but it explains why you&amp;rsquo;d do something other than just this.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also going to need &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt;, so lets get that with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt install npm&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That seemed to download a heap more stuff that the node install.&lt;/p&gt;
&lt;h3 id="deploying-your-project"&gt;Deploying your project&lt;/h3&gt;
&lt;p&gt;Again, the first search result was more complicated than I needed. The advice was to clone my repository onto the server where I wanted to deploy. This is such a minor project, I hadn&amp;rsquo;t pushed it up to GitHub. So that seemed excessive. You know, not everything has to be DevOps CI/CD! I mean, we ain&amp;rsquo;t talking about a very complicated project here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-06-26-at-8.34.20-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got this tiny source file, and the text file I want to serve. All the dependencies (just Express) are in the &lt;code&gt;package.json&lt;/code&gt;, so presumably that&amp;rsquo;s all I need on the server to get going.&lt;/p&gt;
&lt;p&gt;I &lt;code&gt;scp&lt;/code&gt;&amp;rsquo;d those from my laptop to a directory on the folder:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-06-26-at-8.41.19-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once they are there, I need to install the packages from the package.json, so we do that with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That installed 59 packages (presumably Express plus 58 of it&amp;rsquo;s dependencies). Then I started the app with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;node .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and it worked!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-06-26-at-8.55.34-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="insomnia"&gt;Insomnia&lt;/h3&gt;
&lt;p&gt;I should probably explain what you&amp;rsquo;re looking at above. I could have tested this little node server by going to the api address in a browser and checked that I got back the text file I was expecting. And in Chrome (and I assume Firefox) there are developer tools that would show the return code etc. However, most of the REST API videos I&amp;rsquo;ve watched use a better tool - mostly &lt;a href="https://www.postman.com/"&gt;Postman&lt;/a&gt;. These sort of tools give you a heap of other capabilities, none of which I really need for this simple project, but will be very handy for more complex APIs where there is a body to the request.&lt;/p&gt;
&lt;p&gt;The only reason I&amp;rsquo;m using &lt;a href="https://insomnia.rest/"&gt;Insomnia&lt;/a&gt; instead of Postman is that when I tried Postman, it straightaway wanted some of my data to make it work. Insomnia hasn&amp;rsquo;t forced me to do that yet.&lt;/p&gt;</description></item><item><title>Using Node.js to return a static file</title><link>https://blog.iankulin.com/using-node-js-to-return-a-static-file/</link><pubDate>Sun, 02 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-node-js-to-return-a-static-file/</guid><description>&lt;p&gt;As mentioned in the &lt;a href="https://blog.iankulin.com/complicating-the-temperature-api/"&gt;previous post&lt;/a&gt;, stage one is just to return the same static text file, but from the Node server, rather than NGINX. That&amp;rsquo;s non-trivial to a rank beginner since I need to figure out 1) how to serve a static file from Node, and 2) how to configure NGINX to hand off calls to the API to Node. This post will look at both of those, but it&amp;rsquo;s first probably worth just setting out what each of the puzzle pieces are.&lt;/p&gt;
&lt;h3 id="nginx"&gt;NGINX&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.nginx.com/"&gt;NGINX&lt;/a&gt; is a web server - it listens on a port (classically 80 and 443 - http and https) and responds to those requests. Usually by returning some files. However, it can also pass those requests off to something else. This process is called Reverse Proxying. Currently I have NGINX set up to just serve a static text file, but in the change I&amp;rsquo;m proposing, NGINX will pass an API request off to Node.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/JKxlsvZXG7c?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;h3 id="nodejs"&gt;Node.js&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://nodejs.org/en"&gt;Node&lt;/a&gt; is JavaScript packaged up to run on a server, instead of inside a browser. There&amp;rsquo;s lots of different languages we can write server-side code in, and many have some strengths over Javascript. Part of the motivation for using Node might be that web developers have already invested significantly in learning JavaScript to use on the front-end, so it makes sense to use those same skills on the back end.&lt;/p&gt;
&lt;p&gt;A major difference from some other server-side scripting languages (for example, PHP) is that Node is non-blocking, making use of call-backs to handle events resulting in high performance at scale. It&amp;rsquo;s trivial to write a static web server in Node, but that is to seriously under-use it&amp;rsquo;s capability.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/jOupHNvDIq8?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;h3 id="expressjs"&gt;Express.js&lt;/h3&gt;
&lt;p&gt;Once you start writing backends in Node, you&amp;rsquo;ll find yourself writing a lot of the same code over and over to achieve some standard things - time for a framework. &lt;a href="https://expressjs.com/"&gt;Express&lt;/a&gt; is one of the most popular web frameworks for writing APIs on Node. Using Express makes that job simpler and leaves you with cleaner, more succinct code. It&amp;rsquo;s can be argued that there are better frameworks, but at around 5 miliion downloads per day, I think we can regard it as a standard approach to the problems it solves.&lt;/p&gt;
&lt;h3 id="serve-a-static-file-from-node"&gt;Serve a static file from Node&lt;/h3&gt;
&lt;p&gt;I said it was trivial. Here&amp;rsquo;s the code, then we&amp;rsquo;ll discuss it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-06-25-at-8.45.20-am.png" alt="const express = require(&amp;rsquo;express&amp;rsquo;);
const app = express();
const PORT = 3000;
app.get(&amp;quot;/api/gnp_temp.txt&amp;quot;, (req, res) =&amp;gt; {
res.status(200).sendFile(__dirname + &amp;lsquo;/gnp_temp.txt&amp;rsquo;);
});
app.listen(PORT, () =&amp;gt; {console.log(`Listening on port ${PORT}`)});"&gt;&lt;/p&gt;
&lt;p&gt;PORT is the port we&amp;rsquo;re listening on. In this case 3000. So if I open a URL on http://localhost:3000 that request will be handled by this code. The actual work is done in these lines:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/api/gnp_temp.txt&amp;#34;, (req, res) =&amp;gt; {
 res.status(200).sendFile(__dirname + &amp;#39;/gnp_temp.txt&amp;#39;);
});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is only looking for requests to &lt;code&gt;:3000/api/gnp_temp.txt&lt;/code&gt; - everything else is ignored. But if it gets that request, it will return a result status of &lt;code&gt;200&lt;/code&gt; (success) along with the file &lt;code&gt;gnp_temp.txt&lt;/code&gt; from the current directory.&lt;/p&gt;
&lt;p&gt;If you are wondering about setting up the environment to get to the point where you can run and understand this. There are lots of great videos - &lt;a href="https://www.youtube.com/watch?v=SccSCuHhOw0"&gt;Web Dev Simplified&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=pKd0Rpw7O48"&gt;Code with Mosh&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=KNa-wMpry00"&gt;Code with Con&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now that it Works on My Machine™ I need to figure out how to deploy it.&lt;/p&gt;</description></item><item><title>Complicating the Temperature API</title><link>https://blog.iankulin.com/complicating-the-temperature-api/</link><pubDate>Wed, 28 Jun 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/complicating-the-temperature-api/</guid><description>&lt;p&gt;I&amp;rsquo;ve been slammed with other work, so my web dev learning has fallen well behind. Luckily, the YouTube procrastination algorithm noticed this and suggested I watch a video from &lt;a href="https://www.youtube.com/@codewithcon"&gt;CodeWithCon&lt;/a&gt; titled &lt;a href="https://www.youtube.com/watch?v=KNa-wMpry00&amp;amp;list=PLkJHe6eU_tzeoe7vKUEa4MrS74CpVEwdI&amp;amp;index=3&amp;amp;t=305s"&gt;Learn Backend in 10 MINUTES&lt;/a&gt;.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/KNa-wMpry00?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;Since I was watching a video of a guy learning to land a C152 at St Baths (a skill I do &lt;em&gt;not&lt;/em&gt; need) at the time, it was hard to argue with myself that I didn&amp;rsquo;t have ten minutes to learn all of backend programming.&lt;/p&gt;
&lt;p&gt;I mean, &lt;em&gt;all&lt;/em&gt; of backend programming in 10 minutes is a big claim, but the video did do a surprising good job of simple REST APIs in &lt;a href="https://nodejs.org/en"&gt;Node&lt;/a&gt; using the &lt;a href="http://expressjs.com/"&gt;Express&lt;/a&gt; framework.&lt;/p&gt;
&lt;p&gt;I abandoned iOS programming a year ago when I started to think about the sort of applications I wanted to develop, and saw they would need to run against cloud databases, and so I was going to have to learn backend web dev at some stage anyway, and if so, learning that, then writing the front-ends for web seemed like a lower friction, and wider audience approach.&lt;/p&gt;
&lt;p&gt;I have &lt;em&gt;sort&lt;/em&gt; of created an API to solve my &lt;a href="https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/"&gt;temperature logging problem&lt;/a&gt;. A Python script runs as a cron job every 5 minutes on a VPS, calls a weather API, parses the json and drops the values I want into a text file on an NGINX server which can be called with a straightforward GET.&lt;/p&gt;
&lt;p&gt;While that was great to learn a bit of Python, it&amp;rsquo;s not pretty, or standard. It does solve the problem I intended (I wanted that weather data for three servers running at home, but didn&amp;rsquo;t want to hammer the weather API I was using for free) it has a few other problems. As the cron job on the VPS runs each five minutes, the data there can be up to five minutes behind the API, and since the cron jobs on my servers are running on the same five minute intervals, and the call to the Australian VPS is quicker than the API call to the US based API, I&amp;rsquo;m always returning the VPS data from five minutes ago - so now my data is up to ten minutes old.&lt;/p&gt;
&lt;p&gt;Does that matter for this application? No, but the whole exercise was for learning, and this is a good enough reason to improve it my making it even more unnecessarily complicated.&lt;/p&gt;
&lt;p&gt;I think my new system will be that the homelab servers will still poll the VPS, but the VPS will be a Node.js endpoint. When it receives a GET from one of the servers, it will check the age of it&amp;rsquo;s current weather data. If it&amp;rsquo;s less than a minute, it will return that, if it&amp;rsquo;s older than a minute, it will call the weather API, store that and return it.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/20230624-weather.drawio-1.png" width="512" alt=""&gt;
&lt;p&gt;Apart from reducing the latency of the outside temperature data, this has a couple of other benefits. The first is that my VPS won&amp;rsquo;t go on for ever requesting the weather API data after I&amp;rsquo;ve reloaded the operating system on the home servers and completely forgotten about this project. The second is that the temperatures in the data I&amp;rsquo;m getting back look like they only change every 20 minutes, so probably they are stale before I ever get them from Open Weather. There are live weather station web pages that I could scrape for better data, so doing things in node on the VPS leaves a good option open for that future improvement.&lt;/p&gt;
&lt;p&gt;To chunk the project down to really small bite sizes, I&amp;rsquo;ll to it in two parts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first will just be to replicate the current system - return a text file when receiving a GET - in Node. That way I will have dealt with the issue of running Node behind NGINX on the VPS.&lt;/li&gt;
&lt;li&gt;The second part will be to expand that to call the weather API from inside the Node program when it&amp;rsquo;s needed.&lt;/li&gt;
&lt;li&gt;A possible third part would be to convert it all to JSON instead of text, and then deal with that in the Python scripts running on the servers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s the plan.&lt;/p&gt;</description></item><item><title>HDD Swap on A1278 MacBook Pro</title><link>https://blog.iankulin.com/hdd-swap-on-a1278-macbook-pro/</link><pubDate>Wed, 10 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hdd-swap-on-a1278-macbook-pro/</guid><description>&lt;p&gt;My MacBook died, I guess about three years ago. It was randomly difficult for a week or so, but then just behaving as if it had no hard drive at all. It&amp;rsquo;s been in a drawer ever since waiting for me to replace the hard drive and see if I could sell it, which I never quite got to.&lt;/p&gt;
&lt;p&gt;I mentioned a while ago that I&amp;rsquo;d &lt;a href="https://blog.iankulin.com/linux-on-hp-mini-110/"&gt;borrowed an old Atom powered HP Mini 110&lt;/a&gt; to play with a Linux desktop machine, partly for fun &amp;amp; learning, and partly for a first-class SPICE experience (also fun). Meanwhile I&amp;rsquo;ve got an old but still sexy Intel MacBook Pro sitting in a drawer - that doesn&amp;rsquo;t make sense!&lt;/p&gt;
&lt;p&gt;So I ordered an 2.5&amp;quot; SDD in order to resurrect the MacBook. This era (2012) of MacBooks are quite repairable - even the RAM is just regular SODIMM.&lt;/p&gt;
&lt;h3 id="instructions"&gt;Instructions&lt;/h3&gt;
&lt;p&gt;Lay it upside down and remove the screws with a tiny (I guess #00) Philips screwdriver. Note that the screws don&amp;rsquo;t come out perpendicular to the table you&amp;rsquo;re working on - the top four almost do, but the others are perpendicular to the tangent at the point of the screw hole. ie, the case is curved, so take that into account. It doesn&amp;rsquo;t matter much for removing them, but now is the time to notice.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve got the laptop turned around so you can read the writing, then the long screws are the three top right ones. If you lay them out in the order you remove them, you won&amp;rsquo;t have to remember that.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5087.jpg" width="1008" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_5088.jpg" width="1008" alt=""&gt;
&lt;p&gt;Pick the cover up from the back and it just lifts off.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_5067.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The HDD is in the bottom right corner there. It&amp;rsquo;s locked in with that black plastic retainer you can see above the drive. Use your little Philips screwdriver to undo the two screws holding it in place (they don&amp;rsquo;t come right out), then lift out the retainer and put it down somewhere carefully - you will need to put it back the right way later.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5069.jpg" width="1008" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_5072.jpg" width="1008" alt=""&gt;
&lt;p&gt;That plastic tab is for lifting the far side of the drive out. Only lift it far enough to loosen the remove the SATA plug from the drive, then lift the whole drive out.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5073.jpg" width="1008" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_5075.jpg" width="1008" alt=""&gt;
&lt;p&gt;Once it&amp;rsquo;s unplugged, the drive will lift away from the bottom.&lt;/p&gt;
&lt;p&gt;There are four little lug things screwed into the mounting screw holes on the drive. You&amp;rsquo;ll need to remove them and shift them over to the new drive. They nestle into the little round shock mounts at the case edge. You need a tiny torx driver for those lugs. I&amp;rsquo;m not sure what size, but the driver I bought for taking Nokia 5110&amp;rsquo;s apart fits perfectly.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5081.jpg" width="1008" alt=""&gt;
&lt;p&gt;Re-installation is just the opposite of taking it all apart. Be gentle with the SATA connector. I tried moving that sticky tab over to the new drive, but it wasn&amp;rsquo;t interested in re-sticking. So I McGyvered a bit of packing tape.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5085.jpg" width="1008" alt=""&gt;</description></item><item><title>Containers</title><link>https://blog.iankulin.com/containers/</link><pubDate>Sun, 07 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/containers/</guid><description>&lt;p&gt;There&amp;rsquo;s a few things that really strike me as significant improvements to life since I was commercially developing 20 years ago:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accessing information - the first time I &lt;em&gt;bought&lt;/em&gt; the development stack to write commercial software against the Windows SDK it came in a huge carton with, I guess, fifteen or so 2&amp;quot; thick books. That was how you looked things up in those days. Fast forward to an internet connected world of websites, stack exchange, Discord and ChatGPT. So much better.&lt;/li&gt;
&lt;li&gt;Open Source - is an actual useful thing that the entire connected world runs on - not just a weird hippy idea. It&amp;rsquo;s almost routine to open source your code now and everyone benefits from that.&lt;/li&gt;
&lt;li&gt;Containers - &amp;ldquo;getting things working&amp;rdquo; used to be a thing. Most times now I want to spin something up to play with it, it just works because all the dependencies are bundled with it, and it doesn&amp;rsquo;t mutate the environment in any way I don&amp;rsquo;t know about. There&amp;rsquo;s no friction to run a giant app, and no hangover for the OS when I nuke it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I love this great explanation from Coderized about containers - I wish I&amp;rsquo;d seen it five months ago.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/J0NuOlA2xDc?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Beginning Python</title><link>https://blog.iankulin.com/beginning-python/</link><pubDate>Fri, 05 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/beginning-python/</guid><description>&lt;p&gt;I&amp;rsquo;ve gotten interested in &lt;a href="https://plaintextaccounting.org/"&gt;Plain Text Accounting&lt;/a&gt;, and I&amp;rsquo;m looking at using &lt;a href="https://beancount.github.io/"&gt;BeanCount&lt;/a&gt; for small business &amp;amp; personal finances. If you haven&amp;rsquo;t heard of this, it&amp;rsquo;s a command line program that uses text files with a human comprehensible syntax to define transactions, then acts on them to create the needed reports etc. A side benefit for developers is that it&amp;rsquo;s then easily version controlled using GIT, and of course there&amp;rsquo;s VS Code plugins for it.&lt;/p&gt;
&lt;p&gt;Transactions in one of these BeanCount text files looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-30-at-11.40.25-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A good way to enter all these transactions is from your bank statement. Banks make this data available to customers, usually in a range of formats that are digestible by accounting programs. One of the formats I can get this data in looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;116-002 123456789&amp;#34;,&amp;#34;05/07/2022&amp;#34;,&amp;#34;WDL05&amp;#34;,&amp;#34;-49.99&amp;#34;,&amp;#34;RETAIL PURCHASE&amp;#34;,&amp;#34;PAYPAL *EVERNOTE1, 4029357733&amp;#34;,&amp;#34;0107 AUD000000004999&amp;#34;
&amp;#34;116-002 123456789&amp;#34;,&amp;#34;03/07/2022&amp;#34;,&amp;#34;WDL05&amp;#34;,&amp;#34;-95.34&amp;#34;,&amp;#34;RETAIL PURCHASE&amp;#34;,&amp;#34;PUMA ENERGY MOUN,MOUNT MELVILL&amp;#34;,&amp;#34;0207 AUD000000009534&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So the question is how do I get from one format to the other. BeanCount does have the hooks to enable you to write some Python to facilitate these imports, but I might want to do some extra processing - for example, the only thing I ever buy from Mt Melvile Puma is diesel for my ute, so that transaction can be written without any human intervention.&lt;/p&gt;
&lt;p&gt;So what I need is a command line program that takes the bank&amp;rsquo;s CSV input and spits out nicely formatted BeanCount format transaction text with as much of my work done for me as possible. I decided to do this in Python for a few reasons, but mostly because it&amp;rsquo;s the right tool for this sort of job, I might be able to contribute something back to BeanCount, and I should really learn the Python basics.&lt;/p&gt;
&lt;p&gt;The rest of this post looks at snippets of things I learned in the process. They might be helpful to someone completely new to Python but with some other programming experience.&lt;/p&gt;
&lt;h3 id="python-environment"&gt;Python Environment&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m all in on &lt;a href="https://code.visualstudio.com/"&gt;VS Code&lt;/a&gt; these days, and I assumed Python was already installed on my Mac somewhere since BeanCount is in Python and I&amp;rsquo;d been using that all evening. Sure enough, when I created a .py file, VS Code suggested the Microsoft Plugin, then when I allowed that it prompted my to set up the environment, which I also agreed to. It found two versions of Python - a HomeBrew one and the BeanCount one which was a bit newer.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d chosen that, it created a .venv, and I could type in Python code and execute it from VS Code, or drop into the terminal and run it at the command line. The whole operation was delightful.&lt;/p&gt;
&lt;h3 id="python"&gt;Python&lt;/h3&gt;
&lt;p&gt;Python is a high-level, general-purpose, dynamically typed language. It is interpreted and garbage collected - I sort of think of it as scripting, but with a real programming language - but that is majorly underselling it.&lt;/p&gt;
&lt;p&gt;Coming from Delphi/C++/Swift the use of indentation to define code blocks is slightly discomfiting, but no doubt I&amp;rsquo;ll get use to it. Python uses a combination of the colon and white space for blocks.&lt;/p&gt;
&lt;h3 id="command-line-arguments"&gt;Command Line Arguments&lt;/h3&gt;
&lt;p&gt;My plan for this utility is that I&amp;rsquo;ll type something like this at the command line to process the bank CSV file into a text file full of the BeanCount transactions:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;python csv2bean.py bank.csv &amp;gt; 2022_July.bean
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So the first problem is grabbing the command line arguments. There&amp;rsquo;s a library to import for that - &lt;code&gt;sys&lt;/code&gt;. The arguments will be in an array &lt;code&gt;argv[]&lt;/code&gt;so the demo code could look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import sys

print(&amp;#39;Self name: &amp;#39;, sys.argv[0])
if len(sys.argv) &amp;gt; 1:
 print(&amp;#39;First argument: &amp;#39;, sys.argv[1])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-30-at-10.53.24-am.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="csv-processing"&gt;CSV Processing&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s a couple of library options for processing CSV files. One is just called &lt;code&gt;csv&lt;/code&gt;, and the other &lt;code&gt;pandas&lt;/code&gt;. &lt;code&gt;pandas&lt;/code&gt; is a popular library that does all sorts of powerful stuff for data processing type tasks. For our simple needs, &lt;code&gt;[csv](https://docs.python.org/3/library/csv.html)&lt;/code&gt; seems fine.&lt;/p&gt;
&lt;p&gt;csv.reader(filename) will give us a collection of lines to iterate through, and we can break each line into a row array which will contain each piece of text.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# process a bank extended csv file to a beancount transaction
import csv, sys

if len(sys.argv) &amp;lt; 2:
 print(&amp;#39;Error: supply a file name to process&amp;#39;)
input_file = sys.argv[1]

with open(input_file, &amp;#39;r&amp;#39;) as csv_file:
 reader = csv.reader(csv_file)
 for row in reader:
 if len(row) &amp;lt; 6:
 print(&amp;#39;Error: expected more fields: &amp;#39;)
 print(row)
 trans_date = row[1]
 trans_type = row[2]
 trans_amount = float(row[3])
 trans_narration = row[4]
 trans_payee = row[5]
 print(&amp;#34;On &amp;#34;+trans_date+ &amp;#34; &amp;#34;+str(trans_amount)+&amp;#34; paid to &amp;#34;+trans_payee+&amp;#34; for &amp;#34;+trans_narration)
 csv_file.close()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-30-at-11.18.03-am.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="python-functions"&gt;Python Functions&lt;/h3&gt;
&lt;p&gt;Beancount dates have to be in the format of YYYY-MM-DD and my Australian bank ones are currently DD/MM/YYYY so I need to do a little text processing. That seems like something that should be split off into a function.&lt;/p&gt;
&lt;p&gt;Functions are defined with &lt;code&gt;def&lt;/code&gt;, can accept arguments and return values with &lt;code&gt;return&lt;/code&gt;. Here&amp;rsquo;s a (non-functional) version of the function to format the date:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def flip_date(dmy_date):
 return dmy_date
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can think of strings as arrays of characters, so the chopping and changing we want to do will end up like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;def flip_date(dmy_date):
 if len(dmy_date) &amp;lt; 10:
 print(&amp;#39;Error: date too short: &amp;#39;+dmy_date)
 return dmy_date[6:10]+&amp;#39;-&amp;#39;+dmy_date[3:5]+&amp;#39;-&amp;#39;+dmy_date[0:2]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The only notable thing there is that when we slice out a sub-string with these indexes, the second index is exclusive - ie it&amp;rsquo;s the position &lt;em&gt;after&lt;/em&gt; the last position we&amp;rsquo;re interested in.&lt;/p&gt;
&lt;h3 id="output"&gt;Output&lt;/h3&gt;
&lt;p&gt;That&amp;rsquo;s about all the work we need to do for this program, all that&amp;rsquo;s left is to spit it out in the correct format.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;print(trans_date+ &amp;#39; * &amp;#34;&amp;#39;+trans_payee+&amp;#39;&amp;#34; &amp;#34;&amp;#39;+trans_narration+&amp;#39;&amp;#34;&amp;#39;)
print(&amp;#39; Assets:Cheque: &amp;#39;+str(trans_amount)+&amp;#39; AUD&amp;#39;)
if trans_amount &amp;lt; 0.0:
 print(&amp;#39; Expenses:GST_InputCredits: &amp;#39;+f&amp;#39;{-trans_amount/11:.2f}&amp;#39;+&amp;#39; AUD&amp;#39;)
 print(&amp;#39; Expenses:Unknown\n&amp;#39;)
else:
 print(&amp;#39; Income:Unknown\n&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Most of that is self evident - string concatenation in Python is with the &lt;code&gt;+&lt;/code&gt; operator, and the &lt;code&gt;str()&lt;/code&gt; function does the type conversion from integers or floating point numbers.&lt;/p&gt;
&lt;p&gt;In Australia there&amp;rsquo;s a 10% GST (similar to VAT or sales tax), so the output code checks if this is a debit, and if so, calculates the GST by dividing the amount by 11. This is usually going to end in a number with a lot of decimal places, so it needs to the rounded to the nearest cent. That&amp;rsquo;s what this weird looking thing is doing:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;f&amp;#39;{-trans_amount/11:.2f}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s basically the &lt;code&gt;printf()&lt;/code&gt; type formats. The f at the start signifies that&amp;rsquo;s what&amp;rsquo;s happening, then the format goes after the colon.&lt;/p&gt;
&lt;h3 id="resources"&gt;Resources&lt;/h3&gt;
&lt;p&gt;Most everything I googled while figuring things out came up with useful results from either &lt;a href="https://www.w3schools.com/python/python_functions.asp"&gt;W3 School&lt;/a&gt;, or &lt;a href="https://www.digitalocean.com/community/tutorials/parse-csv-files-in-python"&gt;DigitalOcean&lt;/a&gt;, so I&amp;rsquo;d suggest checking them out for more.&lt;/p&gt;</description></item><item><title>Outside Temperature From an API in a Shell Script</title><link>https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/</link><pubDate>Wed, 03 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/</guid><description>&lt;p&gt;I&amp;rsquo;m interested in &lt;a href="https://blog.iankulin.com/linux-shell-script-for-temperature-logging/"&gt;collecting some internal temperature data&lt;/a&gt; from my servers to look at the effect of adding an NMVe drive. Last week we had a couple of warm days immediately followed by a couple of cool ones. I imagine a 20° ambient temperature change could effect the server temperatures, so I thought it would be good to add that to my temperature logs.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have a weather station or other automated system for collecting the temperature, but there are several commercial sources for this data which, while probably not as good as a sensor in the server room, will be fine for our purposes.&lt;/p&gt;
&lt;p&gt;One of the more well known weather APIs was &lt;a href="https://darksky.net/dev"&gt;Dark Sky&lt;/a&gt;, they got bought up by Apple and now similar data is available in the &lt;a href="https://developer.apple.com/weatherkit/get-started/"&gt;WeatherKit API&lt;/a&gt;. I hold a developer program membership, so that would be free to use for the frequency I need, but the API and sign up looked a bit complex, so I looked elsewhere.&lt;/p&gt;
&lt;p&gt;OpenWeather have a &lt;a href="https://openweathermap.org/current"&gt;simple API&lt;/a&gt; (including one intended to make changing over from Dark Sky easy), &lt;a href="https://openweathermap.org/price"&gt;a good free tier&lt;/a&gt;, and simple sign up - no credit card required. On the free tier I can pull the current weather for a location 22 times a minute continuously. Since I&amp;rsquo;m only collecting my server temps on a five minute cycle, that will be more than fine.&lt;/p&gt;
&lt;p&gt;Even thought the API would allow it, it seems wasteful, and greedy (since I&amp;rsquo;m not paying for it), to pull the same data three times (for each of the three servers), so to complicate things (and learn some interesting stuff) I decided to poll the OpenWeather API once every five minutes from my VPS, process that current weather JSON down to just the temperature I was after, then expose that as a http endpoint. Then each of my servers would poll the VPS to get that outside temp as part of their logging.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/20230425-weather.drawio-1.png" width="435" alt=""&gt;
&lt;p&gt;This will all extend involve some scripting that I haven&amp;rsquo;t encountered yet.&lt;/p&gt;
&lt;h3 id="vps--weather-api"&gt;VPS / Weather API&lt;/h3&gt;
&lt;p&gt;The OpenWeather API couldn&amp;rsquo;t be more straightforward, you sign up with an email and get an API token, then it&amp;rsquo;s just this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;https://api.openweathermap.org/data/2.5/weather?lat={lat}&amp;amp;lon={lon}&amp;amp;appid={API key}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s a couple of options for language and units, I went with &lt;em&gt;metric&lt;/em&gt;, then you get have some JSON.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
 &amp;#34;coord&amp;#34;: {
 &amp;#34;lon&amp;#34;: 118,
 &amp;#34;lat&amp;#34;: -33.93
 },
 &amp;#34;weather&amp;#34;: [
 {
 &amp;#34;id&amp;#34;: 803,
 &amp;#34;main&amp;#34;: &amp;#34;Clouds&amp;#34;,
 &amp;#34;description&amp;#34;: &amp;#34;broken clouds&amp;#34;,
 &amp;#34;icon&amp;#34;: &amp;#34;04d&amp;#34;
 }
 ],
 &amp;#34;base&amp;#34;: &amp;#34;stations&amp;#34;,
 &amp;#34;main&amp;#34;: {
 &amp;#34;temp&amp;#34;: 12.59,
 &amp;#34;feels_like&amp;#34;: 11.68,
 &amp;#34;temp_min&amp;#34;: 12.59,
 &amp;#34;temp_max&amp;#34;: 12.59,
 &amp;#34;pressure&amp;#34;: 1007,
 &amp;#34;humidity&amp;#34;: 68,
 &amp;#34;sea_level&amp;#34;: 1007,
 &amp;#34;grnd_level&amp;#34;: 976
 },
 &amp;#34;visibility&amp;#34;: 10000,
 &amp;#34;wind&amp;#34;: {
 &amp;#34;speed&amp;#34;: 7.39,
 &amp;#34;deg&amp;#34;: 307,
 &amp;#34;gust&amp;#34;: 11.23
 },
 &amp;#34;clouds&amp;#34;: {
 &amp;#34;all&amp;#34;: 64
 },
 &amp;#34;dt&amp;#34;: 1682401802,
 &amp;#34;sys&amp;#34;: {
 &amp;#34;country&amp;#34;: &amp;#34;AU&amp;#34;,
 &amp;#34;sunrise&amp;#34;: 1682375848,
 &amp;#34;sunset&amp;#34;: 1682415263
 },
 &amp;#34;timezone&amp;#34;: 28800,
 &amp;#34;id&amp;#34;: 2070753,
 &amp;#34;name&amp;#34;: &amp;#34;Gnowangerup&amp;#34;,
 &amp;#34;cod&amp;#34;: 200
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From this, I want to extract the temperature, and the unix timestamp &amp;ldquo;dt&amp;rdquo;. Here&amp;rsquo;s my bash script.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash

weather_text=`curl -s &amp;#34;https://api.openweathermap.org/data/2.5/weather?lat=-33.93&amp;amp;lon=118.00&amp;amp;appid=somegiantrandomUIDtypenumber&amp;amp;units=metric&amp;#34;`

temp_text=`echo $weather_text | awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1`
time_text=`echo $weather_text | awk -F&amp;#39;&amp;#34;dt&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1`

log_file=&amp;#34;/home/ian/iankulin.com/www/gnp_temp.txt&amp;#34;

printf &amp;#34;%s,%s&amp;#34; $temp_text $time_text &amp;gt; $log_file
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Of note, and that I haven&amp;rsquo;t already discussed:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;weather_text=`curl -s &amp;#34;https://api.openweathermap.org/data/2.5/weather?lat=-33.93&amp;amp;lon=118.00&amp;amp;appid=somegiantrandomUIDtypenumber&amp;amp;units=metric&amp;#34;`
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; basically sends out a network request the same as if you had typed it into the top of your browser. If it was a web page, it would return the text of the HTML, but in this case it returns the JSON I showed before - although less formatted.&lt;/p&gt;
&lt;p&gt;weather_text is a variable to which we are assigning the return value of the curl - ie the string of JSON. Note the backticks `` the curl is enclosed in. This is how the script knows to execute the command and assign the results rather than assigning some text beginning with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;temp_text=`echo $weather_text | awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1`
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Oh man, this took me on a journey. Firstly, keep in mind I&amp;rsquo;ve prettified the JSON above, actually the string looked like this, so it wasn&amp;rsquo;t possible to process it on a line by line.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{&amp;#34;coord&amp;#34;:{&amp;#34;lon&amp;#34;:118,&amp;#34;lat&amp;#34;:-33.93},&amp;#34;weather&amp;#34;:[{&amp;#34;id&amp;#34;:803,&amp;#34;main&amp;#34;:&amp;#34;Clouds&amp;#34;,&amp;#34;description&amp;#34;:&amp;#34;broken clouds&amp;#34;,&amp;#34;icon&amp;#34;:&amp;#34;04d&amp;#34;}],&amp;#34;base&amp;#34;:&amp;#34;stations&amp;#34;,&amp;#34;main&amp;#34;:{&amp;#34;temp&amp;#34;:12.59,&amp;#34;feels_like&amp;#34;:11.68,&amp;#34;temp_min&amp;#34;:12.59,&amp;#34;temp_max&amp;#34;:12.59,&amp;#34;pressure&amp;#34;:1007,&amp;#34;humidity&amp;#34;:68,&amp;#34;sea_level&amp;#34;:1007,&amp;#34;grnd_level&amp;#34;:976},&amp;#34;visibility&amp;#34;:10000,&amp;#34;wind&amp;#34;:{&amp;#34;speed&amp;#34;:7.39,&amp;#34;deg&amp;#34;:307,&amp;#34;gust&amp;#34;:11.23},&amp;#34;clouds&amp;#34;:{&amp;#34;all&amp;#34;:64},&amp;#34;dt&amp;#34;:1682401802,&amp;#34;sys&amp;#34;:{&amp;#34;country&amp;#34;:&amp;#34;AU&amp;#34;,&amp;#34;sunrise&amp;#34;:1682375848,&amp;#34;sunset&amp;#34;:1682415263},&amp;#34;timezone&amp;#34;:28800,&amp;#34;id&amp;#34;:2070753,&amp;#34;name&amp;#34;:&amp;#34;Gnowangerup&amp;#34;,&amp;#34;cod&amp;#34;:200}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We are assigning to the variable &lt;code&gt;temp_text&lt;/code&gt; the contents of this command, where $weather_text is the JSON string.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;echo $weather_text | awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The vertical lines are called &lt;em&gt;pipes&lt;/em&gt; &lt;code&gt;|&lt;/code&gt; they send the output of the command on their left into the command to their right. So there&amp;rsquo;s three different things happening here. The &lt;code&gt;echo&lt;/code&gt; just outputs the JSON, then we process it twice more.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://www.geeksforgeeks.org/awk-command-unixlinux-examples/"&gt;awk&lt;/a&gt; is one of the great text processing commands along with &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt;. The way it is being used here is to break the string into multiple parts, where the parts are delimited by the text &lt;code&gt;&amp;quot;temp&amp;quot;:&lt;/code&gt; which in our case is just two parts. Then we are outputting the second part ready for the next processing. So at this stage, the text would look like this.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;12.59,&amp;#34;feels_like&amp;#34;:11.68,&amp;#34;temp_min&amp;#34;:12.59,&amp;#34;temp_max&amp;#34;:12.59,&amp;#34;pressure&amp;#34;:1007,&amp;#34;humidity&amp;#34;:68,&amp;#34;sea_level&amp;#34;:1007,&amp;#34;grnd_level&amp;#34;:976},&amp;#34;visibility&amp;#34;:10000,&amp;#34;wind&amp;#34;:{&amp;#34;speed&amp;#34;:7.39,&amp;#34;deg&amp;#34;:307,&amp;#34;gust&amp;#34;:11.23},&amp;#34;clouds&amp;#34;:{&amp;#34;all&amp;#34;:64},&amp;#34;dt&amp;#34;:1682401802,&amp;#34;sys&amp;#34;:{&amp;#34;country&amp;#34;:&amp;#34;AU&amp;#34;,&amp;#34;sunrise&amp;#34;:1682375848,&amp;#34;sunset&amp;#34;:1682415263},&amp;#34;timezone&amp;#34;:28800,&amp;#34;id&amp;#34;:2070753,&amp;#34;name&amp;#34;:&amp;#34;Gnowangerup&amp;#34;,&amp;#34;cod&amp;#34;:200}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then I need to do the same sort of thing again - split the string using a delimiter, and just keep the part with the termperature in it. This time we&amp;rsquo;ll use a comma , as the delimiter, and only keep the part in front of it.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cut -d&amp;#39;,&amp;#39; -f1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;re saying cut this string in to bits were the delimiter &lt;code&gt;-d&lt;/code&gt; is a comma, then output the first field.&lt;/p&gt;
&lt;p&gt;You might be wondering why I didn&amp;rsquo;t just use &lt;code&gt;awk&lt;/code&gt; again - I could have, but &lt;code&gt;cut&lt;/code&gt; is simpler. The reason I didn&amp;rsquo;t use &lt;code&gt;cut&lt;/code&gt; both times is that it can only take a single character as a delimiter. In fact, the first version I wrote of this script only used &lt;code&gt;cut&lt;/code&gt;, and I had the delimiters as colon for the first cut and comma for the second. As I was writing it, I was thinking that I should stress in the blog post about it that it was quite fragile - a small change in the JSON (for example adding a field, or changing the order - both things that would not cause a problem to a good Swift or JS JSON library) would break it. Then the weather changed and so was two layers of clouds, and the script broke and output the time as &lt;code&gt;{&amp;quot;all&amp;quot;&lt;/code&gt; instead of a number.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-3.00.13-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;printf&lt;/code&gt; just outputs the two values - temperature and timestamp as plain text with a comma between them into a text file that&amp;rsquo;s in the root of the Nginx webserver.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-8.06.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-8.06.34-pm.png" width="794" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now that&amp;rsquo;s in place, I just edited &lt;code&gt;/etc/crontab&lt;/code&gt; to have the new script run every five minutes to update the file with the temperature and timestamp.&lt;/p&gt;
&lt;h3 id="server-temp-logging"&gt;Server Temp Logging&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ve already seen most of this, but I&amp;rsquo;ve made a couple of additions.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash

#check drivetemp has been loaded - needed for ssd temp
if ! lsmod | grep -wq drivetemp; then
 modprobe drivetemp
fi

#collect the temp data
pch_name=`cat /sys/class/hwmon/hwmon0/name`
pch_temp=`cat /sys/class/hwmon/hwmon0/temp1_input`
cpu_name=`cat /sys/class/hwmon/hwmon1/name`
cpu_temp=`cat /sys/class/hwmon/hwmon1/temp1_input`
ssd_name=`cat /sys/class/hwmon/hwmon2/name`
ssd_temp=`cat /sys/class/hwmon/hwmon2/temp1_input`

#this should contain the current outside temp and unix time
outside_temp=`curl -s &amp;#34;https://iankulin.com/gnp_temp.txt&amp;#34;`

log_file=&amp;#34;/var/log/temps.csv&amp;#34;

# Print the temperatures to a log file
printf &amp;#34;$(date +&amp;#39;%d/%m/%Y,%T&amp;#39;),%s,%d,%s,%d,%s,%d,out,%s\n&amp;#34; $pch_name $pch_temp $cpu_name $cpu_temp $ssd_name $ssd_temp $outside_temp &amp;gt;&amp;gt; $log_file
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;ve already discussed how the curl works - this one is picking up the script we wrote to run on the VPS earlier. More interesting is checking for the &lt;code&gt;drivetemp&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;The drivetemp module needs to be loaded into the Linux kernel before we can read the SSD temperature with the line.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssd_temp=`cat /sys/class/hwmon/hwmon2/temp1_input`
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once it&amp;rsquo;s loaded, it stays there, unless computer is shutdown for any reason. There&amp;rsquo;s a &lt;a href="https://www.baeldung.com/linux/run-script-on-startup"&gt;number of places&lt;/a&gt; we can execute things on startup, but really this &lt;code&gt;drivetemp&lt;/code&gt; module is only needed for this script, so we should do it here. As far as I can make out, telling Linux to load a module that&amp;rsquo;s already loaded does not do any harm, and at once every five minutes it&amp;rsquo;s hardly going to cause a performance issue. Nevertheless, some sort of programmer ethics compels me to only do it if its needed.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#check drivetemp has been loaded - needed for ssd temp
if ! lsmod | grep -wq drivetemp; then
 modprobe drivetemp
fi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;lsmod&lt;/code&gt; returns a list of the loaded modules, this is passed to the &lt;code&gt;grep&lt;/code&gt;. &lt;code&gt;grep&lt;/code&gt; looks through lines of input and usually returns any lines that match. However in this case, we&amp;rsquo;re using the &lt;code&gt;-q&lt;/code&gt; (quiet) option. With this option on, instead of lines of text, you get nothing on the standard output, instead it sets the exit code to 0 (true) if it&amp;rsquo;s found, or 1 (false) if not.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;m interested in only running the &lt;code&gt;modprobe&lt;/code&gt; if &lt;code&gt;drivetemp&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; found, I have to negate the result of the &lt;code&gt;grep&lt;/code&gt; with &lt;code&gt;!&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After that, all the temperature data is collected, then written out to a log fie for later processing.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-9.00.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="the-results"&gt;The Results&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s 24 hours of the five minute temperature logs. For each server I averaged the three different temperatures (PCH, CPU core, and SSD drive) and graphed them along with the outside temperature from OpenWeather. &lt;code&gt;pve-prod1&lt;/code&gt; is the only one doing any real work here. It hosts my Jellyfin media server on a VM, and another VM with a collection of utilities such as Uptime Kuma. The Y axis is degrees centigrade.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/20230427-server-temps.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_4315.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The spike in &lt;code&gt;pve-dev&lt;/code&gt;1 at 2100 was caused by me stress testing one core to 100% load for ten minutes. I think I can see &lt;code&gt;pve-prod2&lt;/code&gt; (which sits directly on top of &lt;code&gt;pve-dev1&lt;/code&gt;) warming up a little as well. But strangely, and perhaps I&amp;rsquo;m imagining it, it seems like &lt;code&gt;pve-prod1&lt;/code&gt; (which sits on top of the stack) was a bit cooler in that time?&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t remember if I watched some TV between 6 and 8pm, but it looks like I did, and the spike at 2am will be the nightly snapshots being taken and sent off to the NAS.&lt;/p&gt;
&lt;p&gt;You can see that &lt;code&gt;pve-prod2&lt;/code&gt; and &lt;code&gt;pve-dev1&lt;/code&gt; were turned on to run this test, and it takes about 40 minutes for them to warm up. It&amp;rsquo;s interesting to notice the bigger amplitude of the production machine compared to the others just idling. And also interesting that &lt;code&gt;pve-dev1&lt;/code&gt; (which wasn&amp;rsquo;t running any load till I ran the stress test on it) was just generally warmer that &lt;code&gt;pve-prod1&lt;/code&gt; which was running a small work load.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t remember if I watched some TV between 6 and 8pm, but it looks like I did, and the spike at 2am will be the nightly snapshots being taken and sent off to the NAS.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at pve-dev1 while the stress test was running.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/pve-dev1-temp.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It makes sense that the PCH which is mm away from, and directly connected to, the CPU would warm up as the CPU was hammered with square root calculations, and since the drive temp is a up a little so I guess that reflects the ambient temperature inside the case.&lt;/p&gt;
&lt;p&gt;The CPU temperature hadn&amp;rsquo;t plateaued yet, so it might be interesting to run it until it does one day and see what that looks like.&lt;/p&gt;</description></item><item><title>Running a Browser Remotely - n.eko</title><link>https://blog.iankulin.com/running-a-browser-remotely-n-eko/</link><pubDate>Tue, 02 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/running-a-browser-remotely-n-eko/</guid><description>&lt;p&gt;When I installed the backup NAS and a media server at the remote site, one of the jobs on my list was to reserve the IP addresses for the NAS, node, and the VM in the local router. I carefully did that, but when I got home (200 km later) and opened my laptop, the browser page was open on the DHCP settings with a table of mac addresses I&amp;rsquo;d added, and the reserved IP&amp;rsquo;s, and at the bottom of the page, a large blue &amp;ldquo;Apply Changes&amp;rdquo; button. Had I pressed that button to save my changes correctly? I&amp;rsquo;m not sure.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got a couple of options to access the router (without one of those sometimes frustrating tech support calls).&lt;/p&gt;
&lt;p&gt;One is that &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; (that I&amp;rsquo;m using for the VPN tunnel) has a feature called &lt;a href="https://tailscale.com/kb/1019/subnets/"&gt;Subnet Routers&lt;/a&gt;. This is intended for this use-case - there&amp;rsquo;s a device (the remote router) that can&amp;rsquo;t run a Tailnet client that we&amp;rsquo;d like to access, but it&amp;rsquo;s on a local network with a device that can. This would often be the case for things like you want to print to a printer at home from work - you can&amp;rsquo;t Tailnet into the printer, but you can to the home server which is on the same network. Installing the Tailscale subnet router would then allow a path to the printer over the local home network.&lt;/p&gt;
&lt;p&gt;To do that, I need to ssh into the media server node and set it all up, and it will involved a network disconnect. This has a bit of risk involved since I might make an error and then not be able to fix it.&lt;/p&gt;
&lt;p&gt;A neater option might be to run a web browser instance on the media server, and use that to access the web interface. In days gone past that might have been &lt;a href="https://lynx.invisible-island.net/"&gt;Lynx&lt;/a&gt; for a pure text experience, but another, more modern possibility might be &lt;a href="https://www.brow.sh/"&gt;browsh&lt;/a&gt;. This very cool project uses Firefox as it&amp;rsquo;s engine, but then renders all the page output as ASCII text into a terminal. There&amp;rsquo;s a &lt;a href="https://www.youtube.com/watch?v=zqAoBD62gvo"&gt;fun video of browsh&lt;/a&gt; playing Youtube videos all in half size ascii colour blocks in the terminal.&lt;/p&gt;
&lt;p&gt;I ended up going an even more modern way by using the virtual browser &lt;a href="https://neko.m1k1o.net/"&gt;N.eko&lt;/a&gt;. This way you get a full graphical browser running on a remote machine. Licenced under the Apache license, it&amp;rsquo;s a project of &lt;a href="https://github.com/m1k1o/neko"&gt;Miroslav Šedivý&lt;/a&gt;. I&amp;rsquo;m sure I&amp;rsquo;ve read in a forum somewhere that he built it to watch anime online with friends - and there&amp;rsquo;s some functionality (such as chats) that supports that.&lt;/p&gt;
&lt;p&gt;Docker is the easy way to spin it up. It is a little bit resource hungry - 1GB is specified for the lowest resolution, but &lt;a href="//www.youtube.com/watch?v=ISunHDh7WyQ"&gt;this guy&lt;/a&gt; shows adding a swap file to make the memory up to that. My VPS has 512KB, but NVMe storage, so I tried without the swap file and could not get it to start, and then with the swap file and it worked fine. Here&amp;rsquo;s my compose:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3.5&amp;#39;services: neko: image: m1k1o/neko:chromium restart: always cap_add: - SYS_ADMIN ports: - &amp;#34;8081:8080&amp;#34; - &amp;#34;59000-59100:59000-59100/udp&amp;#34; environment: DISPLAY: :99.0 SCREEN_WIDTH: 1024 SCREEN_HEIGHT: 576 SCREEN_DEPTH: 16 NEKO_PASSWORD: neko NEKO_ADMIN: admin NEKO_BIND: :8080 NEKO_NAT1TO1: 100.138.120.102 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The only thing in there that took a bit of sorting out was the last line - the NEKO_ATA1TO1 environment variable. I didn&amp;rsquo;t need that at all running on a remote VPS - it just all worked out of the box, but when I was trying on my development node at home (which is a close hardware/software mirror of the remote setup I need it for) I had less luck.&lt;/p&gt;
&lt;p&gt;In that configuration, when I went to log in, it would take ages, then say &lt;em&gt;peer disconnected&lt;/em&gt;. In the logs, there was an error &lt;code&gt;WRN read message error error=&amp;quot;websocket: close 1005 (no status)&amp;quot;&lt;/code&gt;. Some googling took me to a GitHub issue about it. When n.eko starts, it autodetects the external IP and expects connections from there. This will also be an issue for my remote site, since it&amp;rsquo;s on a Tailnet that won&amp;rsquo;t match it&amp;rsquo;s external IP. The solution is to add the &lt;code&gt;NEKO_NAT1TO1&lt;/code&gt; environment variable in your docker-compose.yaml and set it to a single IP address with no quotes - being the IP address you&amp;rsquo;ll connect to it on.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-23-at-5.08.42-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Edits:&lt;/p&gt;
&lt;p&gt;4/3/24 - changed the image name to &lt;code&gt;m1k1o/neko:chromium&lt;/code&gt; from &lt;code&gt;nurdism/neko:chromium&lt;/code&gt; which was an old old version.&lt;/p&gt;</description></item><item><title>ISO wrangling - Etcher and Ventoy</title><link>https://blog.iankulin.com/iso-wrangling-etcher-and-ventoy/</link><pubDate>Mon, 01 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/iso-wrangling-etcher-and-ventoy/</guid><description>&lt;p&gt;If you fiddle around with computers, and especially with Linux drives, you&amp;rsquo;ll often find yourself with an ISO file you need to boot a device from. These can&amp;rsquo;t just be copied onto an existing USB or SD card - they need to be bootable, so you&amp;rsquo;ll need a special program to write the ISO to the storage device.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-23-at-2.02.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-23-at-2.02.44-pm.png" width="247" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Previously I&amp;rsquo;ve been a big fan of &lt;a href="https://www.balena.io/etcher"&gt;Balena Etcher&lt;/a&gt;. It couldn&amp;rsquo;t be much more simple - you chose the ISO file you&amp;rsquo;ve downloaded from somewhere, chose your removable drive (it intelligently hides the non-removable drives to prevent you from accidentally wiping your hard disk), then tell it to do it&amp;rsquo;s thing.&lt;/p&gt;
&lt;p&gt;When you want to try a different ISO file, you go through that whole process again. At least that&amp;rsquo;s what I did till I heard about &lt;a href="https://www.ventoy.net/en/index.html"&gt;Ventoy&lt;/a&gt;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/ventoy.png" width="241" alt=""&gt;
&lt;p&gt;This installs onto the USB in a similar way - although with it&amp;rsquo;s own program Ventoy2Disk (no macOS version). Once it&amp;rsquo;s on there, the USB drive just appears as an ordinary empty ExFat drive if you plug it in.&lt;/p&gt;
&lt;p&gt;You just copy any (multiples allowed) ISO&amp;rsquo;s you might use in the future onto the drive. The if the USB is used to boot from, it starts a GRUB like program that lets you choose one of the ISO&amp;rsquo;s, then will go on to boot from that ISO. It&amp;rsquo;s saved me a lot of time by not having to re-etch ISO files - they are often quite large so is was a time consuming process.&lt;/p&gt;</description></item><item><title>Git/GutHub - macOS - marking file as executable</title><link>https://blog.iankulin.com/git-guthub-macos-marking-file-as-executable/</link><pubDate>Sun, 30 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-guthub-macos-marking-file-as-executable/</guid><description>&lt;p&gt;I&amp;rsquo;m working on the world&amp;rsquo;s shortest shell script - it&amp;rsquo;s called by &lt;code&gt;cron&lt;/code&gt; to pull down a JSON weather report to a text file using &lt;code&gt;curl&lt;/code&gt; so I can expose it on an Nginx endpoint. The purpose is to allow me to hammer that weather API from multiple machines I control without violating the TOS of my free API key.&lt;/p&gt;
&lt;p&gt;Because I&amp;rsquo;m learning all the things, instead of just creating this on the VPS where it runs, it&amp;rsquo;s cloned from my GitHub repo for that machine. I&amp;rsquo;m creating and editing the file in VS Code on macOS, pushing to Github, then pulling the changes on the Ubuntu VPS. The intention is that this will eventually become automated with a Github action.&lt;/p&gt;
&lt;p&gt;The problem I&amp;rsquo;ve run into is that I want the file permissions so show the file is executable so when it arrives on the VPS - so no &lt;code&gt;chmod&lt;/code&gt; is required to make it usable.&lt;/p&gt;
&lt;p&gt;Some googling suggested that the executable flag (but none of the other file permissions) is stored and handled by git, and furthermore, there&amp;rsquo;s a git command to set it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git update-index --chmod=+x bin/fetchWeather.sh 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So I wrote my (one line) script, applied the command above, committed and pushed, then pulled it down on the VPS and the bit wasn&amp;rsquo;t set. So somewhere in this chain there&amp;rsquo;s a problem.&lt;/p&gt;
&lt;p&gt;At this stage, it&amp;rsquo;s helpful to know that if the executable bit is set for a file, GitHub shows this in the header of the file where it says how many lines etc.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.26.25-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.26.41-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In my case, it was showing that the file was not marked as executable in GitHub, so the problem was that the &lt;code&gt;git update-index&lt;/code&gt; was not working for me for some reason.&lt;/p&gt;
&lt;p&gt;A bit more investigation turned up that there&amp;rsquo;s a setting in the &lt;code&gt;.git/config&lt;/code&gt; file called &lt;code&gt;filemode&lt;/code&gt; that controls if the originating file system executable status is preserved. That sounded promising - I was expecting to find that is was set to false, and I could change it to true, and it would fix my problem. I had a quick look and, oh, it&amp;rsquo;s already set to true.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.36.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.36.54-pm.png" width="656" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Seems like it&amp;rsquo;s involved though, so perhaps (my thinking went) I should change it to false and see if the problem goes away&amp;hellip;. and it did. I changed this value to &lt;code&gt;false&lt;/code&gt;, applied the executable bit with the &lt;code&gt;git update-index&lt;/code&gt; command, committed, pushed it to GitHub (it was marked executable), pulled it down to the VPS, it was still marked executable!&lt;/p&gt;
&lt;p&gt;My whole tech life, I&amp;rsquo;ve never been happy with solutions to problems where I don&amp;rsquo;t understand the underlying reasons. If things just start working when you&amp;rsquo;re fiddling around and you&amp;rsquo;re not clear on why, it feels like they could change back with just as easily and with no more reason.&lt;/p&gt;
&lt;p&gt;A clue to what&amp;rsquo;s going on (many readers will already have figured this out) was given to me by ChatGPT. When I was asking it about this issue, it kept insisting I should &lt;code&gt;chmod&lt;/code&gt; the file to be executable before I committed it. I had to be really clear with it that this wasn&amp;rsquo;t possible on macOS because it doesn&amp;rsquo;t have that sort of file permissions&amp;hellip;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/cain.jpg" width="140" alt=""&gt;
&lt;p&gt;Of course, in fact, it does. &lt;a href="https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/BSD/BSD.html"&gt;macOS is based on FreeBSD&lt;/a&gt; (&amp;ldquo;without the good bits&amp;rdquo; goes the old joke told at Unix conferences). I&amp;rsquo;d just somehow forgotten this - I guess in Linux I&amp;rsquo;m used to explicitly seeing them every time I look at a directory contents, but never see it on Mac. Even if you go into &amp;ldquo;Get Info&amp;rdquo; for a file in Finder on the mac, you can see the read/write permissions, but not the executable bit status.&lt;/p&gt;
&lt;p&gt;So how do you set and view the executable status on mac? Exactly the same as on any Unix.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.52.17-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.52.17-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I did that, and changed the /&lt;code&gt;git/config filemode&lt;/code&gt; back to &lt;code&gt;true&lt;/code&gt;. Committed and pushed the file up (without worrying about the &lt;code&gt;git update-index&lt;/code&gt;) and it showed up in GitHub as executable, pulled it down, still executable.&lt;/p&gt;</description></item><item><title>Installing SSL Certificates with Nginx on Docker</title><link>https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/</link><pubDate>Sat, 29 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/</guid><description>&lt;p&gt;When you&amp;rsquo;ve successfully got Nginx running in a Docker container, AND got your &lt;a href="https://blog.iankulin.com/adding-a-domain-name-to-a-vps/"&gt;domain correctly pointing&lt;/a&gt; at your nascent website, you&amp;rsquo;re then going to want to set it up for encrypted, and therefore trusted, browsing with SSL.&lt;/p&gt;
&lt;h3 id="certificates"&gt;Certificates&lt;/h3&gt;
&lt;p&gt;A couple of posts ago, I &lt;a href="https://blog.iankulin.com/adding-a-domain-name-to-a-vps/"&gt;mentioned&lt;/a&gt; that it was simpler to let Porkbun be the authoritative nameserver for a domain. Part of the reason for that is that if we do that, Porkbun had a button you can press which connects to LetsEncrypt and generates the certificates for you. This usually takes an hour or so, then you&amp;rsquo;ll be able to download the bundle from that same page.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-2.30.58-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-2.30.58-pm.png" width="913" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In order for the SSL to work, we&amp;rsquo;re going to have to make a couple of files available to Nginx - &lt;code&gt;fullchain.pem&lt;/code&gt; and &lt;code&gt;private.key.pem&lt;/code&gt;. So there&amp;rsquo;s our first gotcha - we don&amp;rsquo;t have a &lt;code&gt;fullchain.pem&lt;/code&gt;, so we have to build it. To do this, we just combine the domain certificate and the intermediate certificate. On the mac, I did this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat domain.cert.pem intermediate.cert.pem &amp;gt; fullchain.pem
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is me solving the first gotcha, while simultaneously creating the second. Much later in the process when Nginx was failing at startup, I looked in the logs (with the handy &lt;code&gt;docker logs&lt;/code&gt; command) and saw these messages:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;2023/04/21 05:42:45 [emerg] 1#1: cannot load certificate &amp;#34;/etc/nginx/conf.d/fullchain.pem&amp;#34;: PEM_read_bio_X509() failed (SSL: error:0908F066:PEM routines:get_header_and_data:bad end line)
nginx: [emerg] cannot load certificate &amp;#34;/etc/nginx/conf.d/fullchain.pem&amp;#34;: PEM_read_bio_X509() failed (SSL: error:0908F066:PEM routines:get_header_and_data:bad end line)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s a reasonably descriptive error - let&amp;rsquo;s look in the &lt;code&gt;fullchain.pem&lt;/code&gt; file (it&amp;rsquo;s just text like an SSH key file) and see if there&amp;rsquo;s anything suspicious.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-1.46.32-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well there&amp;rsquo;s a problem. These beginning and ends should be on their own lines - I probably could have done that when I concatenated them, but no problem, it&amp;rsquo;s easily fixed in the text editor by counting in five dashes and hitting enter.&lt;/p&gt;
&lt;h3 id="nginx-docker"&gt;Nginx Docker&lt;/h3&gt;
&lt;p&gt;In order to have the certificates work with Nginx, we&amp;rsquo;re going to need to add them to the a config file. There&amp;rsquo;s also a couple of gotcha&amp;rsquo;s in that process.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re as new to running Nginx in a container as I am, you might have been starting it up with a command like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run -p 80:80 -d -v ~/www:/usr/share/nginx/html nginx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s fine and all, but as your system gets a bit more complex (which it&amp;rsquo;s about to) this will quickly become unmanageable. It&amp;rsquo;s time to put your big person pants on and embrace the wonders of &lt;code&gt;docker compose&lt;/code&gt;. There are many resources for learning this, but the short version is that all of that information you&amp;rsquo;ve got in your command line can be stored in a human readable YAML file. If you&amp;rsquo;re smart it will also be in version control and you&amp;rsquo;re on your journey to automating your infrastructure as code.&lt;/p&gt;
&lt;p&gt;Below is my &lt;code&gt;docker-compose.yaml&lt;/code&gt; file for Nginx. Note that these files are always called that, so you keep the compose files for different containers in separate directories.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#34;3.9&amp;#34;

services:
 client:
 image: nginx
 container_name: nginx
 ports:
 - 80:80
 - 443:443
 volumes:
 - /home/ian/iankulin.com/www:/usr/share/nginx/html
 - /home/ian/iankulin.com/nginx/conf/:/etc/nginx/conf.d/:ro
 restart: always
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;version&lt;/code&gt; - just the compose yaml version docker should use to read this file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/client&lt;/code&gt; - it&amp;rsquo;s possible to combine several programs (clients) in a docker container. We&amp;rsquo;re not doing that today&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt; - the name of the docker image we&amp;rsquo;re pulling down from &lt;a href="https://hub.docker.com/"&gt;docker hub&lt;/a&gt;. We could also add the version here if we were picky, if one&amp;rsquo;s not specified, it assumes &amp;rsquo;latest'&lt;/li&gt;
&lt;li&gt;&lt;code&gt;container_name&lt;/code&gt; - nice name for our container - it&amp;rsquo;s possible to run several versions of the same image so you may want to name them something different. If you miss this off, docker will make up a default name like &lt;code&gt;agressive_einstein&lt;/code&gt; and you&amp;rsquo;ll constantly be running &lt;code&gt;docker ps&lt;/code&gt; because you can&amp;rsquo;t remember the name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ports&lt;/code&gt; - the underlying idea of containers is that they are mostly immutable inside, but of course to be useful they need to have some access to the outside world. This ports declaration is doing just that. These two lines are saying port 80 outside the container is connected to port 80 inside the container. If we wanted Nginx running on port 8080 we&amp;rsquo;d say &lt;code&gt;8080:80&lt;/code&gt; ie the outside first, and the inside second&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volumes&lt;/code&gt; - similar to ports, we&amp;rsquo;re joining a directory of our file system in outside world to a directory inside the container. In the first one, Nginx is going to look for files to serve inside the container at &lt;code&gt;/usr/share/nginx/html&lt;/code&gt; but where we want it to look for html files to serve is actually out here in the real filesystem world. Same as with the ports, the format is real world first, inside the container second. Same with the config directory. You might be wondering how I know what the paths inside the container are for these things - you just have to figure it out from the &lt;a href="https://hub.docker.com/_/nginx"&gt;documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;So we&amp;rsquo;ve got two outside world locations - our html files in &lt;code&gt;/home/ian/iankulin.com/www&lt;/code&gt; and the Nginx config files in &lt;code&gt;/home/ian/iankulin.com/nginx/conf/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the config file. This is stored in &lt;code&gt;/home/ian/iankulin.com/nginx/conf/&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
 listen 80 default_server;
 listen [::]:80 default_server;
 root /usr/share/nginx/html;
 server_name iankulin.com www.iankulin.com;

 listen 443 ssl; 

 # RSA certificate
 ssl_certificate /etc/nginx/conf.d/fullchain.pem; 
 ssl_certificate_key /etc/nginx/conf.d/private.key.pem; 
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is mostly pretty decodable just by looking at it, but there&amp;rsquo;s a couple of things worth noting.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the root for the html, like all of the paths in this file, are the paths &lt;em&gt;inside&lt;/em&gt; the container. Don&amp;rsquo;t get confused. To the programs running inside the container, everything looks like it&amp;rsquo;s inside the container. This config file is being consumed by the Nginx program inside the container, so the paths have to be inside-the-container paths.&lt;/li&gt;
&lt;li&gt;Following that logic, I&amp;rsquo;ve actually stored the SSL certificates at &lt;code&gt;/home/ian/iankulin.com/nginx/conf/&lt;/code&gt; but to Nginx inside the container, they look like they&amp;rsquo;re at &lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;There is &lt;em&gt;way&lt;/em&gt; more stuff you can do in this config file. This is just the simplest version possible to make things work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So now that&amp;rsquo;s in place, and I&amp;rsquo;ve got a skeleton of an index.html file stored at &lt;code&gt;/home/ian/iankulin.com/www&lt;/code&gt; I just enter &lt;code&gt;sudo docker compose up -d&lt;/code&gt; in the directory where my &lt;code&gt;docker-compose.yaml&lt;/code&gt; file is, and I should be able to navigate to &lt;code&gt;http**s**://iankulin.com&lt;/code&gt; and get a webpage with a padlock in the corner.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-1.48.00-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-1.48.00-pm.png" width="859" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="success"&gt;Success&lt;/h3&gt;
&lt;p&gt;Well of sorts. We have obtained our certificates, and installed them in the webserver, but certificates like these only last 90 days. In 75 days I can obtain new certificates and copy them over the old ones. If we fail to do that by the 90th day, visitors to the website will get a scary message saying the website might not be who it says it is, and users will have to click around a bit to ignore it. You will have almost certainly seen this message as it&amp;rsquo;s a reasonably common problem.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a problem calling out for an automated solution, of which there &lt;a href="https://certbot.eff.org/"&gt;is one&lt;/a&gt; that we&amp;rsquo;ll install on another day. Probably the day I come back to this server and discover the certificates have expired&amp;hellip;&lt;/p&gt;</description></item><item><title>Adding a Domain Name to a VPS</title><link>https://blog.iankulin.com/adding-a-domain-name-to-a-vps/</link><pubDate>Fri, 28 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/adding-a-domain-name-to-a-vps/</guid><description>&lt;p&gt;I&amp;rsquo;ve had a small &lt;a href="https://www.binarylane.com.au/"&gt;BinaryLane VPS&lt;/a&gt; for a while that I use for homelab type stuff, but now need to serve a tiny amount of JSON from it. A longer term plan is to use it as a &lt;a href="https://www.wireguard.com/"&gt;Wireguard&lt;/a&gt; tunnel back to my cluster at home to expose the services that need to be internet facing. I&amp;rsquo;ve also had a domain name I bought from &lt;a href="https://porkbun.com/products/domains"&gt;Porkbun&lt;/a&gt; sitting round for a bit, so it&amp;rsquo;s probably a good time to join them up.&lt;/p&gt;
&lt;p&gt;When you type a domain name into your web browser it needs to be turned into an IP address in order to return the content you need from that web server. For example if I type in &lt;code&gt;google.com&lt;/code&gt; it needs to be turned into &lt;code&gt;172.217.24.46&lt;/code&gt; in order to fetch the front page of Google.&lt;/p&gt;
&lt;p&gt;The things that provide this translation service are the Domain Name Service (DNS). There&amp;rsquo;s several layers of DNS and if the first layer asked does not know, the request gets escalated until it is found or the request fails - but somewhere in that chain there needs to be a name server (the Authoritative DNS) that knows the domain name and the IP address. DNS is cached all over the place, so most requests don&amp;rsquo;t get all the way back there (and changes sometimes take a little while to percolate around) but there must an an authoritative name server somewhere.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to have my domain pointed to the BinaryLane name servers, but it&amp;rsquo;s currently pointed to the Porkbun name servers, and it&amp;rsquo;s simpler for me to leave it there.&lt;/p&gt;
&lt;p&gt;All I need to do at the Porkbun end is go into Domain Management, and open up the &amp;ldquo;DNS entries&amp;rdquo; for the domain, and edit the &amp;ldquo;A Records&amp;rdquo; to point at the IP address.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-9.24.12-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s enough that a few minutes later, typing the domain address into a web browser pulls up the test page from the Nginx web server running in a container on my VPS.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-9.35.48-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-9.35.48-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Linux Shell Script for Temperature Logging</title><link>https://blog.iankulin.com/linux-shell-script-for-temperature-logging/</link><pubDate>Thu, 27 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/linux-shell-script-for-temperature-logging/</guid><description>&lt;p&gt;A potential solution to my concern about the either perfect, or nearly dead, SSD would be to add a NVMe disk to the M.2 slot in the HP Elitedesk 800 G2&amp;rsquo;s. I&amp;rsquo;d use those to boot from and run Proxmox, then the existing SSD&amp;rsquo;s on each node in the cluster would just be part of the CephFS pool that has some redundancy built into it and hosts the VMs that are not using the NAS for their storage.&lt;/p&gt;
&lt;p&gt;These &amp;lsquo;gumstick&amp;rsquo; NVMe drives are remarkably good value in the smaller sizes at the moment, with Samsung 250GB NVMe&amp;rsquo;s costing less than a pack of cigarettes in Australia.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-20-at-7.02.57-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A small concern I&amp;rsquo;ve got about that, and about the (very cute looking) way I&amp;rsquo;ve just got the computers all stacked on top of each other, is about the internal temperatures. I noticed SSD temperatures in the SMART data I was looking the other day, and I&amp;rsquo;ve seen CPU temperatures somewhere, so this data is available. So I set out on a quest to log some of it so I could do a before and after (NMVe installation) look at the temperatures.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://www.pyroelectro.com/tutorials/cron-automation/check.html"&gt;This article&lt;/a&gt; was very close to what I wanted - a shell script to run in a cron job that would log the drive and CPU temperatures. The script goes a fair way beyond that, but my main issue was that it uses a couple of packages - &lt;code&gt;sensors&lt;/code&gt;, and &lt;code&gt;hddtemp&lt;/code&gt;. I like to avoid dependencies if I can, but I also thought the temp data was pretty simple and is probably just sitting in the &lt;code&gt;/sys/&lt;/code&gt; directory tree somewhere.&lt;/p&gt;
&lt;p&gt;That sort of turned out to be true. In /sys/class/hwmon/ there&amp;rsquo;s a couple of directories (actually symlinks) for bits of hardware that can be monitored for temperature, and in those directories are text files with some values, the ones we&amp;rsquo;re interested in being &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;temp1_input&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve-dev1:~# tree /sys/class/hwmon/
/sys/class/hwmon/
├── hwmon0 -&amp;gt; ../../devices/virtual/thermal/thermal_zone0/hwmon0
├── hwmon1 -&amp;gt; ../../devices/platform/coretemp.0/hwmon/hwmon1
└── hwmon2 -&amp;gt; ../../devices/pci0000:00/0000:00:17.0/ata1/host0/target0:0:0/0:0:0:0/hwmon/hwmon2

3 directories, 0 files
root@pve-dev1:~# ls /sys/class/hwmon/hwmon0
device name power subsystem temp1_input uevent
root@pve-dev1:~# cat /sys/class/hwmon/hwmon0/name /sys/class/hwmon/hwmon0/temp1_input
pch_skylake
45500
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://commons.wikimedia.org/w/index.php?curid=9817206"&gt;&lt;img src="https://blog.iankulin.com/images/intel_5_series_architecture.png" width="232" alt=""&gt;&lt;/a&gt;
&lt;em&gt;Intel 5 architecture - Anas hashmi&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The first two are temperatures of the &lt;a href="https://commons.wikimedia.org/w/index.php?curid=9817206"&gt;Platform Controller Hub (PCH)&lt;/a&gt; and actual CPU. Both of these values were already just sitting there.&lt;/p&gt;
&lt;p&gt;The third value, for the SSD temperature didn&amp;rsquo;t appear until added by running &lt;code&gt;modprobe drivetemp&lt;/code&gt; to load a kernel module.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the three values I want to log sorted then, but how to go about it? &lt;a href="http://www.pyroelectro.com/tutorials/cron-automation/check.html"&gt;That first article&lt;/a&gt; I mentioned had a shell script using printf() to output some values to a log file, then the script was triggered by a cron job. Two things I&amp;rsquo;ve never done before, so let&amp;rsquo;s dive in. Here&amp;rsquo;s the finished code.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-20-at-8.32.11-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I wrote MS-DOS batch files in the 1980s so this wasn&amp;rsquo;t completely alien to me. A few points were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everyone else&amp;rsquo;s shell scripts start with &lt;code&gt;#!/bin/bash&lt;/code&gt; so I assume that&amp;rsquo;s compulsory. Other lines starting with &lt;code&gt;#&lt;/code&gt; are comments.&lt;/li&gt;
&lt;li&gt;In that list of variables, each one is filled with the results of the command being assigned to it in the single quotes. So if typing in &lt;code&gt;cat /sys/class/hwmon/hwmon0/name&lt;/code&gt; at the terminal prompt would result in the output &lt;code&gt;pch_skylake&lt;/code&gt;, then the variable &lt;code&gt;pch_name&lt;/code&gt; will contain &lt;code&gt;pch_skylake&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you&amp;rsquo;re not coming to this from a programming background, this printf() command is going to look weird.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;printf &amp;#34;$(date +&amp;#39;%d/%m/%Y,%T&amp;#39;),%s,%d,%s,%d,%s,%d\n&amp;#34; $pch_name $pch_temp $cpu_name $cpu_temp $ssd_name $ssd_temp &amp;gt;&amp;gt; $log_file
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;How these work is that the first part is the string to print, but it has some placeholders (all those &lt;code&gt;%&lt;/code&gt;letters). At runtime, the values from the end of the line are inserted into them. Like this:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve-dev1:~# printf &amp;#34;Hello %s\n&amp;#34; &amp;#34;Ian&amp;#34;
Hello Ian
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;%s&lt;/code&gt; is a placeholder for a some text, then we supply the text at the end - &lt;code&gt;&amp;quot;Ian&amp;quot;&lt;/code&gt;. In this case, &lt;code&gt;&amp;quot;Ian&amp;quot;&lt;/code&gt; is a string literal, if we&amp;rsquo;d used a variable (as in our logging script) then the contents of the variable would be used instead. The &lt;code&gt;\n&lt;/code&gt; at the end of the string is a newline character so whatever comes after starts on a new line.&lt;/p&gt;
&lt;p&gt;At this point I know enough about Linux permissions that I knew I&amp;rsquo;d have to set the shell script file to be executable with a &lt;code&gt;chmod 755&lt;/code&gt;, and to call it with the &lt;code&gt;./&lt;/code&gt; in front of it that I was perplexed about a couple of days ago.&lt;/p&gt;
&lt;p&gt;Again, the original article gave an example of the line to put into /etc/crontab. It just needed the path to my script and it was good to go.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) 
# | | | | |
# * * * * * user-name command to be executed
*/5 * * * * root /root/bin/tempCheck.sh 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next thing you know, the log file is slowly growing at &lt;code&gt;/var/log/temps.csv&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;20/04/2023,20:30:01,pch_skylake,45000,coretemp,38000,drivetemp,38000
20/04/2023,20:35:01,pch_skylake,45000,coretemp,37000,drivetemp,38000
20/04/2023,20:40:01,pch_skylake,44500,coretemp,37000,drivetemp,38000
20/04/2023,20:45:01,pch_skylake,45000,coretemp,37000,drivetemp,38000
20/04/2023,20:50:01,pch_skylake,44500,coretemp,37000,drivetemp,38000
20/04/2023,20:55:01,pch_skylake,44500,coretemp,37000,drivetemp,38000
20/04/2023,21:00:01,pch_skylake,45000,coretemp,37000,drivetemp,38000
20/04/2023,21:05:01,pch_skylake,45500,coretemp,38000,drivetemp,38000
20/04/2023,21:10:01,pch_skylake,45000,coretemp,38000,drivetemp,38000
20/04/2023,21:15:01,pch_skylake,45500,coretemp,37000,drivetemp,38000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Obviously I&amp;rsquo;m going to graph this, and also obviously, I&amp;rsquo;m going to run a CPU stress test in a VM in the middle of the data collection.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/temp-chart.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>SDD Wearout numbers</title><link>https://blog.iankulin.com/sdd-wearout-numbers/</link><pubDate>Tue, 25 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sdd-wearout-numbers/</guid><description>&lt;p&gt;I didn&amp;rsquo;t understand why the default Proxmox install sets up the storage the way it does - with the available disk split up into an LVM and an LVM thin storage - so I&amp;rsquo;ve been reading this excellent &lt;a href="https://blog.programster.org/proxmox-storage-guide"&gt;Proxmox Storage Guide&lt;/a&gt; by Programster (spoiler - the LVM thin makes VM snapshots easier).&lt;/p&gt;
&lt;p&gt;At one point in the post they mention that you can see the &amp;ldquo;Wearout&amp;rdquo; percentage for any SSD drives in the Proxmox GUI, so of course, since I now own five second hand HP Elitedesk 800 G1/G2&amp;rsquo;s all with SSD drives, I dived in to have a look at each drive and found this.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Server&lt;/th&gt;
 &lt;th&gt;GB&lt;/th&gt;
 &lt;th&gt;Model&lt;/th&gt;
 &lt;th&gt;SMART&lt;/th&gt;
 &lt;th&gt;Wearout&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;pve-prod1&lt;/td&gt;
 &lt;td&gt;512&lt;/td&gt;
 &lt;td&gt;Micron_1100 SATA&lt;/td&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;6%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;pve-prod2&lt;/td&gt;
 &lt;td&gt;120&lt;/td&gt;
 &lt;td&gt;SSD2S120SF1200SA2&lt;/td&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;100%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;pve-dev1&lt;/td&gt;
 &lt;td&gt;256&lt;/td&gt;
 &lt;td&gt;TOSHIBA_THNSNK256GCS8&lt;/td&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;2%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;pve-kr01&lt;/td&gt;
 &lt;td&gt;120&lt;/td&gt;
 &lt;td&gt;KINGSTON_SA400S37120G&lt;/td&gt;
 &lt;td&gt;Pass&lt;/td&gt;
 &lt;td&gt;0%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I&amp;rsquo;m no expert, but 100% &amp;ldquo;wearout&amp;rdquo; sounds bad, or maybe these figures go the other way, and that drive is 100% good and the others are just about dead. Either way, I&amp;rsquo;m suddenly interested in this number and what it means.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a button to look at the S.M.A.R.T (Self-Monitoring, Analysis and Reporting Technology backronym) attributes, so let&amp;rsquo;s have a look at this suspicious no-name drive.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-7.27.43-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well, some of this is comprehensible. The Power_On_Hours is saying it&amp;rsquo;s been on for about one and a half years worth of hours. Since it&amp;rsquo;s been power cycled over a thousand times, that all sort of matches a corporate desk machine that&amp;rsquo;s been in use for five or six years. These values look like the sort of data you get from running the &lt;code&gt;smartctl -a /dev/sda&lt;/code&gt; command. I&amp;rsquo;ve snipped this output because it is huge, but the middle part is very similar to the table above, and there was nothing scary it it,&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;...

SMART overall-health self-assessment test result: PASSED

...
 

ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE
 1 Raw_Read_Error_Rate 0x0032 120 120 050 Old_age Always - 0
 5 Reallocated_Sector_Ct 0x0033 100 100 003 Pre-fail Always - 0
 9 Power_On_Hours 0x0032 060 060 000 Old_age Always - 35173 (2 96 0)
 12 Power_Cycle_Count 0x0032 099 099 000 Old_age Always - 1059
171 Unknown_Attribute 0x000a 100 100 000 Old_age Always - 0
172 Unknown_Attribute 0x0032 100 100 000 Old_age Always - 0

...

No self-tests have been logged. [To run self-tests, use: smartctl -t]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s a lot, but it clearly says that it&amp;rsquo;s &amp;ldquo;passed&amp;rdquo; the test.&lt;/p&gt;
&lt;p&gt;I tried to run the short SMART test a couple of times with the command: &lt;code&gt;smartctl --test=short /dev/sda&lt;/code&gt; but each time (after I&amp;rsquo;d waited a couple of minutes) when I ran &lt;code&gt;smartctl -l selftest /dev/sda&lt;/code&gt; to look at the results, it claimed the test had been aborted by the host. Presumably I need to shut down Proxmox to run the test properly.&lt;/p&gt;
&lt;p&gt;For the moment, I&amp;rsquo;m just hoping that different manufacturers report that wearout figure differently, but I&amp;rsquo;ll show an increased interest in these drives health for a while.&lt;/p&gt;
&lt;p&gt;The reason I have three nodes locally is that I&amp;rsquo;m anticipating going to HA (high availability) as I move more services out of the paid cloud onto self-hosted. When I do that some of the VM&amp;rsquo;s (with low disk speed needs) will have their storage on the NAS, and the others in a Ceph or ZFS pool to facilitate quick migration on failure. To support that, I&amp;rsquo;m probably looking at provisioning new high quality 512GB SSDs to these machines anyway, so if I do get to that stage, that&amp;rsquo;s a strong (although expensive) possibility, and I&amp;rsquo;d certainly rather buy two than three.&lt;/p&gt;</description></item><item><title>Why use './' in front of filenames?</title><link>https://blog.iankulin.com/why-use-in-front-of-filenames/</link><pubDate>Sun, 23 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/why-use-in-front-of-filenames/</guid><description>&lt;p&gt;In Linux (and MS-DOS I guess) the period signifies the current directory, so if I have a file in the current directory called &lt;code&gt;test.txt&lt;/code&gt;, I can refer to it as &lt;code&gt;test.txt&lt;/code&gt; or &lt;code&gt;./test.txt&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@enrico-rider:~$ cat test.txt
test
ian@enrico-rider:~$ cat ./test.txt
test
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I mostly see this in references to files in HTML and have often wondered why. Here it is being used in a Udemy course I&amp;rsquo;m following.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-10.49.00-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s one of those things that&amp;rsquo;s difficult to Google, so these days my reflex is to ask ChatGPT such questions.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-11.17.53-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-11.17.53-am.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Okay. That makes sense for executable files. If you just type in the name, Linux will look in the current directory, then if not found, in each of the directories in your $PATH variable. But if you add the ./ to the front, it will only look in your current directory. This claim of ChatGPT&amp;rsquo;s is easily tested, lets try with &lt;code&gt;cat&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@enrico-rider:~$ cat test.txt
test
ian@enrico-rider:~$ ./cat test.txt
-bash: ./cat: No such file or directory
ian@enrico-rider:~$ cp /usr/bin/cat cat
ian@enrico-rider:~$ ./cat test.txt
test
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So that checks out, but it doesn&amp;rsquo;t explain the main place I see it - in HTML.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.00.12-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Again, that makes sense. But it still doesn&amp;rsquo;t answer why the instructor in my course is using it for:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cp /etc/passwd ./users.txt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.06.52-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Lol - I feel this is a real edge case. I can see it being more a problem with the first file rather than the second one. eg.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.21.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.21.05-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, TL:DR; using &amp;lsquo;./&amp;rsquo; in front of a filename can be useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;executing a shell script in the current directory (to avoid ambiguity with other executable files in your PATH);&lt;/li&gt;
&lt;li&gt;when running a command that takes filenames as arguments, and the filename might be confused with an argument;&lt;/li&gt;
&lt;li&gt;in HTML to avoid confusion between the directory the current file is in and the root directory of the web server.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Mounting NFS shares into LXC containers</title><link>https://blog.iankulin.com/mounting-nfs-shares-into-lxc-containers/</link><pubDate>Fri, 21 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/mounting-nfs-shares-into-lxc-containers/</guid><description>&lt;p&gt;I&amp;rsquo;m playing with &lt;a href="https://syncthing.net/"&gt;Syncthing&lt;/a&gt; with the idea that it might be a good replacement for Dropbox. There wasn&amp;rsquo;t a Docker container listed in the install options, so I thought this might be a good app to run in an LXC.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m going to use a share from the NAS, and I&amp;rsquo;m assuming I&amp;rsquo;ll need it mount it into the container for Syncthing to access. I&amp;rsquo;m experienced enough to know that I&amp;rsquo;m going to want a privileged container, and I thought I&amp;rsquo;d done all the NFS sharing correctly, but when I tried to mount the NFS share, I was getting an error.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@ct356-syncthing:~# showmount -e 192.168.100.32

Export list for 192.168.100.32:
/volume1/syncthing 192.168.100.37
/volume1/proxmox 192.168.100.24,192.168.100.31,192.168.100.28,192.168.100.23

root@ct356-syncthing:~# mount -t nfs 192.168.100.32:/volume1/syncthing /mnt/syncthing

mount.nfs: access denied by server while mounting 192.168.100.32:/volume1/syncthing
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is just part of the security nature of the LXC containter getting in our way. We can edit the &lt;code&gt;.conf&lt;/code&gt; for the container, or just change it in the container options via the web GUI and restart the container.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-18-at-7.19.41-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-18-at-7.19.41-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-18-at-7.49.05-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I learned this from &lt;code&gt;[theorangeone](https://theorangeone.net/posts/mount-nfs-inside-lxc/)&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Running Multiple Linux Commands in One Line</title><link>https://blog.iankulin.com/running-multiple-linux-commands-in-one-line/</link><pubDate>Wed, 19 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/running-multiple-linux-commands-in-one-line/</guid><description>&lt;p&gt;Since I&amp;rsquo;m constantly standing up Linux virtual machines and containers - almost always of the &lt;code&gt;apt&lt;/code&gt; variety, I&amp;rsquo;m constantly typing in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt update
apt upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then hitting enter again to allow whatever installation is needed to proceed. I&amp;rsquo;ve noticed in some of the commands I&amp;rsquo;ve been pasting in from installation instructions or StackExchange solutions have been separated by characters that look like it allows several commands to be run one after the other. To cut a long story short, the commands above could be entered like this with double ampersands:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The ampersands mean that the second command will run after the first one as long as it completes without an error. To avoid having to hit enter again, we can add the &lt;code&gt;-y&lt;/code&gt; flag.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;According to the &lt;a href="https://www.makeuseof.com/how-to-run-multiple-linux-commands-at-once/"&gt;MakeUseOf article&lt;/a&gt; I learned about the double ampersands from, we could also have used a semicolon if we want each command to run regardless of the success of the previous one, or a double pipe if you only wanted the second command run run if the first one fails.&lt;/p&gt;</description></item><item><title>Linux on HP Mini 110</title><link>https://blog.iankulin.com/linux-on-hp-mini-110/</link><pubDate>Mon, 17 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/linux-on-hp-mini-110/</guid><description>&lt;p&gt;I&amp;rsquo;ve been furthering my Linux education by playing with some desktop distros in VMs, but it&amp;rsquo;s not a great experience accessing them through the Proxmox web GUI. The alternative to this is to use a good &lt;a href="https://en.wikipedia.org/wiki/Simple_Protocol_for_Independent_Computing_Environments"&gt;SPICE&lt;/a&gt; client on the remote desktop, but there is &lt;a href="https://forum.proxmox.com/threads/access-vm-thru-spice-on-osx.66727/"&gt;not a simple good solution&lt;/a&gt; for this for MacOS.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been playing with the idea of picking up an old i3/i5 Thinkpad - these are around the AUD130 mark on eBay, to run a Linux distro with the main idea being to use it to SPICE into my VMs.&lt;/p&gt;
&lt;p&gt;This weekend at my parents house, I&amp;rsquo;ve been going through the cupboard secure wiping a couple of the discarded laptops, and found a fancy looking HP Mini 110-1131dx.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.notebookcheck.net/HP-Mini-110-Series.24414.0.html"&gt;&lt;img src="https://blog.iankulin.com/images/9563377_ra.jpg" width="500" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.notebookcheck.net/HP-Mini-110-Series.24414.0.html"&gt;&lt;img src="https://blog.iankulin.com/images/9563377cv3a.jpg" width="500" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This was a netbook, quite a cute little thing, and like most HP hardware - well made and popular enough that I should be able to googlesolve any issues I encounter. The Atom N270 that mousepowers it is a single core 32bit baby, but there&amp;rsquo;s also a moderate graphics accelerator chip - the &lt;a href="https://www.notebookcheck.net/Intel-Graphics-Media-Accelerator-950.2177.0.html"&gt;GMA 950&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Mint or Ubuntu would probably be my first choices for a desktop distro, but given the very low specs of the HP 110, I&amp;rsquo;m guessing that&amp;rsquo;s not going to be a good experience even if you go back to the most recent 32 bit versions. After a bit of googling around, I decided &lt;a href="https://antixlinux.com/"&gt;antiX&lt;/a&gt; or &lt;a href="https://lubuntu.net/"&gt;Lubuntu&lt;/a&gt; might be good choices (I&amp;rsquo;m partial to the &lt;code&gt;apt get&lt;/code&gt; family of distros).&lt;/p&gt;
&lt;h3 id="antix"&gt;antiX&lt;/h3&gt;
&lt;p&gt;As with most modern distros, the install was a painless experience - booting from the USB and following prompts. The UI was pleasant and crisp, and I especially liked the background on the desktop with some live statistics.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linuxinsider.com/story/antix-linux-not-pretty-but-highly-functional-86942.html"&gt;&lt;img src="https://blog.iankulin.com/images/86942_antix-3-small.jpg" width="620" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;From the base install, the wireless would not work properly. On the HP 100, there&amp;rsquo;s a little momentary switch on the front left of the keyboard with an indicator light. It was correctly indicating that the wireless was disabled (by glowing orange) and if I flicked it, I could see in the settings the bluetooth was going off and on, but not the wireless.&lt;/p&gt;
&lt;h2 id="lubuntu"&gt;Lubuntu&lt;/h2&gt;
&lt;p&gt;Again, a painless install experience. The ISO was about twice the size at 2.7GB and the install took a lot longer, although much of it was familiar to me from the numerous Debian and Ubuntu server installs I&amp;rsquo;ve done. Once it was installed and booted, the desktop seemed a bit chunky and dated compared to the flatter antiX, and although slower it was very usable.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4846b.jpg" width="1000" alt=""&gt;
&lt;p&gt;The wifi didn&amp;rsquo;t work, although the indicator was blue suggesting it was turned on. In the menu was an option to check for needed propitiatory drivers, and when I plugged into the Ethernet and ran this, it decided there was a Broadcom chipset wifi that it knew the drivers for. I allowed it to fetch and install them, and the wireless came to life.&lt;/p&gt;
&lt;p&gt;The proprietary drivers not being installed is a common and reasonable thing, and almost certainly the issue with my antiX install, so it seemed like it was probably worth having another go at that plugged into the ethernet and enabling whatever is needed to allow the non-FOSS stuff.&lt;/p&gt;
&lt;h2 id="antix-wifi"&gt;antiX wifi&lt;/h2&gt;
&lt;p&gt;My first thought was that perhaps I could just enable the non-free option for the Debian sources. The way that the apt package manager works is that there&amp;rsquo;s a list of sources it checks with. Some distros are strict about non-FOSS stuff and this needs changed in /etc/apt/sources to &lt;a href="https://serverfault.com/questions/240920/how-do-i-enable-non-free-packages-on-debian"&gt;make it check the non-free parts of the repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;antiX had a slightly different setup, with a whole directory of sources, but the non-free option was set. I also mucked around with the &lt;code&gt;rfkill&lt;/code&gt; command which kept saying the softblock was on - although I could see, by using rfkill list that the hardware button was working exactly how it should - flick it once and the hardware block for wifi and bluetooth was activated, flick it again and it went off.&lt;/p&gt;
&lt;p&gt;I also jumped off the cliff of just trying commands that I found on the internet that were suggested for similar sounding situations, and that I only had the shakiest idea of what they did. I&amp;rsquo;m certain that the problem at this stage is that I need to install those Broadcom 43 drivers. Without something poping up to ask me if I want to do that (which is exactly the sort of thing a lean distro wouldn&amp;rsquo;t have) I&amp;rsquo;m a bit lost.&lt;/p&gt;
&lt;p&gt;Antix seemed so right for my purposes, I might come back and try it again when my Linux knowledge is a bit better. In the mean time, I need a popular distro, lighter weight than Lubuntu, so I&amp;rsquo;ll give Mint a shot.&lt;/p&gt;
&lt;h2 id="mint-xfce"&gt;Mint Xfce&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://distrowatch.com/"&gt;Distrowatch&lt;/a&gt; currently lists Mint as #3, so it meets my &amp;ldquo;popular&amp;rdquo; criterion, and is has a Xfce (lightweight desktop environment) version, so perhaps it will run crisply on the Atom, but still hold my hand to install these wifi drivers.&lt;/p&gt;
&lt;p&gt;As with all the other distros, it went on smoothly. Once I booted in to it, some sort of system checker popped up to complain about the Broadcom drivers, and offered to extract them from the install USB - that didn&amp;rsquo;t work for me since I&amp;rsquo;d used &lt;a href="https://www.ventoy.net/en/index.html"&gt;Ventoy&lt;/a&gt; for the install, and the ISO was not loaded after I&amp;rsquo;d rebooted. Heading back up to the office to reconnect to an Ethernet cable allowed the system to download the drivers and five minutes later I was on wifi.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if I&amp;rsquo;ve used it enough to be sure, but the performance with Mint Xfce seems similar to Lubuntu - ie not as good as antiX. Also, there&amp;rsquo;s a scary message saying long term support runs out in nine days since I had to go back to version 19.3 to find a 32 bit version.&lt;/p&gt;</description></item><item><title>Recursively Deleting Files in Linux</title><link>https://blog.iankulin.com/recursively-deleting-files-in-linux/</link><pubDate>Fri, 14 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/recursively-deleting-files-in-linux/</guid><description>&lt;p&gt;I&amp;rsquo;ve been using this rsync command to backup files from my NAS to a USB drive. The &amp;ndash;excludes are to avoid copying over some junk hidden files - some created by MacOS and some by Synology.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo rsync -rvit --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;.DS_Store&amp;#39; /volume1/media/ /volumeUSB1/usbshare1-2/media --del
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;.DS_Store&lt;/code&gt; files seem to be dropped by MacOS every time I view a directory on the NAS from my MacBook. They&amp;rsquo;re not doing any harm, and they presumably do something handy for the Mac - remembering the view settings for that folder or some such. Nevertheless, they annoy me. It makes sense to not back them up - they don&amp;rsquo;t serve any useful purpose in that context.&lt;/p&gt;
&lt;p&gt;If I wanted to delete them anyway, how would I go about it? They are scattered randomly around, including in sub-directories of sub-directories. Is there some recursive flag I can add to &lt;code&gt;rm&lt;/code&gt; to accomplish this?&lt;/p&gt;
&lt;p&gt;I found a better solution on &lt;a href="https://askubuntu.com/questions/377438/how-can-i-recursively-delete-all-files-of-a-specific-extension-in-the-current-di"&gt;AskUbuntu&lt;/a&gt;, we can use the &lt;code&gt;find&lt;/code&gt; command. This way it&amp;rsquo;s easy to safely test your filename matching first before you destroy any files.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;find . -name &amp;#34;.DS_Store&amp;#34; -type f
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;-name&lt;/code&gt; is clear enough, and the &lt;code&gt;-type f&lt;/code&gt; option is just saying to look for files (rather than directories etc). The period at the start is the location, ie, start in the directory above so the current working directory is included. Once you&amp;rsquo;ve tuned this, you can add &lt;code&gt;-delete&lt;/code&gt; to go nuclear.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;find . -name &amp;#34;.DS_Store&amp;#34; -type f -delete
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Proxmox LXC backup to NFS share failing</title><link>https://blog.iankulin.com/proxmox-lxc-backup-to-nfs-share-failing/</link><pubDate>Wed, 12 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-lxc-backup-to-nfs-share-failing/</guid><description>&lt;p&gt;I was doing updates on all my nodes and VM&amp;rsquo;s today, and backing up the VMs that aren&amp;rsquo;t already on a backup schedule. On my dev machine I have a Debian LXC container that I mostly just use for trying out Linux commands and playing around. I used to have a backup of it that I used a lot - after playing around I like to set it back to a fresh install plus my ssh keys - but I lost it somehow when moving the VM to new metal.&lt;/p&gt;
&lt;p&gt;When I tried to back it up today, I got this drama.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;INFO: starting new backup job: vzdump 200 --node pve-dev1 --mode snapshot --remove 0 --notes-template &amp;#39;{{vmid}}-{{guestname}} ({{node}}) - after timezone fix&amp;#39; --storage NAS-DS2 --compress zstd
INFO: Starting Backup of VM 200 (lxc)
INFO: Backup started at 2023-04-07 17:11:08
INFO: status = running
INFO: CT Name: babydeb
INFO: including mount point rootfs (&amp;#39;/&amp;#39;) in backup
INFO: backup mode: snapshot
INFO: ionice priority: 7
INFO: create storage snapshot &amp;#39;vzdump&amp;#39;
 Logical volume &amp;#34;snap_vm-200-disk-0_vzdump&amp;#34; created.
INFO: creating vzdump archive &amp;#39;/mnt/pve/NAS-DS2/dump/vzdump-lxc-200-2023_04_07-17_11_08.tar.zst&amp;#39;
INFO: tar: /mnt/pve/NAS-DS2/dump/vzdump-lxc-200-2023_04_07-17_11_08.tmp: Cannot open: Permission denied
INFO: tar: Error is not recoverable: exiting now
INFO: cleanup temporary &amp;#39;vzdump&amp;#39; snapshot
 Logical volume &amp;#34;snap_vm-200-disk-0_vzdump&amp;#34; successfully removed
ERROR: Backup of VM 200 failed - command &amp;#39;set -o pipefail &amp;amp;&amp;amp; lxc-usernsexec -m u:0:100000:65536 -m g:0:100000:65536 -- tar cpf - --totals --one-file-system -p --sparse --numeric-owner --acls --xattrs &amp;#39;--xattrs-include=user.*&amp;#39; &amp;#39;--xattrs-include=security.capability&amp;#39; &amp;#39;--warning=no-file-ignored&amp;#39; &amp;#39;--warning=no-xattr-write&amp;#39; --one-file-system &amp;#39;--warning=no-file-ignored&amp;#39; &amp;#39;--directory=/mnt/pve/NAS-DS2/dump/vzdump-lxc-200-2023_04_07-17_11_08.tmp&amp;#39; ./etc/vzdump/pct.conf ./etc/vzdump/pct.fw &amp;#39;--directory=/mnt/vzsnap0&amp;#39; --no-anchored &amp;#39;--exclude=lost+found&amp;#39; --anchored &amp;#39;--exclude=./tmp/?*&amp;#39; &amp;#39;--exclude=./var/tmp/?*&amp;#39; &amp;#39;--exclude=./var/run/?*.pid&amp;#39; ./ | zstd --rsyncable &amp;#39;--threads=1&amp;#39; &amp;gt;/mnt/pve/NAS-DS2/dump/vzdump-lxc-200-2023_04_07-17_11_08.tar.dat&amp;#39; failed: exit code 2
INFO: Failed at 2023-04-07 17:11:09
INFO: Backup job finished with errors
TASK ERROR: job errors
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/could-it-be-a-permissions-problem/"&gt;Permissions&lt;/a&gt;! I was puzzled - the line before (creating the backup file) is working, but not creating the temp file on the same share and directory? Very odd. Backing up a real VM on the same node and to the same share was working fine. Luckily it&amp;rsquo;s a simple, and fast, matter to create a heap of LXCs with different settings and see if the error can be reproduced, so I was soon confidently able to say the problem only existed for unprivileged LXC containers backing up to the share - I didn&amp;rsquo;t have the problem if I used the local disk.&lt;/p&gt;
&lt;p&gt;If I dropped to the console for the node, I could create an identically named file in the same directory with no problems.&lt;/p&gt;
&lt;p&gt;During all that testing, I saw some output that led to more helpful thinking.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;INFO: starting new backup job: vzdump 303 --storage NAS-DS2 --compress zstd --notes-template &amp;#39;{{guestname}}&amp;#39; --remove 0 --node pve-dev1 --mode snapshot
INFO: Starting Backup of VM 303 (lxc)
INFO: Backup started at 2023-04-07 18:43:44
INFO: status = running
INFO: CT Name: apline-unpriv
INFO: including mount point rootfs (&amp;#39;/&amp;#39;) in backup
INFO: mode failure - some volumes do not support snapshots
INFO: trying &amp;#39;suspend&amp;#39; mode instead
INFO: backup mode: suspend
INFO: ionice priority: 7
INFO: CT Name: apline-unpriv
INFO: including mount point rootfs (&amp;#39;/&amp;#39;) in backup
INFO: temporary directory is on NFS, disabling xattr and acl support, consider configuring a local tmpdir via /etc/vzdump.conf
INFO: starting first sync /proc/39778/root/ to /mnt/pve/NAS-DS2/dump/vzdump-lxc-303-2023_04_07-18_43_44.tmp
INFO: first sync finished - transferred 9.35M bytes in 2s
INFO: suspending guest
INFO: starting final sync /proc/39778/root/ to /mnt/pve/NAS-DS2/dump/vzdump-lxc-303-2023_04_07-18_43_44.tmp
INFO: final sync finished - transferred 0 bytes in 0s
INFO: resuming guest
INFO: guest is online again after &amp;lt;1 seconds
INFO: creating vzdump archive &amp;#39;/mnt/pve/NAS-DS2/dump/vzdump-lxc-303-2023_04_07-18_43_44.tar.zst&amp;#39;
INFO: tar: /mnt/pve/NAS-DS2/dump/vzdump-lxc-303-2023_04_07-18_43_44.tmp: Cannot open: Permission denied
INFO: tar: Error is not recoverable: exiting now
ERROR: Backup of VM 303 failed - command &amp;#39;set -o pipefail &amp;amp;&amp;amp; lxc-usernsexec -m u:0:100000:65536 -m g:0:100000:65536 -- tar cpf - --totals --one-file-system -p --sparse --numeric-owner --acls --xattrs &amp;#39;--xattrs-include=user.*&amp;#39; &amp;#39;--xattrs-include=security.capability&amp;#39; &amp;#39;--warning=no-file-ignored&amp;#39; &amp;#39;--warning=no-xattr-write&amp;#39; --one-file-system &amp;#39;--warning=no-file-ignored&amp;#39; &amp;#39;--directory=/mnt/pve/NAS-DS2/dump/vzdump-lxc-303-2023_04_07-18_43_44.tmp&amp;#39; ./etc/vzdump/pct.conf ./etc/vzdump/pct.fw &amp;#39;--directory=/mnt/pve/NAS-DS2/dump/vzdump-lxc-303-2023_04_07-18_43_44.tmp&amp;#39; --no-anchored &amp;#39;--exclude=lost+found&amp;#39; --anchored &amp;#39;--exclude=./tmp/?*&amp;#39; &amp;#39;--exclude=./var/tmp/?*&amp;#39; &amp;#39;--exclude=./var/run/?*.pid&amp;#39; . | zstd --rsyncable &amp;#39;--threads=1&amp;#39; &amp;gt;/mnt/pve/NAS-DS2/dump/vzdump-lxc-303-2023_04_07-18_43_44.tar.dat&amp;#39; failed: exit code 2
INFO: Failed at 2023-04-07 18:43:47
INFO: Backup job finished with errors
TASK ERROR: job errors
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And sure enough, there is a helpful &lt;code&gt;/etc/vzdump.conf&lt;/code&gt; file. Uncommenting the &lt;code&gt;tmpdir&lt;/code&gt; line and pointing it to &lt;code&gt;/tmp&lt;/code&gt; fixed all my problems.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-6.50.45-pm-copy.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So what&amp;rsquo;s going on? I did some googling and found some discussions &lt;a href="https://forum.proxmox.com/threads/cannot-backup-only-lxc-to-nfs-vm-works.90797/"&gt;1&lt;/a&gt;/&lt;a href="https://forum.proxmox.com/threads/in-7-0-i-cant-backup-a-container-to-a-nfs-that-worked-in-6-0.97808/"&gt;2&lt;/a&gt;/&lt;a href="https://forum.proxmox.com/threads/backup-of-lxc-containers-to-nfs-mount-fail.95146/"&gt;3&lt;/a&gt; in the &lt;a href="https://forum.proxmox.com/"&gt;Proxmox forums&lt;/a&gt;. They are saying it&amp;rsquo;s because the unprivileged containers (they don&amp;rsquo;t run as root, which seems like good practice) don&amp;rsquo;t have permissions for the NFS share directory. I feel there&amp;rsquo;s a few problems with this theory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It seems to do fine creating the other files&lt;/li&gt;
&lt;li&gt;Why would the LXC container be doing this work? Surely the process is being run at the Proxmox level.&lt;/li&gt;
&lt;li&gt;Actually the LXC container should not have access to the NAS at all, even if it&amp;rsquo;s privileged - it&amp;rsquo;s not mounted in there, the LXC knows nothing about it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nevertheless, I&amp;rsquo;m sure they know better than me. If I was shipping this product, I&amp;rsquo;d probably engineer around this problem. Maybe by detecting it and switching to &lt;code&gt;/var/tmp&lt;/code&gt; or even just by making that the default in the config file.&lt;/p&gt;</description></item><item><title>Using NAS for Proxmox backups</title><link>https://blog.iankulin.com/using-nas-for-proxmox-backups/</link><pubDate>Mon, 10 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-nas-for-proxmox-backups/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/"&gt;A few weeks ago&lt;/a&gt;, I was very excited to be able to take a snapshot of a virtual machine, copy it across the network from that Proxmox node, copy it back across the network to a different Proxmox node, start it there, and have it up and running, without it noticing it was actually on different hardware.&lt;/p&gt;
&lt;p&gt;Backing up a VM is pretty simple, you just click on the node, choose &lt;em&gt;Backup&lt;/em&gt; and click the &lt;em&gt;Backup Now&lt;/em&gt; button. The ease, and completeness of backing up a VM is one of the main reasons I&amp;rsquo;m using Proxmox for my systems.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-12.02.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-12.02.59-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By default, VM backups are saved to the &amp;ldquo;local drive&amp;rdquo; - actually the &lt;code&gt;/var/lib/vz&lt;/code&gt; directory. This would not be useful if the physical machine dies, but also it&amp;rsquo;s not convenient to restore to a different machine. Ideally you&amp;rsquo;d have a central place to store these files that was accessible to all the Proxmox nodes.&lt;/p&gt;
&lt;p&gt;This is exactly the situation I&amp;rsquo;ve setup with my lab, the NAS is the storage for the VM backups. Each of the Proxmox nodes uses the same directory for backups, so moving a machine from one node to another is a simple as backing it up on one node, stopping the VM, and restoring it on another node just by choosing the backup file to restore in the web GUI.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;p&gt;Proxmox can use all sorts of shares as a location for backups (and other files such as the ISO&amp;rsquo;s used to boot new machines), but the simplest is probably &lt;a href="https://en.wikipedia.org/wiki/Network_File_System"&gt;NFS&lt;/a&gt;. This is also straightforward to do from the Synology NAS.&lt;/p&gt;
&lt;p&gt;In the web interface for the NAS, go into &lt;em&gt;Control Panel&lt;/em&gt;, &lt;em&gt;Shared Folder&lt;/em&gt; and create a new shared folder. I called mine Proxmox. One of the tabs there is for NFS permissions - just add the IP address of the Proxmox node that you&amp;rsquo;d life to access the folder.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-1.46.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not much harder from the Proxmox end. Although the storage you add will appear at the node level in the &lt;em&gt;Server View&lt;/em&gt; of the web GUI, it is added at the &lt;em&gt;Datacenter&lt;/em&gt; level.&lt;/p&gt;
&lt;p&gt;Go into &lt;em&gt;Storage&lt;/em&gt;, select &lt;em&gt;Add&lt;/em&gt; and choose &lt;em&gt;NFS&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.00.04-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.00.04-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then enter an ID (this will be the name of the storage in Proxmox) and the IP address. If you wait half a second, then you can click the dropdown for all the folders that are shared from that IP address.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.06.19-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The last field is content - this refers the the type of Proxmox stuff you want to keep in there - for backups, you just need VZDumps, but I usually click on everything since I&amp;rsquo;ll also use it for ISOs for new VMs and templates for LXCs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.11.03-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve added that, the storage will appear in the server view, but also as an option when you go into &lt;em&gt;Backup&lt;/em&gt; for a VM and select &lt;em&gt;Backup Now&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.15.53-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Where Do Docker Container Logs Go?</title><link>https://blog.iankulin.com/where-do-docker-container-logs-go/</link><pubDate>Sat, 08 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/where-do-docker-container-logs-go/</guid><description>&lt;p&gt;I&amp;rsquo;m still loving the Docker &amp;ldquo;just works&amp;rdquo; magic, despite their &lt;a href="https://www.theregister.com/2023/03/17/docker_free_teams_plan/"&gt;terrible PR skills&lt;/a&gt;, but sometimes I start a container, then the &lt;code&gt;docker ps -a&lt;/code&gt; shows it exited almost immediately. Clearly I&amp;rsquo;ve made a mistake, but there&amp;rsquo;s no stdout error message to tell me what I&amp;rsquo;ve done wrong, where is it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at an example from today. I&amp;rsquo;m testing &lt;a href="https://filebrowser.org/"&gt;Filebrowser&lt;/a&gt; on a dev machine before I deploy it to the remote backup machine I&amp;rsquo;m assembling. And instead of following the &lt;a href="https://filebrowser.org/installation"&gt;official instructions&lt;/a&gt;, I&amp;rsquo;m following a &lt;a href="https://bobcares.com/blog/filebrowser-installation-in-docker/"&gt;blog post&lt;/a&gt; which has a few more details, but unfortunately also a small error.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-02-at-1.35.16-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-02-at-1.35.16-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The first sign of a problem is that the container is not running after I&amp;rsquo;ve launched it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-02-at-1.42.09-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;To cat the log for the exited container is simple. Note that the (randomly provided) name for the container is &lt;code&gt;eager_haslett&lt;/code&gt; so to see the log we enter:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker logs eager_haslett
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The log output was:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;panic: While parsing config: invalid character &amp;#39;\n&amp;#39; in string literal

goroutine 1 [running]:
github.com/filebrowser/filebrowser/v2/cmd.initConfig()
	/home/runner/work/filebrowser/filebrowser/cmd/root.go:410 +0x346
github.com/spf13/cobra.(*Command).preRun(...)
	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:886
github.com/spf13/cobra.(*Command).execute(0x1828580, {0xc000030220, 0x0, 0x0})
	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:822 +0x44e
github.com/spf13/cobra.(*Command).ExecuteC(0x1828580)
	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:974 +0x3b4
github.com/spf13/cobra.(*Command).Execute(...)
	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:902
github.com/filebrowser/filebrowser/v2/cmd.Execute()
	/home/runner/work/filebrowser/filebrowser/cmd/cmd.go:9 +0x25
main.main()
	/home/runner/work/filebrowser/filebrowser/main.go:8 +0x17
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s our answer right at the top of the log - there&amp;rsquo;s a newline character in the middle of a key:value pair in the config JSON. If you look at the blog page above you can see it after the database.db&lt;/p&gt;</description></item><item><title>Allowing Proxmox to use a Dynamic IP</title><link>https://blog.iankulin.com/allowing-proxmox-to-use-a-dynamic-ip/</link><pubDate>Thu, 06 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/allowing-proxmox-to-use-a-dynamic-ip/</guid><description>&lt;p&gt;I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/proxmox-dynamic-ip/"&gt;discussed before&lt;/a&gt;, that when you first install Proxmox, it grabs an IP address from your DHCP server (this usually runs in your ISP modem if you haven&amp;rsquo;t created a better setup), but then it stores it as a static ip. This is a sort of compromise that makes sense and works for most circumstances.&lt;/p&gt;
&lt;p&gt;As soon as I&amp;rsquo;ve provisioned a new Proxmox server, I then usually tell the DHCP server, to always serve that address to the MAC address of the new Proxmox server. Since Proxmox does not use the DHCP server on subsequent boots, all that really does is prevent the DHCP server give the same IP address out to another device - which had happened to me prompting the earlier post. The DHCP server had given the address to a wifi lightbulb while the server was off, then when the Proxmox server booted up, the netwrok access was all messed up.&lt;/p&gt;
&lt;p&gt;In general, servers should have a static IP address - they are providing resources that other devices on the network need to access, so the combination of grabbing a DHCP address, using it statically, then me locking it in at the DHCP server makes sense.&lt;/p&gt;
&lt;p&gt;Except that I&amp;rsquo;m building a system with a couple of VM&amp;rsquo;s and a NAS that I&amp;rsquo;m going to post off, and have it set up by a non-techie at a remote site. So I really need Proxmox on that machine to look for a DHCP server when it boots and collect a dynamic IP address. Like a lot of things in Linux, this is quite a simple change if you know where to look.&lt;/p&gt;
&lt;h3 id="what-to-change"&gt;What to Change&lt;/h3&gt;
&lt;p&gt;The configuration file for the network interfaces is /&lt;code&gt;etc/network/interfaces&lt;/code&gt; the one on the Proxmox machine I&amp;rsquo;m setting up looked like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;iface lo inet loopback

iface eno1 inet manual

auto vmbr0
iface vmbr0 inet static
	address 192.168.100.30/24
	gateway 192.168.100.1
	bridge-ports eno1
	bridge-stp off
	bridge-fd 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;iface&lt;/code&gt; is short for interface, and is followed by the interface name. These are the same names you see when you type in &lt;code&gt;ip addr&lt;/code&gt; to see the IP addresses.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/edit.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So this is the bit we are interested in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;iface vmbr0 inet static
	address 192.168.100.30/24
	gateway 192.168.100.1
	bridge-ports eno1
	bridge-stp off
	bridge-fd 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All that bridge stuff can stay the same, I&amp;rsquo;ll comment out the static bits and change it to use the DHCP. The final file looks like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;auto lo
iface lo inet loopback

iface eno1 inet manual

auto vmbr0
#iface vmbr0 inet static
#	address 192.168.100.30/24
#	gateway 192.168.100.1
iface vmbr0 inet dhcp
	bridge-ports eno1
	bridge-stp off
	bridge-fd 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I used the mac address to tell the DCHP server to allocate it a different address, and rebooted and Proxmox picked up the new address perfectly.&lt;/p&gt;
&lt;h3 id="hosts"&gt;Hosts&lt;/h3&gt;
&lt;p&gt;Now the server had a new address, there&amp;rsquo;s one more place I need to update; /etc/hosts contains the domain information you set during the Proxmox install, and it will include that old IP address. Once the system has a new one, it needs to be edited to include that.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;127.0.0.1 localhost.localdomain localhost
192.168.100.28 pve-kr01.local pve-kr01

# The following lines are desirable for IPv6 capable hosts

::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After the system is installed at the remote site and booted up, I&amp;rsquo;ll ssh in (with Tailscale) and make that change, and hopefully be able to access the DHCP server so it does not change in future.&lt;/p&gt;
&lt;h3 id="resources"&gt;Resources&lt;/h3&gt;
&lt;p&gt;I found these posts useful when figuring this out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forum.proxmox.com/threads/set-a-dynamic-address-to-pve.119847/"&gt;Set a dynamic address to PVE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://schoolitexpert.com/network-tools/proxmox-ve/dynamic-ip-address"&gt;Dynamic IP address&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Versions: Proxmox 7.4-3&lt;/p&gt;</description></item><item><title>RAID Rescue</title><link>https://blog.iankulin.com/raid-rescue/</link><pubDate>Tue, 04 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/raid-rescue/</guid><description>&lt;p&gt;I&amp;rsquo;m in the process of shuffling disks around as I move towards my 3-2-1 storage arrangements. I thought after my extensive rsync adventures I&amp;rsquo;d mirrored everything everywhere, but then realised, with a sinking (no pun) feeling, after I&amp;rsquo;d repurposed a drive out of the 2 drive Synology as a USB caddy drive and wiped it, that I&amp;rsquo;d forgotten my audio book directory. All my rsync fiddling around had been on the video subdirectory of the media folder, not the whole media directory that included my audiobooks.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not the end of the world if I&amp;rsquo;d wiped them, I&amp;rsquo;ve just been working through downloading them from Audible and de-drming, so I could do that again in the few days I&amp;rsquo;ve got left till my subscription cancellation date comes around. That was a painful and slow process, so I don&amp;rsquo;t really want to.&lt;/p&gt;
&lt;p&gt;I still had one of the RAID drives that hadn&amp;rsquo;t been wiped, so in theory it should have a full copy of the data, and if I put it back in the Synology by itself it should work. That would be the same situation as if one drive in the RAID pool had died completely.&lt;/p&gt;
&lt;p&gt;A few screws, and a drive swap later and I&amp;rsquo;m looking at this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-31-at-4.35.40-pm-copy.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There must be a tiny bit of storage in the Synology, so it knows I&amp;rsquo;ve been fiddling around. I hit &lt;em&gt;Recover&lt;/em&gt;, and it did the ten minute thing that I assume is it downloading and installing the new DSM.&lt;/p&gt;
&lt;p&gt;During this process, it started beeping in a plaintive way. I couldn&amp;rsquo;t access with the old Tailscale address, so I fired up the IP address to the web interface, logged in with the old credentials and was greeted with this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-31-at-4.08.58-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It was not happy working in the degraded state, but the files were all still there, I was able to mount it to the other NAS and copy my files out. A success. The Tailscale package was still installed, so perhaps that business at the beginning was not really a new install of DSM, but some sort of checking.&lt;/p&gt;
&lt;p&gt;This was a good experience, it is worthwhile to test these scenarios, and I&amp;rsquo;m reassured to discover the audible beeping when the RAID pool was degraded. At the moment while I get everything sorted I&amp;rsquo;m in the web interfaces a lot, but the dream is this learning and setup time comes to an end and I just consume my self-hosted services without much manual intervention. In that scenario, some un-ignorable beeping when the NAS needs attention is a good thing.&lt;/p&gt;</description></item><item><title>HP EliteDesk 800 G2 Memory Upgrade</title><link>https://blog.iankulin.com/hp-elitedesk-800-g2-memory-upgrade/</link><pubDate>Sun, 02 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hp-elitedesk-800-g2-memory-upgrade/</guid><description>&lt;p&gt;The hardware engineering of these corporate world mini-PCs is really nice. I swapped out the RAM today to bump my main machine up to 32GB from 16GB. It was a straightforward task - no screwdrivers, no drama.&lt;/p&gt;
&lt;p&gt;To open the machine up, there is a single large screw on the back that can be undone with your fingers - it&amp;rsquo;s a captive screw, as in it doesn&amp;rsquo;t fall out - just another nice engineering thought.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4432.jpg" width="331" alt=""&gt;
&lt;p&gt;Once that&amp;rsquo;s undone, to get the case off, you just push down lightly on the top so the rubber feet grip the desk and slide it towards the front about an inch. Then it just lifts off - no wires. Once that&amp;rsquo;s off, you&amp;rsquo;ll see the SSD on the left (looking from the front) and fan on the right.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4434.jpg" width="364" alt=""&gt;
&lt;p&gt;There&amp;rsquo;s a little plastic tab on the front of the fan just over the front USB ports. If you lift that up a little, you can pull the fan towards you a couple of centimeters then put it down on it&amp;rsquo;s back next to the case without unplugging its power. You can see the RAM modules were underneath the fan.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4438.jpg" width="813" alt=""&gt;
&lt;p&gt;Either side of each RAM module you can see little metal clips. If you push these both outwards, the module will pop up to a 20° angle, then it can just be pulled out of the connector gently. Do this first for the top one, then the bottom.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4439.jpg" width="1000" alt=""&gt;
&lt;p&gt;Inserting the new modules is done in the reverse order. Push the bottom one all the way into it&amp;rsquo;s socket at the same angle you took the other one out. Then with a finger on the raised edge, push it down until the clips both sides engage. Then do the same with the top one.&lt;/p&gt;
&lt;p&gt;When you flip the fan over to replace it, you&amp;rsquo;ll see that it has a small protrusion each side on the back legs, these slide into the two metal slots on top of the CPU cooler, then the fan just sits down into it&amp;rsquo;s previous spot. Lower the case top down about an inch from the back, slide it into place and finger tighten the screw, and you&amp;rsquo;re down.&lt;/p&gt;
&lt;h3 id="ram-specifications"&gt;RAM Specifications&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://support.hp.com/au-en/product/hp-elitedesk-800-35w-g2-desktop-mini-pc/7633266/manuals"&gt;Hardware Reference and Maintenance and Service guides&lt;/a&gt; for the HP EliteDesk 800 G2 Desktop Mini have this page on the RAM specifications. You&amp;rsquo;re looking for 1.2V DDR4-2133MHz SODIMMs, PC4-17000&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-26-at-3.11.40-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-26-at-3.11.40-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The eBay listing for the ones I bought said they were &amp;ldquo;SK Hynix 16GB DDR4 SODIMM RAM 2133 MHz Laptop PC4-17000 HMA82GS6MFRN-TF&amp;rdquo; and they went in and worked perfectly.&lt;/p&gt;</description></item><item><title>Proxmox Backup Files</title><link>https://blog.iankulin.com/proxmox-backup-files/</link><pubDate>Fri, 31 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-backup-files/</guid><description>&lt;p&gt;I&amp;rsquo;ve got some extra RAM to drop into the HP 800 G2 mini that I use as my production server. I feel like that&amp;rsquo;s a low risk change, but since it&amp;rsquo;s easy to take VM snapshots I shutdown the VM&amp;rsquo;s and did that, and wanted to just copy them off the local storage.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m moving towards having these backups (and the ISOs) on the NAS rather than locally, but have not implemented that. So to get my backups I need to SSH in and find them.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://pve.proxmox.com/wiki/Storage:_Directory"&gt;Proxmox documentation for storage&lt;/a&gt; says to have a look in &lt;code&gt;/etc/pve/storage.cfg&lt;/code&gt; to see what&amp;rsquo;s up. Mine looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;dir: local
	path /var/lib/vz
	content iso,vztmpl,backup

lvmthin: local-lvm
	thinpool data
	vgname pve
	content rootdir,images
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And sure enough, if I look in &lt;code&gt;/var/lib/vz/dump&lt;/code&gt; (dump is the backup location mentioned in the docs):&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-26-at-11.59.10-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-26-at-11.59.10-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I ain&amp;rsquo;t messing around this morning, so I&amp;rsquo;ll just grab these onto my laptop with scp.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;scp root@192.168.100.23:/var/lib/vz/dump/\* Downloads
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You may notice in the command above that I&amp;rsquo;ve got a backslash in front of the wildcard. This was a little gotcha that is specific to using zsh/OhMyZsh that I had to escape the wildcard. I found I could specify the whole filename and it worked okay, but the wildcards needed escaping. Thanks again &lt;a href="https://superuser.com/questions/420525/scp-with-zsh-no-matches-found"&gt;StackExchange&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-26-at-12.15.35-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>rsync episode IV - a sudo hope</title><link>https://blog.iankulin.com/rsync-episode-iv-a-sudo-hope/</link><pubDate>Thu, 30 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-episode-iv-a-sudo-hope/</guid><description>&lt;p&gt;With all those earlier rsync bumps out of the way, I was ready to try my first rsync backup at the command line to sync my movies directory on the NAS to a (NTFS formatted) USB drive plugged into the same NAS. This is to be one of the simplest since there&amp;rsquo;s no remote server involved, just copying from mount point directory to another - so no drama with remote permissions.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a lot of files involved, and I knew from running the dry run that there would be a lot of output. I could see a few error messages, but each of the file copies was taking a while so I was confident they, at least, were working. If you missed the last episode, here&amp;rsquo;s where I landed for this rsync command.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -avi --exclude &amp;#39;*@eaDir*&amp;#39; /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Before I worried about the error messages, I had a look to see if the files had been copied correctly, but they had not. Even though each file was taking about the right amount of time to copy, the new files were not making it to the destination directories. Nor did they seem to be anywhere else. So I guess go back to the error messages and try to understand them. Here&amp;rsquo;s a &lt;em&gt;very&lt;/em&gt; condensed selection of the output.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;
rsync: failed to set times on &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)&amp;#34;: Operation not permitted (1)

...

&amp;gt;f+++++++++ Jungle Book (1942)/Jungle Book (1942).mkv
&amp;gt;f+++++++++ Jungle Book (1942)/trailer.mp4

...

rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.Jungle Book (1942).mkv.Wd141R&amp;#34; failed: Operation not permitted (1)

rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.trailer.mp4.TNu7UC&amp;#34; failed: Operation not permitted (1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This log is from one of my later attempts. This video was new on the NAS so an earlier running of the rsync would have had some more lines about creating the directory on the USB - and I had a through the file browser in the NAS. The correct directory had been created, but there were no files in it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at this output a bit at a time:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync: failed to set times on &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)&amp;#34;: Operation not permitted (1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;rsync is trying to update the date/time of the destination folder to match the source one - something similar to the &lt;code&gt;touch&lt;/code&gt; command. &lt;code&gt;not permitted&lt;/code&gt; sounds like a &lt;a href="https://blog.iankulin.com/could-it-be-a-permissions-problem/"&gt;permissions issue&lt;/a&gt;. There was one of these messages for every directory and they were all near the start of the log.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt;f+++++++++ Jungle Book (1942)/Jungle Book (1942).mkv
&amp;gt;f+++++++++ Jungle Book (1942)/trailer.mp4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These are not errors, but encouraging output. The code at the beginning says what&amp;rsquo;s going on - they are files, and are being copied from the source to the destination. These showed up for every file that was in the source but not the destination, and the times for the file copies felt about right - the .mkv file took a while, the trailer was quick. These messages were all together in the middle of the log.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.Jungle Book (1942).mkv.Wd141R&amp;#34; failed: Operation not permitted (1)

rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.trailer.mp4.TNu7UC&amp;#34; failed: Operation not permitted (1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;[mkstemp](https://man7.org/linux/man-pages/man3/mkstemp.3.html)&lt;/code&gt; is a command for creating a temporary file. It&amp;rsquo;s common in operating systems to use a hidden temporary file when transferring a file - you save into the temp file then rename it when its successfully completed. I&amp;rsquo;m guessing that&amp;rsquo;s what&amp;rsquo;s happening here, and it&amp;rsquo;s failing for permissions reasons. What I don&amp;rsquo;t understand is why they would all be grouped together at the end of the log instead of back next to each individual copy.&lt;/p&gt;
&lt;p&gt;But anyway, this is clearly a permissions problem. I can easily check this just by trying the copy manually. I&amp;rsquo;m still logged in as the same user who executed the &lt;code&gt;rsync&lt;/code&gt; command so the &lt;code&gt;cp&lt;/code&gt; will have the same rights etc and so should also fail&amp;hellip;&lt;/p&gt;
&lt;p&gt;No - this copy worked perfectly. No error message, and when I used the NAS filebrowser, the new file had correctly copied onto the USB drive.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cp /volume1/media/video/Movies/&amp;#39;Jungle Book (1942)&amp;#39;/&amp;#39;Jungle Book (1942).mkv&amp;#39; /volumeUSB1/usbshare/media/video/Movies/&amp;#39;Jungle Book (1942)&amp;#39;/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So somehow &lt;code&gt;rsync&lt;/code&gt; is running with lower permissions than &lt;code&gt;cp&lt;/code&gt; when executed by the same user? Well, (grasping at straws now) what if I &lt;code&gt;sudo&lt;/code&gt; it? I tried that, and (1) there&amp;rsquo;s no &lt;em&gt;set times&lt;/em&gt; error, (2) all the file copies worked, and (3) no &lt;em&gt;mkstemp&lt;/em&gt; errors.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo rsync -avi --exclude &amp;#39;*@eaDir*&amp;#39; /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I did notice one other difference, there was lots of &amp;lsquo;o&amp;rsquo; in the itemize changes output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.d..tpo.... Jungle Book (1942)/
&amp;gt;f..tpo.... Jungle Book (1942)/Jungle Book (1942).mkv
&amp;gt;f+++++++++ Jungle Book (1942)/trailer.mp4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From the &lt;a href="https://download.samba.org/pub/rsync/rsync.1#opt--itemize-changes"&gt;man page&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.d..tpo....&lt;/code&gt; - not being copied, it&amp;rsquo;s a directory, modification time is different and is being updated, permissions are different and are being updated, the owner is different and is being updated&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;f..tpo....&lt;/code&gt; - is being copied to destination, it&amp;rsquo;s a file, modification time is different and is being updated, permissions are different and are being updated, the owner is different and is being updated&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;f+++++++++&lt;/code&gt; - is being copied to destination, it&amp;rsquo;s a file, everything is being newly created so will be the same as the source&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s a major step forward, the files are syncing correctly, and if I rerun the rsync and haven&amp;rsquo;t made any changes to the source files, it zips through. I do notice it is still updating the permissions and owner each time. This doesn&amp;rsquo;t produce an error message, but since it thinks they need done every run it suggests it is not happening correctly.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible the owner/permissions issue is related to the USB drive being NTFS formatted. It&amp;rsquo;s also possible I can get rsync to stop trying to change those since they are not important in this context. the &lt;code&gt;-a&lt;/code&gt; flag (short for archive) is a shortcut that pulls in a number of other flags. Perhaps I can just pick the ones I need and eliminate the owner and permissions ones.&lt;/p&gt;
&lt;p&gt;Having to sudo to get this to work does not seem like a great solution - presumably this will come back to bite me when I try and automate it. So there is still some figuring out to do, but at least one step of my backup is down now.&lt;/p&gt;</description></item><item><title>rsync / Synology / @eaDir</title><link>https://blog.iankulin.com/rsync-synology-eadir/</link><pubDate>Tue, 28 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-synology-eadir/</guid><description>&lt;p&gt;The reason I&amp;rsquo;ve been figuring out rsync is to setup my backup strategy. Eventually this will partly be managed with scheduled tasks (ie cron jobs) running rsync. I wanted the SSH in and try this out, since I didn&amp;rsquo;t know some basic things like the mount points of the shares.&lt;/p&gt;
&lt;h3 id="mount-points"&gt;Mount points&lt;/h3&gt;
&lt;p&gt;My first issue was to find the paths to all my data. This turned out not to be a drama. Each of the volumes you create when the NAS is set up are just in the root directory. This includes any USB drives plugged in.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-8.08.10-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Inside each of those &lt;em&gt;volumes&lt;/em&gt; are any &lt;em&gt;shares&lt;/em&gt; you&amp;rsquo;ve created. At the moment I want to rsync my movies which are in a &amp;lsquo;media&amp;rsquo; share on volume1 to the usb drive, so the directories I&amp;rsquo;ll be using are:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/volume1/media/video/Movies/&lt;/code&gt;&lt;br&gt;
&lt;code&gt;/volumeUSB1/usbshare/media/video/Movies&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="rsync-attempt"&gt;rsync attempt&lt;/h3&gt;
&lt;p&gt;rsync has a cool feature whereby you can do a &amp;lsquo;dry run&amp;rsquo; where it goes through the motions of the command you&amp;rsquo;ve given it, but doesn&amp;rsquo;t change any files. If you combine this with the verbose output, you can clearly see what it&amp;rsquo;s going to do before you let it start changing things. That&amp;rsquo;s an especially good idea when you&amp;rsquo;re dealing with large amounts of data, so my first pass at this included the -n option.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -avin /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The situation with these two lots of data is that I&amp;rsquo;ve copied my media off the USB drive onto the NAS, then when I installed Jellyfin to access it, I discovered lots of misnamed items (had the years incorrect mostly) and I&amp;rsquo;ve been combining some directories, and renaming others and so on. So I expected this first run or rsync to pull up a heap of changes to make, which it did - thousands of lines of them.&lt;/p&gt;
&lt;p&gt;I noticed a lot of them included this weird directory that I didn&amp;rsquo;t recognise.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt;f+++++++++ @eaDir/Tora Tora Tora (1970 PG)@SynoEAStream
&amp;gt;f+++++++++ @eaDir/Tora Tora Tora (1970 PG)@SynoResource
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;ve since learned it might be extended attributes, people started noticing it around the introduction of DSM7. &lt;a href="https://tech.webit.nu/synology-nas-those-eadir-folders/"&gt;I don&amp;rsquo;t seem to be the only user who hates&lt;/a&gt; Synology messing with my data. There&amp;rsquo;s some consensus they are created by the indexing service (which I&amp;rsquo;ve turned off as much as is possible in the GUI) and when the &lt;a href="https://www.reddit.com/r/synology/comments/exh5ho/preventing_eadir_from_being_created/"&gt;drives are externally mounted&lt;/a&gt; - which of course I have been doing quite a bit while moving things around.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll tackle removing them all and trying to prevent their reoccurence another day, but for the moment, I&amp;rsquo;ll just tell rsync to ignore them using the &lt;code&gt;--exclude&lt;/code&gt; option.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -avin --exclude &amp;#39;*@eaDir*&amp;#39; /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>SSH with Keys to Synology</title><link>https://blog.iankulin.com/ssh-with-keys-to-synology/</link><pubDate>Mon, 27 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-with-keys-to-synology/</guid><description>&lt;p&gt;The Synology operating system DSM (I&amp;rsquo;m on DSM 7.1.1) is Linux, but its highly customised for the purpose of making running a complicated Linux NAS doable for less technical users.&lt;/p&gt;
&lt;p&gt;Due to that, some things that are routine in a regular distro, require a few more steps to jump through to get them to work. SSH-ing in to a Synology with keys is one of those things.&lt;/p&gt;
&lt;h3 id="should-you"&gt;Should you?&lt;/h3&gt;
&lt;p&gt;Before you do start fiddling around, it&amp;rsquo;s probably worth mentioning that almost all the things you might want to do on the Synology can be accomplished through their web interface, or by installing a &amp;lsquo;package&amp;rsquo; from the &lt;em&gt;Package Center&lt;/em&gt;. For example, if you need to run a cron job, that&amp;rsquo;s done through the &lt;em&gt;Control Panel&lt;/em&gt; &amp;lsquo;&lt;em&gt;Task Scheduler&lt;/em&gt;&amp;rsquo;. If you need TailScale installed to easily access it over Wireguard, there&amp;rsquo;s a TailScale package. In general it&amp;rsquo;s probably easier and safer to do things their way.&lt;/p&gt;
&lt;h3 id="enabling-ssh"&gt;Enabling SSH&lt;/h3&gt;
&lt;p&gt;Before you can SSH into the Synology, you need to enable the SSH service. This is straightforward with the web interface. In &lt;em&gt;Control Panel&lt;/em&gt;, look for &lt;em&gt;Terminal &amp;amp; SMNP&lt;/em&gt; and tick the box, and click &lt;em&gt;Apply&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-6.01.35-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="home-directory"&gt;Home directory&lt;/h3&gt;
&lt;p&gt;If you SSH to the Synology now, it works, but you&amp;rsquo;ll notice that there&amp;rsquo;s a warning message saying &amp;ldquo;Could not chdir to home directory /var/services/homes/&lt;user name&gt;: No such file or directory&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-7.12.36-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The reason for this is that unlike most distros, when you create a user in DSM, there&amp;rsquo;s no home directory created for them. There must be some bash config somewhere since I get that nice prompt, but no user home directory.&lt;/p&gt;
&lt;p&gt;Lot&amp;rsquo;s of times, you could just ignore that warning, you can still probably do what you wanted to, but it is going to be an issue for installing SSH keys - when you do the &lt;code&gt;ssh-copy-id&lt;/code&gt; it will want to create a .ssh file in the user&amp;rsquo;s home directory, and if they haven&amp;rsquo;t got one, that is not going to work. You&amp;rsquo;ll get a similar sort of error saying something like&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;sh: line 0: cd: /var/services/homes/&amp;lt;user_name&amp;gt;: No such file or directory mkdir: cannot create directory '.ssh': Permission denied&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Again, there&amp;rsquo;s a setting in the web interface to create home directories for the users. We&amp;rsquo;re in the &lt;em&gt;Control Panel&lt;/em&gt; again, but this time look for &lt;em&gt;User &amp;amp; Group&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-6.20.35-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Tick the box for &lt;em&gt;Enable user home service&lt;/em&gt;, and hit &lt;em&gt;Apply&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Now you&amp;rsquo;ll be able to copy the keys as usual with ssh-copy-id.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-7.25.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-7.25.59-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>rsync basics</title><link>https://blog.iankulin.com/rsync-basics/</link><pubDate>Sun, 26 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-basics/</guid><description>&lt;p&gt;I&amp;rsquo;ve started down the path of improved storage management, including embracing the &lt;a href="https://www.backblaze.com/blog/the-3-2-1-backup-strategy/"&gt;3-2-1&lt;/a&gt; mantra. I&amp;rsquo;ve settled on a RAID6 NAS for local, mirrored to an off-site NAS, and an offline local USB drive.&lt;/p&gt;
&lt;p&gt;While I&amp;rsquo;ve been setting those up, my services have been live, so files have been changing on my main storage, which I&amp;rsquo;ve then switched to the bigger NAS, and I&amp;rsquo;ve been trying to keep data in sync by remembering what changes have been made where, and manually replicating them. That&amp;rsquo;s not sustainable and not the plan.&lt;/p&gt;
&lt;h3 id="beyond-compare"&gt;Beyond Compare&lt;/h3&gt;
&lt;p&gt;Many years ago, on Windows, I paid for a file/text comparison app called &lt;a href="https://www.scootersoftware.com/features.php"&gt;Beyond Compare&lt;/a&gt; - it says on their website that these are perpetual licences, and I&amp;rsquo;ve had many years of value out of it. I think I bought it about the same time I dabbled in version control thanks to the excellent book Code Complete (&lt;a href="https://www.amazon.com.au/Code-Complete-Steve-McConnell/dp/0735619670"&gt;this link&lt;/a&gt; is to the second version, but I&amp;rsquo;ve had both over the years).&lt;/p&gt;
&lt;p&gt;If you have to do manual syncing jobs between different directories or machines, this is an excellent graphical tool to do that with. I have never tried any other software because it&amp;rsquo;s always just done exactly what I needed with no hassle. Highly recommend.&lt;/p&gt;
&lt;p&gt;However, doing things manually is no way to do backups - you need a setup that runs without human intervention so it actually gets done.&lt;/p&gt;
&lt;h2 id="rsync"&gt;rsync&lt;/h2&gt;
&lt;p&gt;I never fail to be amazed at the substantial but unassuming command line tools from the Linux world, and this is another one. &lt;code&gt;rsync&lt;/code&gt; is perfect for this specific job - of keeping my remote backup in sync with the production storage. Here&amp;rsquo;s a few examples of how it works.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s say I&amp;rsquo;ve got two directories in my home folder called localdir and remotedir. localdir has some files in it and I want them copied to the remotedir. &lt;code&gt;rsync&lt;/code&gt; can do that for us with this command. The &lt;code&gt;-a&lt;/code&gt; flag does a few things, including recursing into directories and preserving some of the file attributes when it copies files. I found if I didn&amp;rsquo;t do this, and just used -&lt;code&gt;r&lt;/code&gt; for the recursion, rsync&amp;rsquo;s system for checking for changes didn&amp;rsquo;t work.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -a localdir/ remotedir
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.13.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Okay, that&amp;rsquo;s not impressive, I could have done the same thing with &lt;code&gt;cp&lt;/code&gt; to copy those files. And actually I could do that for my remote backup as well, except there&amp;rsquo;s no point burning up resources for copying identical files on top of each other. Really we just want to copy the files that have changed, or new files that have been added.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s try that by editing &lt;code&gt;file1.txt&lt;/code&gt;, and adding a new &lt;code&gt;file0.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.18.57-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You might be thinking that perhaps rsync really is just copying all the files again. We can add a couple more flags to get rsync to tell us what is getting copied (&lt;code&gt;-v&lt;/code&gt; for verbose, and &lt;code&gt;-i&lt;/code&gt; which gives some output explaining how it decided a file needed copied.&lt;/p&gt;
&lt;p&gt;Without making any changes, let&amp;rsquo;s re-run the rsync with those flags. We shouldn&amp;rsquo;t see any files updated.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.24.51-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So no files transferred, but if we edit a one and change the timestamp on another one, that should trigger a couple of files to be copied.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.27.48-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The optimisation in rsync extends further than just copying the files that have changed, the &lt;a href="https://download.samba.org/pub/rsync/rsync.1"&gt;man page&lt;/a&gt; says &amp;ldquo;&lt;em&gt;It is famous for its delta-transfer algorithm, which reduces the amount of data sent over the network by sending only the differences between the source files and the existing files in the destination.&lt;/em&gt;&amp;rdquo;&lt;/p&gt;
&lt;h3 id="deletions"&gt;Deletions&lt;/h3&gt;
&lt;p&gt;So that covers copying most of the changes in the source, but what about if a local file is deleted, does that propagate to the destination and delete the file there? Naturally, there is also an option for this; simply add &lt;code&gt;--del&lt;/code&gt; to the end of the command:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-3.35.49-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="remote-hosts"&gt;Remote hosts&lt;/h3&gt;
&lt;p&gt;So far, all of these examples have been between directories on a single instance. What about on a remote machine? There&amp;rsquo;s a couple of steps.&lt;/p&gt;
&lt;p&gt;First, the machine where the rsync is being executed must have ssh access to the other machine. This is just the usual ssh setup - &lt;code&gt;ssh-keygen&lt;/code&gt; some keys if you don&amp;rsquo;t have any, then copy them over with &lt;code&gt;ssh-copy-id&lt;/code&gt; and we&amp;rsquo;re ready to go&lt;/p&gt;
&lt;p&gt;The second part is to add an ssh like address to the remote directory. So instead of just&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -avi localdir/ remotedir --del
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;it will be&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -avi localdir/ ian@192.168.100.33:remotedir --del
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Before I&amp;rsquo;ve run this, I&amp;rsquo;ve sshed in and created the directory &lt;code&gt;remotedir&lt;/code&gt; on the target machine, but then it&amp;rsquo;s simple as&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-5.06.01-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="reading"&gt;Reading&lt;/h3&gt;
&lt;p&gt;To figure all this out, I&amp;rsquo;ve leaned heavily on this tutorial &amp;ldquo;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-use-rsync-to-sync-local-and-remote-directories"&gt;How To Use Rsync to Sync Local and Remote Directories&lt;/a&gt;&amp;rdquo; from Digital Ocean, and for the stuff about deleting remote files when they are deleted locally, on &lt;a href="https://askubuntu.com/questions/476041/how-do-i-make-rsync-delete-files-that-have-been-deleted-from-the-source-folder"&gt;this Stack Exchange&lt;/a&gt; question.&lt;/p&gt;
&lt;p&gt;The source of truth, is the surprisingly readable &lt;a href="https://download.samba.org/pub/rsync/rsync.1"&gt;man page&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>CPU Comparisons</title><link>https://blog.iankulin.com/cpu-comparisons/</link><pubDate>Fri, 24 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/cpu-comparisons/</guid><description>&lt;img src="https://blog.iankulin.com/images/cloud.jpg" width="800" alt=""&gt;
&lt;p&gt;When I was a young whipper-snapper, working at the &amp;ldquo;data processing&amp;rdquo; centre, you could see if one CPU was better than another one by the CPU name/number. No one wanted an 8086 once the 286&amp;rsquo;s came out. Then a 386 was what you wanted for the latest multitasking support, but only till the 486 was available, then you wanted that for the gargantuan memory addressing.&lt;/p&gt;
&lt;p&gt;With that idea firmly in mind, I&amp;rsquo; wanted an i5 to be better than an i3, and an i7 better than all of them, but it&amp;rsquo;s &lt;a href="https://www.makeuseof.com/tag/compare-different-cpus-right-way/"&gt;apparently not that simple&lt;/a&gt;. I do come across people in forums talking about &amp;lsquo;generations&amp;rsquo; of Intel processors - so all this is probably decodable, but I&amp;rsquo;m not exactly sure how.&lt;/p&gt;
&lt;p&gt;Luckily, there are some handy CPU comparison sites like &lt;a href="http://versus.com"&gt;versus.com&lt;/a&gt;. I was looking at it tonight to try and decide if my new i5 6500T is better than my i7 6700T. Whichever one is best will get a RAM upgrade, and be the boss node in my cluster and run all my self-hosted services.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-21-at-8.45.04-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Spoiler alert - it turns out the i7 is not 2 better than the i5, they are almost identical except the i7 has double the threads (because Hyperthreading?) - a not insignificant different for a machine that&amp;rsquo;s running several VM&amp;rsquo;s&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-21-at-8.49.52-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-21-at-8.49.52-pm.png" width="787" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>HP Secure Boot Pain</title><link>https://blog.iankulin.com/hp-secure-boot-pain/</link><pubDate>Thu, 23 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hp-secure-boot-pain/</guid><description>&lt;p&gt;Since the HP EliteDesk 800 G1 I&amp;rsquo;m using as a dev/homelab machine is going to be re-purposed as a media/backup server elsewhere, I&amp;rsquo;ve grabbed another G2 to use as a second box. The homelab machine serves as a backup device for the production server that runs my self-hosted services, but also is the machine I play with - testing my software, but also trying out any new self-hosted software I&amp;rsquo;m having a look out or configurarions I&amp;rsquo;m thinking about for the &amp;lsquo;production&amp;rsquo; server.&lt;/p&gt;
&lt;p&gt;Normally, installing Proxmox is pretty routine for me, but the newer G2&amp;rsquo;s have a Secure Boot &amp;lsquo;feature&amp;rsquo; which is probably super secure for installing Windows from the restore partition, but a pain for installing Proxmox from a USB. You&amp;rsquo;ll know this is the issue if you are trying to boot to the USB and getting the message &lt;code&gt;Selected boot image did not authenticate.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_4416.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you Google this error there&amp;rsquo;s good advice to turn it off in the BIOS settings (it&amp;rsquo;s on the &lt;code&gt;Advanced&lt;/code&gt; page by setting &lt;code&gt;Secure Boot&lt;/code&gt; to &lt;code&gt;Legacy Support Enable and Secure Boot Disable&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_4418.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;d probably think now would be the time for the old F10 Save &amp;amp; Exit, then start mashing the F9 (boot device) key while it reboots. At least, that&amp;rsquo;s what I thought, but every time I&amp;rsquo;d just get that same error, and if I went back into the BIOS, Secure Boot was somehow turned back on.&lt;/p&gt;
&lt;p&gt;When I googled &lt;em&gt;that&lt;/em&gt; problem, several responses in HP forums from HP support mentioned setting a BIOS password in order to be able to change some BIOS settings like secure boot. I tried, but it wouldn&amp;rsquo;t let me set the password. This was actually a blessing in disguise since this wasn&amp;rsquo;t the problem that was stopping me from turning secure boot off.&lt;/p&gt;
&lt;p&gt;I was right to be saving the settings and exiting with F10, but then instead of mashing buttons during what I thought was the reboot, I should have waited for a different message asking me to type in a number shown on the screen to confirm the change. I&amp;rsquo;m not sure what the purpose of this message is - perhaps it&amp;rsquo;s a &amp;lsquo;confirm you&amp;rsquo;re a human&amp;rsquo; thing. As you type in the numbers, they don&amp;rsquo;t appear, you just have to trust you&amp;rsquo;re doing it correctly. Once that&amp;rsquo;s done, you&amp;rsquo;ll be able to reboot, mach the F9, select the USB, and you&amp;rsquo;re on your way to Proxmox land.&lt;/p&gt;
&lt;h3 id="bonus-problem"&gt;Bonus Problem&lt;/h3&gt;
&lt;p&gt;If you get the message &lt;code&gt;No support for KVM virtualization detected&lt;/code&gt; from Proxmox.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4419.jpg" width="560" alt=""&gt;
&lt;p&gt;Then follow its advice. Head into the BIOS settings and look for Virtualization Technology (VTx) and turn it on.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4420.jpg" width="697" alt=""&gt;</description></item><item><title>Mounting one Synology NAS to another one</title><link>https://blog.iankulin.com/mounting-one-synology-nas-to-another-one/</link><pubDate>Tue, 21 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/mounting-one-synology-nas-to-another-one/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_4344.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I went over mounting a Synology NAS share on a Mac or Linux host &lt;a href="https://blog.iankulin.com/accessing-a-synology-nas-from-linux/"&gt;a while ago&lt;/a&gt;. Now I&amp;rsquo;ve populated a new NAS, and I want to copy my data over to it. I could mount them both to my laptop, and the data flow would look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;NAS1 - switch - wifi - laptop - wifi - switch - NAS2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since I&amp;rsquo;m copying 4TB, it will take a few hours, and if I forget what&amp;rsquo;s going on and close the laptop, or take it outside of my wifi the transfer will die, and I won&amp;rsquo;t be sure which files are patent. What might be better would be something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;NAS1 - switch - NAS2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is eminently possible, we just need to mount a share from one NAS into the other, then I can just initiate the copy and bug out, leaving it them to do their thing.&lt;/p&gt;
&lt;p&gt;DSM - the Synology operating system, is Linux, so in theory I could just SSH in and mount the other NAS with the &lt;code&gt;mount&lt;/code&gt; command, or by editing &lt;code&gt;/etc/fstab&lt;/code&gt;, but DSM is highly customised and slimmed down, and somethings are just outright different, sometimes in a dangerous way.&lt;/p&gt;
&lt;p&gt;On the flipside, Synology&amp;rsquo;s whole reason for existing is to make things easier and GUI-ier, so there&amp;rsquo;s no need to drop into the command line, we can do what we want quite easily via their web interface.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll start out on NAS2 (the empty one). Using FileStation, I&amp;rsquo;ll create a share, then inside that, I&amp;rsquo;ll create a &lt;code&gt;nas1&lt;/code&gt; directory - this will be the mount point. In the root of the share, &lt;code&gt;Open Tools | Mount Remote Folder&lt;/code&gt;. I&amp;rsquo;m using SMB/CIFS for my share so I&amp;rsquo;ll chose CIFS option, but obviously if you&amp;rsquo;re using NFS, use that instead.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.03.08-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.03.08-pm.png" width="1008" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since underneath it&amp;rsquo;s actually running the &lt;code&gt;mount&lt;/code&gt; for us, or doing whatever their equivalent of editing &lt;code&gt;/etc/fstab&lt;/code&gt; is, it needs all the same information.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.04.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.04.45-pm.png" width="1007" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once you press &lt;code&gt;Mount&lt;/code&gt;, the other (remote) NAS will appear in FileStation under &lt;code&gt;Remote Folder&lt;/code&gt;. The it was just a matter of &lt;code&gt;right click | copy&lt;/code&gt; on the NAS1 then &lt;code&gt;paste&lt;/code&gt; into the NAS2 share.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.05.43-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.05.43-pm.png" width="1009" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;According to &lt;a href="https://datarecovery.com/rd/does-hard-drive-rpm-affect-lifespan/"&gt;these guys&lt;/a&gt;, the theoretical max transfer rate of a 5400RPM drive (which is what I&amp;rsquo;m running in both NASs) is 75MB/s. NAS1 that I&amp;rsquo;m copying from is RAID1, so I assume if the OS is smart enough it could pull data from both disks at once, but I don&amp;rsquo;t really understand what happens with the writes when that data hits the RAID6. In any case, it was maxing out at around 70MB/s. Although most of the time it was anywhere between 30 and 70.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.55.58-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-17-at-5.55.58-pm.png" width="1008" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Proxmox VM Memory Upgrade</title><link>https://blog.iankulin.com/proxmox-vm-memory-upgrade/</link><pubDate>Sun, 19 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-vm-memory-upgrade/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.36.10-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I ordered some RAM this week for my production server - it&amp;rsquo;s quickly becoming clear that memory is the limiting factor when running lots of services and VM&amp;rsquo;s that don&amp;rsquo;t get much use - rather than processing power. I&amp;rsquo;m not really a hardware guy, so figuring out exactly what RAM I need is a slightly fraught process - I won&amp;rsquo;t be fully confident I&amp;rsquo;ve ordered the right thing until I install it, boot up, and see my &lt;a href="https://support.hp.com/us-en/product/hp-elitedesk-800-35w-g2-desktop-mini-pc/7633266/document/c04816235"&gt;G2 800&lt;/a&gt; come to life maxed out at 32GB.&lt;/p&gt;
&lt;p&gt;Something that&amp;rsquo;s not fraught however, is upgrading the RAM in a virtual machine (VM) running under &lt;a href="https://www.proxmox.com/en/proxmox-ve"&gt;Proxmox&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="ram-hunger"&gt;RAM Hunger&lt;/h3&gt;
&lt;p&gt;I run two VM&amp;rsquo;s full time on the production node - a general docker host for a variety of small services, and a separate VM for &lt;a href="https://jellyfin.org/"&gt;Jellyfin&lt;/a&gt;. I&amp;rsquo;d allocated 6GB for this VM, but when I checked tonight ProxMox was reporting that 5GB was already being used.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.16.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.16.57-pm.png" width="974" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have noticed that the Jellyfin memory usage seems to slowly grow over time. That might be related to my current usage pattern - I&amp;rsquo;m frequently re-scanning the libraries as I check and update the metadata.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.17.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In any case, it needs more RAM, and I&amp;rsquo;ve got some up my sleeve on this physical machine so let&amp;rsquo;s allocate some more to the Jellyfin VM.&lt;/p&gt;
&lt;p&gt;Normally, you specify the amount of RAM to allocate when you&amp;rsquo;re creating the machine, but it&amp;rsquo;s quite straightforward to change it afterwards. With your VM selected, click into the &amp;ldquo;Hardware&amp;rdquo; page. Then if you double click on &amp;ldquo;Memory&amp;rdquo; a dialogue will open up to&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.18.19-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can just edit this number, in MB. Once you OK it, there will be two values listed for memory in the Hardware specs. The first is what the VM is running with now, and the second, orange value is what you are changing it to. In my case, I&amp;rsquo;ve bumped it up to 8GB from 6.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.19.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not possible to change the memory dynamically - it requires a reboot. Of course, rebooting the machine also restarts Jellyfin, so after the reboot we have plenty of headroom.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.58.21-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>No DNS on Proxmox machine</title><link>https://blog.iankulin.com/no-dns-on-proxmox-machine/</link><pubDate>Fri, 17 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/no-dns-on-proxmox-machine/</guid><description>&lt;p&gt;I had some more network weirdness setting up this new Proxmox machine. When I went to run the updates it couldn&amp;rsquo;t resolve any of the addresses:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve-kr01:~# apt update
Err:1 http://ftp.au.debian.org/debian bullseye InRelease
 Temporary failure resolving &amp;#39;ftp.au.debian.org&amp;#39;
Err:2 http://download.proxmox.com/debian/pve bullseye InRelease
 Temporary failure resolving &amp;#39;download.proxmox.com&amp;#39;
Err:3 http://security.debian.org bullseye-security InRelease
 Temporary failure resolving &amp;#39;security.debian.org&amp;#39;
Err:4 https://enterprise.proxmox.com/debian/pve bullseye InRelease
 Temporary failure resolving &amp;#39;enterprise.proxmox.com&amp;#39;
Err:5 http://ftp.au.debian.org/debian bullseye-updates InRelease
 Temporary failure resolving &amp;#39;ftp.au.debian.org&amp;#39;
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
All packages are up to date.
W: Failed to fetch http://ftp.au.debian.org/debian/dists/bullseye/InRelease Temporary failure resolving &amp;#39;ftp.au.debian.org&amp;#39;
W: Failed to fetch http://ftp.au.debian.org/debian/dists/bullseye-updates/InRelease Temporary failure resolving &amp;#39;ftp.au.debian.org&amp;#39;
W: Failed to fetch http://download.proxmox.com/debian/pve/dists/bullseye/InRelease Temporary failure resolving &amp;#39;download.proxmox.com&amp;#39;
W: Failed to fetch http://security.debian.org/dists/bullseye-security/InRelease Temporary failure resolving &amp;#39;security.debian.org&amp;#39;
W: Failed to fetch https://enterprise.proxmox.com/debian/pve/dists/bullseye/InRelease Temporary failure resolving &amp;#39;enterprise.proxmox.com&amp;#39;
W: Some index files failed to download. They have been ignored, or old ones used instead.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So some sort of DNS problem. The entry for the DNS is in &lt;code&gt;/etc/resolv.conf&lt;/code&gt; when I looked in there, it said:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;search local
nameserver 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Well, that does not seem great. I feel like it should be pointing at the DNS in my router, or even upstream at my ISP or google&amp;rsquo;s DNS server. Before I dive in and start editing, I thought I&amp;rsquo;d check my other servers. The first one has clearly been altered as part of installing TailScale, so that wasn&amp;rsquo;t much help, but on the dev machine it said:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;search local
nameserver 192.168.100.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which is more like what I was expecting, that&amp;rsquo;s the address given out by my DHCP server for DNS. I could just edit the new machine to this, but since this is the third lot of network related weirdness related to this install (the second was that my managed switch&amp;rsquo;s web interface was down, and I couldn&amp;rsquo;t ping it, but it was still passing traffic, &lt;a href="https://blog.iankulin.com/netgear-gs108e-switch-problem/"&gt;again&lt;/a&gt;), and the first, discussed in yesterday&amp;rsquo;s post, was that DHCP had provided a dynamic address that was already assigned to another device.&lt;/p&gt;
&lt;p&gt;I swapped out the network cable, and noticed the port lights flashing. Perhaps there is a broken pair in the other cable? It was odd that it was working sort of.&lt;/p&gt;
&lt;p&gt;I reinstalled Proxmox from scratch, and carefully watched the console messages and checked all the network settings (it correctly picked up the reserved address and correct DNS server). Then everything worked.&lt;/p&gt;</description></item><item><title>Proxmox Dynamic IP</title><link>https://blog.iankulin.com/proxmox-dynamic-ip/</link><pubDate>Thu, 16 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-dynamic-ip/</guid><description>&lt;p&gt;I ran into a little hiccup today. I&amp;rsquo;m building out a Jellyfin media server in a little HP G2 Mini PC. The config was going to be a Debian server inside Proxmox (because I love VM snapshots for backups) running Jellyfin in a container. There&amp;rsquo;ll be an external USB3 hard drive for the media storage.&lt;/p&gt;
&lt;p&gt;I was intending to build it all out and test it, then ship it to it&amp;rsquo;s final home.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve probably installed Proxmox five or six times by now since I&amp;rsquo;m always playing around with my test machine, and never really thought about the screen that comes up during the install showing the network details it&amp;rsquo;s picked up from DHCP.&lt;/p&gt;
&lt;p&gt;Today once I&amp;rsquo;d finished installing Proxmox, I couldn&amp;rsquo;t SSH into it&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-12-at-3.57.03-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I knew I had the right IP address since it shows that on the console at the end of the boot process. Looking in my router, it said 192.168.100.2 was connected, but by wifi on the SSID I use for IOT devices.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-12-at-4.01.09-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That ESP device name is a giveaway - it&amp;rsquo;s one of my wifi light bulbs. A quick &lt;code&gt;ip addr&lt;/code&gt; on the new Proxmox via the console shows it is convinced that it is 192.168.100.2 I can ping 8.8.8.8 from it, but DNS is not working. My conclusion is that I&amp;rsquo;ve got two devices with the same IP on my network.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure how this came about. The network cable I was using is an old CAT5 with the clips broken off both ends I use for fiddling around, so perhaps there was a dodgy connection right at a crucial moment? It seems odd. usually when I encounter the &amp;rsquo;two machines with the same IP&amp;rsquo; problem, I&amp;rsquo;ve caused it somehow.&lt;/p&gt;
&lt;p&gt;No problem &lt;em&gt;I thought&lt;/em&gt;, now I&amp;rsquo;ve got the MAC address from the Proxmox machine, I&amp;rsquo;ll just reserve an available IP address for it. I did that, and rebooted Proxmox, but it was still on the old address. Then I remembered that question during the install process - it must collect an address from DHCP, then after the users has committed to it, write it into &lt;code&gt;/etc/hosts&lt;/code&gt; and &lt;code&gt;/etc/network/interfaces&lt;/code&gt; I reinstalled Proxmox, it picked up the new address and I saved it as the static IP.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not really problem solved though - I&amp;rsquo;m sending this off to a network where I don&amp;rsquo;t know the network configuration. I was hoping just to let it pick up a DHCP address that would remain somewhat stable since the machine is going to be on 24/7.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not unreasonable for Proxmox to expect a VM host machine is going to have a static IP address, but it&amp;rsquo;s inconvenient for this situation. I&amp;rsquo;ll have to discover how to make it dynamic (probably by editing those two files). I&amp;rsquo;ll have Tailscale on it, so I can remote in afterwards to make it static, although without also reserving it in their router that carries a small risk too.&lt;/p&gt;</description></item><item><title>Nostalgia</title><link>https://blog.iankulin.com/nostalgia/</link><pubDate>Tue, 14 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nostalgia/</guid><description>&lt;p&gt;I&amp;rsquo;m not super interested in FreeDOS, but did enjoy this video from Jim Hall since I lived through all this, and was working in IT (well, &amp;lsquo;data processing&amp;rsquo; actually) during the introduction of the IBM PC.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/3E5Hog5OnIM?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;My first DOS was 2.11, but spent a lot more time on 3.12, and later 4.01. Windows wasn&amp;rsquo;t really ready for anyone until 3.1 which is when I dived in there. I seem to remember purchasing a PC with a whole megabyte of RAM in anticipation!&lt;/p&gt;
&lt;p&gt;Jim mentions AsEasyAs in the video, which was a &amp;lsquo;shareware&amp;rsquo; spreadsheet I used extensively at home (work had Lotus 123 - AsEasyAs was a play on that name).&lt;/p&gt;
&lt;p&gt;It couldn&amp;rsquo;t conceivably serve any useful purpose, but I do have a small hankering to find the executables on old drives and see if I can set up my &lt;a href="https://en.wikipedia.org/wiki/Clipper_(programming_language)"&gt;Clipper&lt;/a&gt; environment and compile some of my first commercial software.&lt;/p&gt;
&lt;p&gt;I do wish I had a few screenshots from over the years - you never know what you&amp;rsquo;re going to enjoy looking back on. For example, I&amp;rsquo;ve spent hundreds of hours over the last few years in various versions of the Wordpress editor and never thought to record that.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-12-at-1.28.53-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>NAS Storage Calculations</title><link>https://blog.iankulin.com/nas-storage-calculations/</link><pubDate>Sat, 11 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nas-storage-calculations/</guid><description>&lt;p&gt;I&amp;rsquo;ve been really happy with my two bay Synology NAS - a DS216j. The Synology&amp;rsquo;s seem to have great reputation for just pushing on. Mine is loaded up with two 8TB Seagate Barracudas in RAID 1 leaving me with a one drive failure redundancy.&lt;/p&gt;
&lt;p&gt;I guess a more hard-core host-er than me would be building their own array and using Unraid or ZFS or something. I&amp;rsquo;m pretty comfortable with the Synology off the shelf system; it&amp;rsquo;s a good match for my (low) level of expertise, and more robust than my previous storage system of a USB external drive.&lt;/p&gt;
&lt;p&gt;As I start to move real world applications out of the cloud and on to self-hosting, I need to be serious about availability and data security. The general standard for this in the self-hosting community seems to be three versions of data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;production version&lt;/li&gt;
&lt;li&gt;local backup&lt;/li&gt;
&lt;li&gt;remote backup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I feel like the local and remote backups don&amp;rsquo;t &lt;em&gt;have&lt;/em&gt; to be NAS - a large enough external drive might be a reasonable cost saving. I would like the local backup to be able to be swapped into production though, so even if it was an external USB drive and would provide a degraded service, it should be able to be used to maintain services.&lt;/p&gt;
&lt;p&gt;A few days ago, there was an 8 bay, hot swappable Synology on eBay that got me a bit excited thinking about running different pools with a variety of RAIDs or just packing it with low cost smaller HDDs. Luckily I didn&amp;rsquo;t win it, but it triggered me to think about exactly what I need and what the trade-offs are.&lt;/p&gt;
&lt;h3 id="drive-quality"&gt;Drive Quality&lt;/h3&gt;
&lt;p&gt;The drives I&amp;rsquo;ve got in my NAS are second-hand, brand name non-NAS drives. They had just under 9000 hours on them, and the company selling them had hundreds of identical drives. From this, I&amp;rsquo;m assuming they came out of a data centre who replace drives at the one year mark.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to buy &amp;ldquo;NAS&amp;rdquo; drives (and also &amp;ldquo;surveillance&amp;rdquo; drives) which are sold for uses where they are in use 24/7. They cost more.&lt;/p&gt;
&lt;p&gt;Is it possible these two types of drives (and perhaps even the USB external drives) are all the same drives, just marketed differently? Well, it&amp;rsquo;s possible. But it&amp;rsquo;s also possible they are mechanically identical, but have been sent to different market segments based on their initial test results.&lt;/p&gt;
&lt;h3 id="raid"&gt;RAID&lt;/h3&gt;
&lt;p&gt;RAID is a way of combining physical disks into one logical volume, usually in a way that reduces the capacity but allows for a drive failure without data loss. There&amp;rsquo;s several different &amp;rsquo;levels&amp;rsquo; of RAID. My current setup is RAID1 - I have two 8TB disks which present to the system as a single 8TB disk, but if one drive fails, I still have access to all of my data. If you have more than a couple of disks, RAID5 is a better option - if you had 3 x 8TB drives, you&amp;rsquo;d end up with 16TB usable space, and still be able to tolerate one drive failure. If you&amp;rsquo;re super cautious, RAID6 will allow two drive failures before you&amp;rsquo;re in danger. Of course this comes at a cost, if we had a 4x 8TB drive setup, there&amp;rsquo;d only be 16TB available, but any two of the drives could die without stopping the system.&lt;/p&gt;
&lt;h3 id="scoping-out-the-options"&gt;Scoping out the options&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve decided I need about 12TB - I currently have about 3TB of media locally and 0.5TB of general data on my laptop, that&amp;rsquo;s backed up to an external drive about weekly, along with about 20GB in DropBox. The bottom Dropbox plan is about AUD190 for 2TB, and I&amp;rsquo;m only using a fraction of it, so as part of my self-hosting that will get canned. 12TB seems like a lot of headroom from 4TB which is about where I&amp;rsquo;m sitting. I&amp;rsquo;d like to offer a couple of TB to whichever relative ends up hosting my remote backup. And finally it&amp;rsquo;s a multiple of 6TB which is a common ex-enterprise second hand drive size on ebay, so I know I can get year old ones for about $100&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll run through the thinking of each of the options I&amp;rsquo;ve considered.&lt;/p&gt;
&lt;h3 id="ds412"&gt;DS412+&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-10-at-5.14.28-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I had a couple of Synologys with more bays than this in my eBay watchlist, but as you add drives, you add power consumption and heat, so I think realistically at the 12TB point, 4 bays is the most you could justify. There&amp;rsquo;s a bit of a price step up as well when you go to five bays and leave the serious home user segment of the market.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s worth mentioning how the Synology model numbers work in the DS range. My existing unit is a DS216j. The 2 is for two bays and it&amp;rsquo;s 2016 model. So the DS412+ is from 2012 and has four bays. The &amp;lsquo;j&amp;rsquo; on mine denotes its a mouse-power CPU - in this case a Marvell Armada 385 - some sort of low power ARM. The DS412+ is rocking a bigger mouse - the Intel Atom D2700, and it has a bit more RAM.&lt;/p&gt;
&lt;p&gt;The processor is not a big deal to me. Some folk host a lot of apps - media servers etc on their NAS. I&amp;rsquo;m not planning to do that. As long as it can run Tailscale in a container (which the &amp;lsquo;j&amp;rsquo; models can) we&amp;rsquo;re good to go.&lt;/p&gt;
&lt;p&gt;My theory with drive quality is that the lower the quality of the drive, the higher level RAID I need. So If I scope this unit out with the $100 used, non-NAS drives, I can install four disks as RAID6, have two fail on the same day, and still be operational.&lt;/p&gt;
&lt;p&gt;This second-hand (about ten years old) unit was on &amp;ldquo;buy now&amp;rdquo; for $416, the four year old drives adds $440 making it $856 unit - around $71/TB. The quoted power draw is 44W which works out at 3.7W/TB - the highest of everything I considered.&lt;/p&gt;
&lt;h3 id="ds420j"&gt;DS420j&lt;/h3&gt;
&lt;p&gt;Thinking about likely points of failure with the eleven year old NAS (if I used the DS412j) made me wonder if a unit failure might be higher on the probability list than a second-hand drive failure. The answer is who knows? - probably both events are quite unlikely. However I have some redundancy built in to the drives, but a single point of failure in the NAS unit. It made me wonder what a new 4 bay Synology costs, and the answer is not much more.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/27459.jpg" width="229" alt=""&gt;
&lt;p&gt;This DS420j is again 4 bays, and like the older 4 nay unit it&amp;rsquo;s hot-swapable. This means that in the event of a drive failure, you can leave the NAS running, remove the faulty drive and insert a new one. The unit will then (slowly) rebuild the RAID array, but while you are removing a drive and rebuilding the RAID the system is still fully operational.&lt;/p&gt;
&lt;p&gt;Listed at $439, that works out to $880 total if I used the same second-hand 6TB drives as in the calculation above. So with my RAID 6 (two drive failures can be tolerated without losing data) the cost per TB is $73 - only a couple more than in the first example.&lt;/p&gt;
&lt;p&gt;Apart from the peace of mind of running a newer unit, there&amp;rsquo;s a big difference in power consumption. The DS420j uses 44W, this one is 22W with all the drives spinning, or if you allow them to hibernate as low as 8W. So the max power burn is half at 1.8W per TB&lt;/p&gt;
&lt;h3 id="trading-raid"&gt;Trading RAID&lt;/h3&gt;
&lt;p&gt;RAID 6 - where I&amp;rsquo;m installing 4 x 6TB drives, and only ending up with 12TB of usable space is very conservative - and I was doing that because I was using the second hand drives. What if I bought cheap, but still brand name HDDs, but only three of them and configured as RAID 5 so I&amp;rsquo;d still get 12TB of usable disk, and a single drive can fail without affecting my data?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-10-at-5.40.03-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-10-at-5.40.03-pm.png" width="239" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There are no three bay Synologys, so I&amp;rsquo;d use the same NAS as above - the DS420j. I can buy three Seagate Skyhawk 6TB disks for $660, so the total comes to $1100 or $92/TB - quite a bit more than four of the older drives in RAID6. With less drives spinning, we can probably assume a total power consumption of around 15W - 1.25W/TB&lt;/p&gt;
&lt;h3 id="even-less-disks"&gt;Even less disks&lt;/h3&gt;
&lt;p&gt;What if we reduce the number of disks even further? If we double the drive capacity to 12TB we could run 2 x 12TB drives as RAID1, have a smaller NAS, save some power, and still have a single drive fail with no data loss. We might want to go to an even higher quality of drive, perhaps one of the ones rated for NAS use - The cheapest new brand name 12TB NAS drive on eBay was a Seagate IronWolf. Two of those costs $756. The two bay DS220j NAS only adds $241 for a total of $997 - $83/TB. Power looks great at 1.1W/TB.&lt;/p&gt;
&lt;p&gt;These smaller NAS&amp;rsquo;s are not hot-swappable. You have to power down the NAS to replace a drive. This is not as cool as just clicking a button and sliding a drive out while all your services are still up, but it&amp;rsquo;s not really a significant factor in my decision making.&lt;/p&gt;
&lt;h3 id="disk-singular"&gt;Disk singular&lt;/h3&gt;
&lt;p&gt;I wouldn&amp;rsquo;t do this for my production storage, but it&amp;rsquo;s worth mentioning the single disk NAS. You&amp;rsquo;d want the best quality of drive possible, and probably schedule to swap it out after three years or so. A Synology single drive NAS with a NAS rated drive is a big step up in quality and convenience from an external USB drive. With that same IronWolf 12TB drive and a DS120j you&amp;rsquo;d be out of pocket $578 or $48/TB, and power is down to 0.83W/TB.&lt;/p&gt;
&lt;h3 id="ye-olde-usb-drive"&gt;Ye Olde USB Drive&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve not had a USB drive failure, but mine generally live a happy life powered down in a cool dry drawer until they are fished out for a backup session. Just by way of a comparison to the options above, a WD &amp;ldquo;Elements&amp;rdquo; USB drive costs the same as the single disk NAS at $580 ($48/TB) but the power is down at 0.67W/TB. The cheaper Seagate &amp;ldquo;One Touch Desktop Hub&amp;rdquo; drive works out at $35/TB and 0.83W/TB&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;NAS&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;Disks&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;$/TB&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;W/TB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS412+&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;4 x 6TB used&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$856&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;71&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;3.7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS420j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;4 x 6TB used&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$880&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;73&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1.8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS420j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;3 x 6TB new&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$1,100&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;92&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1.25&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS220j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;2 x 12TB NAS&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$997&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;83&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1.1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS120j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1 x 12TB NAS&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$578&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;48&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;0.83&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;WD Elements&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1 x 12TB USB&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$580&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;48&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;0.67&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;Seagate&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1 x 12TB USB&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$423&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;35&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;0.83&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Clearly out of the first two options, you&amp;rsquo;d choose the second. $2 extra per TB of storage is easily worth it to start with a new NAS compared with an eleven year old one. After that the price per TB doesn&amp;rsquo;t go down till you hit the single drive devices.&lt;/p&gt;
&lt;p&gt;Someone else may well start with different assumptions that I have made here, especially in the way I&amp;rsquo;ve decided to increase the drive quality as I&amp;rsquo;ve reduced the redundancy. For instance, you may be happy with a couple of second hand 12TB drives in a DS220j at $54/TB instead of rooting for new NAS drives. This would be along the lines of my original purchase of a DS216j and two 8TB second hand drives for $58/TB.&lt;/p&gt;
&lt;h2 id="the-plan"&gt;The Plan&lt;/h2&gt;
&lt;p&gt;Based on all this, I went with option two - the new DS420j and four old drives in a RAID6. It turned out a bit cheaper since there was a discount code for the NAS, and the price per drive was a touch less when buying four.&lt;/p&gt;
&lt;p&gt;For a local backup, I&amp;rsquo;ll use a single second hand 12TB drive in a DS120j, and for the remote, mainly because I want to share some storage with the home owner, and I feel that has to be on RAID, I&amp;rsquo;ll buy a pair of 14TB second hand drives to put in the DS216j for the remote, so I can open up a 2TB pool for them to use as a local backup for laptops or what have you.&lt;/p&gt;</description></item><item><title>Recursive list of files in Linux</title><link>https://blog.iankulin.com/recursive-list-of-files-in-linux/</link><pubDate>Wed, 08 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/recursive-list-of-files-in-linux/</guid><description>&lt;p&gt;I&amp;rsquo;ve spent a few hours over the weekend migrating a media library from an external USB drive to the NAS, and in the process reorganised it, and in many cases bulk changed file names. I&amp;rsquo;ve also added a heap of metadata.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to check that I haven&amp;rsquo;t missed any files, but a side by side listing of each data source won&amp;rsquo;t do the trick, so I&amp;rsquo;ll probably end up pulling the data into a spreadsheet, but I&amp;rsquo;d like to get as close as possible with Linux-fu first.&lt;/p&gt;
&lt;p&gt;Before I go over my trial and error, and eventual solution, here&amp;rsquo;s how I&amp;rsquo;ve set up my test data for the examples. I thought I&amp;rsquo;d better start with something simple and small for testing commands.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.02.17-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.02.17-pm.png" width="495" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is actually the output of the &lt;code&gt;tree&lt;/code&gt; command on a &lt;code&gt;test&lt;/code&gt; directory I&amp;rsquo;ve created in my home directory. (I had to install it - &lt;code&gt;sudo apt install tree&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;What I need to end up with is something that recursively lists all the files, with one file per line, and it needs to include the directory tree to reach it. I should be able to pipe it through something to ignore lines that are just directories (and any other fluff).&lt;/p&gt;
&lt;h3 id="ls"&gt;ls&lt;/h3&gt;
&lt;p&gt;My go to for listing files is &lt;code&gt;ls -all&lt;/code&gt;, perhaps than can help us? It lists one line per file (along with permissions etc), so if we add &lt;code&gt;-R&lt;/code&gt; for recursive, that could be it. Here&amp;rsquo;s the output for &lt;code&gt;ls -all -R test&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;test:
total 16
drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 .
drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 ..
drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 dir1
drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 dir2

test/dir1:
total 8
drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 .
drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 ..
-rw-r--r-- 1 ian ian 0 Mar 6 17:01 ignore.me
-rw-r--r-- 1 ian ian 0 Mar 6 17:00 media1.ex1
-rw-r--r-- 1 ian ian 0 Mar 6 17:00 media1.ex2
-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media3.ex1
-rw-r--r-- 1 ian ian 0 Mar 6 16:36 somefile
-rw-r--r-- 1 ian ian 0 Mar 6 16:36 somefile2

test/dir2:
total 8
drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 .
drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 ..
-rw-r--r-- 1 ian ian 0 Mar 6 17:01 ignore.me
-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media4.ex1
-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media5.ex1
-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media6.ex2
-rw-r--r-- 1 ian ian 0 Mar 6 16:37 somefile
-rw-r--r-- 1 ian ian 0 Mar 6 16:37 somefile3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So we get one line per file, but the directory is on it&amp;rsquo;s own at the beginning of each directory listing.&lt;/p&gt;
&lt;h3 id="find"&gt;find&lt;/h3&gt;
&lt;p&gt;Based on &lt;a href="https://www.cyberciti.biz/faq/how-to-show-recursive-directory-listing-on-linux-or-unix/"&gt;this post&lt;/a&gt;, there is a command, &lt;code&gt;find&lt;/code&gt;, that might do what we want. The simple version would be &lt;code&gt;find test&lt;/code&gt; (remember test is the directory name).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@vm102-jellyfin:~$ find test
test
test/dir2
test/dir2/ignore.me
test/dir2/media4.ex1
test/dir2/somefile
test/dir2/media5.ex1
test/dir2/somefile3
test/dir2/media6.ex2
test/dir1
test/dir1/media3.ex1
test/dir1/ignore.me
test/dir1/somefile
test/dir1/media1.ex1
test/dir1/media1.ex2
test/dir1/somefile2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Well that is real close, but there&amp;rsquo;s no way to discern between a file and directory. In that same post, it;s suggested to use the &lt;code&gt;-ls&lt;/code&gt; option to see some more detail. Let&amp;rsquo;s try find &lt;code&gt;test -ls&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.20.59-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This looks pretty close. If there was someway of using that &amp;rsquo;d&amp;rsquo; in the first position of the permissions output to eliminate those lines, we&amp;rsquo;d be well on our way. I have a feeling this is a &lt;code&gt;grep&lt;/code&gt; question. I have some basic grep, so for example I know I could pull all of those directories with &lt;code&gt;find test -ls | grep ' d'&lt;/code&gt;, or even invert it with the &lt;code&gt;-v&lt;/code&gt; flag to get just the files (which is out eventual goal).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.36.37-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;However, this is pretty hacky. A space followed by a lowercase could easily occur in a filename. What I really need to do is look at just that column which I think is character number 18. Off to &lt;a href="https://unix.stackexchange.com/questions/32170/find-all-lines-in-a-file-with-a-certain-character-at-a-certain-position"&gt;Stack Exchange&lt;/a&gt; I guess&amp;hellip;&lt;/p&gt;
&lt;h3 id="grep-with-regex"&gt;grep with regex&lt;/h3&gt;
&lt;p&gt;Okay, it turns out we can use regex with grep. I&amp;rsquo;m no expert in that either, but in regex the caret ^ represents the start of the line, a fullstop represents any character, and we can repeat that however many times we want by following it with a number in (escaped) curly braces. Something like &lt;code&gt;'^.{17}d'&lt;/code&gt; should do it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.44.46-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Okay! We&amp;rsquo;re getting close. I also want to ignore all the metadata and just see the media files. This can be determined by the extensions - probably .avi .mp4 .mkv .mv4. With this test data, we&amp;rsquo;ll pretend it&amp;rsquo;s .ex1 and .ex2&lt;/p&gt;
&lt;h3 id="combining-grep-tests-with-logical-or"&gt;Combining grep tests with logical or&lt;/h3&gt;
&lt;p&gt;I guess I could build some sort of super regex combined with the first one, but I&amp;rsquo;m only dealing with thousands of files, not millions so the extra overhead of piping through another grep is not going to be a drama, and I can simplify my work. In the same way that the caret ^ marks the start of a line, the dollar $ marks the end of it. So to just get the .ex1 files something like &lt;code&gt;'\.ex1$'&lt;/code&gt; should do it. The backslash at the start is to escape the period, because here we want that to mean a literal full stop, and not a wildcard for any character.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.55.25-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Nice, but remember I&amp;rsquo;ve got a big list of extensions, so I need to logical or a few together. This is done by putting the expressions with a pipe between them. I had a couple of goes at this with no luck and that familiar feeling of being out of my depth with regex. However, there&amp;rsquo;s a grep way out of this, because the grep flag -e allows us to OR matching expressions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.06.52-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re definitely getting somewhere. You might think at this point that the chance of a directory name ending in .mp4 or one of the other media extensions is no low we could ignore it, and you&amp;rsquo;d probably be right. But as a matter of programmer pride, I never like to leave a future problem, so I&amp;rsquo;ll be keeping the directory rejecting grep. So now my command looks like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;find test -ls | grep -v &amp;#39;^.{17}d&amp;#39; | grep -e &amp;#39;\.ex1$&amp;#39; -e &amp;#39;\.ex2$&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Any experienced regex people would be pointing out the match for .ex1 .ex2 can easily be merged into a simple expression, but remember when I do this for real I&amp;rsquo;ve got a list of more complex extensions to test for.&lt;/p&gt;
&lt;h3 id="cut"&gt;cut&lt;/h3&gt;
&lt;p&gt;All that text at the beginning of these lines is not needed. Surely I can trim that off somehow? Yep - there&amp;rsquo;s a command &lt;code&gt;cut&lt;/code&gt; that does exactly that. The -b flag specifies which byte to extract, and this can also be a range. putting a dash after the position number says to output all of the bytes after that position. So if we applied &lt;code&gt;cut -b 5-&lt;/code&gt; to the string &lt;code&gt;123456789&lt;/code&gt;, the output would be &lt;code&gt;56789&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.26.18-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Bingo. Just one more problem. In my data, I have a heap of files with a valid extension but I want to exclude them based on their file name. Every directory with a movie has a trailer named &lt;code&gt;trailer.mp4&lt;/code&gt;, so I need to eliminate them. To simulate this, lets add in another extension with our test data.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.29.48-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So I want to take out the lines that include &amp;lsquo;/ignore.me&amp;rsquo;. I should be able to do this with another &lt;code&gt;grep -v&lt;/code&gt; regex on a line end. Something like &lt;code&gt;grep -v 'ignore.me$'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.33.22-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And we&amp;rsquo;re done! I&amp;rsquo;ll just direct this into a file, run it on both disks and pull them into Excel to separate the file names and directories, and sort them to compare.&lt;/p&gt;</description></item><item><title>Could it be a permissions problem?</title><link>https://blog.iankulin.com/could-it-be-a-permissions-problem/</link><pubDate>Sun, 05 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/could-it-be-a-permissions-problem/</guid><description>&lt;p&gt;Unix, and therefore Linux, was built from the ground up as a multi-user system. Thanks to this, great security is baked in, for example every file has permission attributes for it&amp;rsquo;s owner, the group the owner is a member of, and then everyone. For example, it might be a good idea if I can read, write and execute my own files, but the other members of my group can just read them, and any other user on the system has none of those rights.&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://blog.iankulin.com/folder-ownership-problems-with-jellyfin/"&gt;talked a bit about this&lt;/a&gt; when I was solving the first round of problems with getting Jellyfin working. I actually solved all of those problems - they were permissions related. Once I&amp;rsquo;d figured out the group id of the jellyfin user and applied that when mounting the NAS I had a week of blissful media consumption on the TV (via Google TV Chromecast), on my laptop, and phone. The eventual plan for this little box is to move it offsite though, so I needed TailScale, which has worked perfectly and effortlessly everywhere else I&amp;rsquo;d tried it, but it turns out it is not happy living in an LXC container on Proxmox which is where my Jellyfin instance was running.&lt;/p&gt;
&lt;p&gt;Googling around, it sounds like it is possible, but more work than I was prepared to invest, and didn&amp;rsquo;t actually needed to. The little i3 it is going to live on has plenty of headroom to run a full VM so I decided to do that.&lt;/p&gt;
&lt;p&gt;I started from scratch with a Debian VM and had it working perfectly, but then an hour or so after I&amp;rsquo;d downloaded all the metadata for my content, some, but not all of the posters would disappear. Once again, the problem turned out to be permissions (Jellyfin wanted write access to the media locations even though I&amp;rsquo;d told it not to save metadata there). I solved that with the &lt;a href="https://pimylifeup.com/chmod-777/"&gt;cursed 777&lt;/a&gt; then did something terrible to the jellyfin process so now it would not start again.&lt;/p&gt;
&lt;p&gt;Sadly, I had not saved a snapshop before I started messing around with things I only half understand.&lt;/p&gt;
&lt;p&gt;In the process of looking for a solution to the jellyfin process retrying starting five times then dying after I&amp;rsquo;d tried to restart it for a reason I can&amp;rsquo;t even recall, I stumbled on a StackExchange that was my exact problem. In the thread of answers, one of them was just &amp;ldquo;Use the container version&amp;rdquo;. I took slight offence at this, as did OP, but when the commenter pointed out that these fiddly installation problems that lead you all over the place and are painful because your configuration is different to everyone else&amp;rsquo;s, and the problem could lie there - is the exact problem containers are intended to solve.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve taken that advice on board and installed the official container version. First problem I&amp;rsquo;ve run into - Jellyfin can&amp;rsquo;t see my media folder - permissions.&lt;/p&gt;
&lt;p&gt;ANY problem you have running something on Linux, you should always start thinking about it in terms of permissions. Who is the user, what are they acessing?&lt;/p&gt;</description></item><item><title>Problems mounting network share at boot</title><link>https://blog.iankulin.com/problems-mounting-network-share-at-boot/</link><pubDate>Wed, 01 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/problems-mounting-network-share-at-boot/</guid><description>&lt;p&gt;I had Jellyfin working nicely in an LXC container in Proxmox, but could not get Tailscale working in the container. Since this is going to be an important part of accessing my media away from home, I decided it was probably worth the extra bulk to run JellyFin in a VM.&lt;/p&gt;
&lt;p&gt;Following &lt;a href="https://blog.iankulin.com/accessing-a-synology-nas-from-linux/"&gt;my own instructions&lt;/a&gt;, I had the mount command in the /etc/fstab file so it would persist across reboots. It looked a bit like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,uid=1000,gid=115,file_mode=0660,dir_mode=07
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The problem I had was that this was not mounting the NAS at boot time, but if I reran it with &lt;code&gt;mount -a&lt;/code&gt; it worked perfectly. A likely cause for this problem is that the network interface is not properly up at the time the mount is being tried.&lt;/p&gt;
&lt;p&gt;I found a few different suggestions for this, but the one that worked for me and I liked the most is from the first answer in &lt;a href="https://askubuntu.com/questions/43363/how-to-auto-mount-using-sshfs/1242516#1242516"&gt;this StackExchange question&lt;/a&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,noauto,x-systemd.automount,_netdev,uid=1000,gid=115,file_mode=0660,dir_mode=07
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From that answer, the explanation for these is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;noauto&lt;/code&gt; will stop the no-brainer actions like forcibly mounting whatsoever at booting regardless if the network is up or not.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x-systemd.automount&lt;/code&gt; is the smart daemon that knows when to mount.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;_netdev&lt;/code&gt; tag will also identify that it uses network devices, thus it will wait until the network is up.​​​​​​&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I had tried &lt;code&gt;_netdev&lt;/code&gt; by itself, but that wasn&amp;rsquo;t enough magic. The three together was.&lt;/p&gt;</description></item><item><title>Sudoers' file not working</title><link>https://blog.iankulin.com/sudoers-file-not-working/</link><pubDate>Mon, 27 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sudoers-file-not-working/</guid><description>&lt;p&gt;A couple of weeks ago, I posted &lt;a href="https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/"&gt;about the sudoers&amp;rsquo; file&lt;/a&gt;, and how there was a special tool for editing it since breaking it is a bad idea, and that in fact I needn&amp;rsquo;t bother, since I can just add my user to the sudoers&amp;rsquo; group with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;usermod -a -G sudo ian
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That worked (on Unbuntu) since &lt;code&gt;/etc/sudoers&lt;/code&gt; contained a line saying:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Allow members of group sudo to execute any command
%sudo	ALL=(ALL:ALL) ALL
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I tried the same trick on a fresh Debian install today, and no dice:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-26-at-3.32.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I assumed this might mean that the &lt;code&gt;sudoers&lt;/code&gt; file is different on Debian than Ubuntu, but no, that same line granting permission to the &lt;code&gt;sudo&lt;/code&gt; group is there. My next guess is that I hadn&amp;rsquo;t correctly added ian to that group. But no, that looks okay.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-26-at-3.48.51-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/offon.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Yup. Log out, log in&amp;hellip;&lt;/p&gt;</description></item><item><title>Folder ownership problems with Jellyfin</title><link>https://blog.iankulin.com/folder-ownership-problems-with-jellyfin/</link><pubDate>Wed, 22 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/folder-ownership-problems-with-jellyfin/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-5.32.36-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-5.32.36-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After being so blase about the file permissions when mounting the share to the Linux file system, and testing that root could read and write to the share, I ran into problems immediately when trying to add the media folder as a library in Jellyfin - getting the error &amp;ldquo;The path could not be found. Please ensure the path is valid and try again.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I definitely had the path correct - I could copy it from the dialog and cd to it at the CLI. So I suspected it was a permissions thing. The app might not have read permissions for the directory.&lt;/p&gt;
&lt;p&gt;If, as root, I ls -l (-l for long) any of the directories in this path, they look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@jellyfin:/mnt/media/video# ls -l
total 0
drwxrwx--- 2 1000 1000 0 Feb 18 09:13 Movies
drwxrwx--- 2 1000 1000 0 Feb 18 04:30 Shows
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those letters at the start of each listing &lt;a href="https://detailed.wordpress.com/2017/10/28/understanding-ls-command-output/"&gt;have a meaning&lt;/a&gt;. The first &lt;code&gt;d&lt;/code&gt; just means it&amp;rsquo;s a directory. Then there&amp;rsquo;s three groups of three letters:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;d rwx rwx --- (I&amp;#39;ve just added those spaces to make things clear)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These three lots of letters are the &lt;em&gt;permissions&lt;/em&gt; in the order of &lt;em&gt;owner&lt;/em&gt;, &lt;em&gt;group&lt;/em&gt; &amp;amp; &lt;em&gt;everybody&lt;/em&gt;. So for these directories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The owner can read, write &amp;amp; execute (&lt;code&gt;rwx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Members of this group can read, write &amp;amp; execute (&lt;code&gt;rwx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Everybody else can&amp;rsquo;t read, can&amp;rsquo;t write, and can&amp;rsquo;t execute (&lt;code&gt;---&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This raises the question, who is the owner of this directory, and what is the group we are talking about? The answer to those questions are the next items in the listing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://detailed.wordpress.com/2017/10/28/understanding-ls-command-output/"&gt;&lt;img src="https://blog.iankulin.com/images/ls-command3.jpg" width="686" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In our case the owner is &lt;code&gt;1000&lt;/code&gt; and the group is &lt;code&gt;1000&lt;/code&gt;. Where did these come from? Well, they were in the mount command I used in &lt;code&gt;etc/fstab&lt;/code&gt; yesterday:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,uid=1000,gid=1000,file_mode=0660,dir_mode=07
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So most likely that&amp;rsquo;s the source of my troubles. As I mentioned, I tested this from the command line logged in as root, and it worked fine. And I was imagining since I&amp;rsquo;d installed Jellyfin as root that Jellyfin would have all those rights, but perhaps (as would be wise) Jellyfin is running as a different user, and I need to add that user to the 1000 group in order to make this work.&lt;/p&gt;
&lt;p&gt;How to I find out what user Jellyfin is running as? A good place to start is to look at the running processes with the &lt;code&gt;ps&lt;/code&gt; command:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.02.04-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.02.04-pm.png" width="998" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, lookee here. User &lt;code&gt;jellyfin&lt;/code&gt; is running this process. We can see what groups she&amp;rsquo;s a member of by running the &lt;code&gt;groups&lt;/code&gt; command.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@jellyfin:/# groups jellyfin
jellyfin : jellyfin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, not a member of the &lt;code&gt;1000&lt;/code&gt; group then. We can use the &lt;code&gt;getent&lt;/code&gt; command to see the group numbers for users:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@jellyfin:/# getent group jellyfin 
jellyfin:x:115:
root@jellyfin:/# getent group root 
root:x:0:
root@jellyfin:/# 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Okay, so that&amp;rsquo;s likely out problem. To confirm it, we could change the group for the directory tree and files, or add &lt;code&gt;jellyfin&lt;/code&gt; to the &lt;code&gt;1000&lt;/code&gt; group. Since I now know that &lt;code&gt;jellyfin&lt;/code&gt; is a member of the &lt;code&gt;115&lt;/code&gt; group, and that I just plucked &lt;code&gt;1000&lt;/code&gt; out of the air, I&amp;rsquo;m inclined to remount the share with a &lt;code&gt;gid=115&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.24.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.24.11-pm.png" width="757" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And&amp;hellip;. it works.&lt;/p&gt;</description></item><item><title>Accessing a Synology NAS from Linux</title><link>https://blog.iankulin.com/accessing-a-synology-nas-from-linux/</link><pubDate>Mon, 20 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/accessing-a-synology-nas-from-linux/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_4154x.jpg" width="1000" alt=""&gt;
&lt;p&gt;I picked up a Synology DS216j NAS from eBay to use for storage for the rapidly growing home lab. The eventual plan is that as well as my VM backups, it will host the media library, and eventually (when this has all proved itself reasonably bullet-proof) my current DropBox contents. That won&amp;rsquo;t all fit on the 2x2TB drives that the DS216j came with, and I have a pair of 8TBs on hand, but I wanted to set it up and checked it all worked.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.15.25-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Configuration of the NAS was a &amp;lsquo;follow the prompts&amp;rsquo; exercise for the most part. The Synology OS is a Linux port called DSM, but it&amp;rsquo;s intended to be an appliance so all the interactions are through the web client. I&amp;rsquo;m using RAID 1 since the plan is that the production segment of the homelab will all be high-ish available. There&amp;rsquo;s a few options to install extras (such as Tailscale), but these little &amp;lsquo;j&amp;rsquo; models don&amp;rsquo;t run an x86 processor, so no docker etc.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d got through all of that, I created a share in &amp;lsquo;File Station&amp;rsquo; and copied a couple of files in. By default, Samba shares are on (with the name WORKGROUP - so I guess this is aimed at making it simple for Windows users) but NFS are not. I know nothing about NFS, so this suits me for the moment. Additionally, my &lt;a href="https://en.wikipedia.org/wiki/WD_TV"&gt;WD-TV&lt;/a&gt; shares it&amp;rsquo;s attached USB drive using Samba, so I&amp;rsquo;m used to accessing it from the MacBook. Let&amp;rsquo;s try the NAS from the MacBook:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.23.38-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.23.38-pm.png" width="512" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It asked for the login details, then I was in. Could not have been much easier.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.28.55-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.28.55-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="accessing-from-linux"&gt;Accessing from Linux&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m planning on running Jellyfin in an LCX container. So I&amp;rsquo;ll set that up for this test too. I stood it up with the Debian server .iso in Proxmox and specified it should be a &amp;lsquo;privileged&amp;rsquo; container, and in the Proxmox options for the LXC ticked &amp;lsquo;SMB/CIFS&amp;rsquo;. This process is not just for Synology - it will work to mount any samba share on a network to your Linux machine.&lt;/p&gt;
&lt;p&gt;We need to make an empty directory to mount to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir /mnt/media
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then edit (I use nano) the file &lt;code&gt;/etc/fstab&lt;/code&gt; to include:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,uid=1000,gid=1000,file_mode=0660,dir_mode=07
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;etc/fstab&lt;/code&gt; runs at startup, and if the shares are available (cautionary note about booting up your lab after a power outage) it will set them up. There&amp;rsquo;s a fair bit going on in the command, perhaps we should pull it apart:&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;//192.168.100.25/media /mnt/media&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The first directory is the share, the second is the empty directory on this machine we are mounting the share to.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;username=jelly, password=jellypass&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Our credentials needed to log into the share. Actually I used my root credentials, but obviously a good idea would be to make a user on the NAT for this specific purpose with only access to the share they need to operate.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;uid=1000, gid=1000&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Okay, we're about to enter the Linux zone... These numbers are a &lt;a href="https://medium.com/@gggauravgandhi/uid-user-identifier-and-gid-group-identifier-in-linux-121ea68bf510"&gt;Linux user id and a group ownership id&lt;/a&gt; that Linux assigns to resources - you know, for &lt;code&gt;chown&lt;/code&gt; and stuff like that. If you type in &lt;code&gt;id&lt;/code&gt; at the CLI you can see your numbers. For some reason code examples often use 1000 for both, and things seem to work so I don't worry about it.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;file_mode=0660, dir_mode=07&lt;/code&gt;&lt;/td&gt;&lt;td&gt;More Linux permission stuff. Used in combination with the previous two parameters, and &lt;a href="http://file_mode=0660,dir_mode=07"&gt;will probably cause me problems later&lt;/a&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Note that a couple of other posts on the internet about mounting samba shares thought I&amp;rsquo;d have to do one or both of these commands to install extra samba goodness:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt install smbclient
apt install cifs-utils
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But it turns out I didn&amp;rsquo;t. I suspect that was something to do with ticking the box for SMB/CIFS when I was creating the LXC container in Proxmox.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve saved that command in &lt;code&gt;/etc/fstab&lt;/code&gt;, reload the mounts with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mount -a
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If there&amp;rsquo;s no errors, you are probably right to go. Have a look at your mount point to see your shared files.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-4.59.09-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Since the mount command is in the /etc/fstab file, this mount will be durable - as long as the share is available, it will be mounted every time this machine starts.&lt;/p&gt;</description></item><item><title>External USB Drives in Linux</title><link>https://blog.iankulin.com/external-usb-drives-in-linux/</link><pubDate>Sat, 18 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/external-usb-drives-in-linux/</guid><description>&lt;p&gt;Many modern Linux distros will auto-mount USB drives - they just pop up in the graphical file manager as users would expect. When you&amp;rsquo;re running server, older, or smaller versions, that&amp;rsquo;s probably not going to be the case, and you&amp;rsquo;ll have to do it old school.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at some basics. &lt;code&gt;[lsblk](https://man7.org/linux/man-pages/man8/lsblk.8.html)&lt;/code&gt; will list the &amp;lsquo;block&amp;rsquo; devices. Your output will almost certainly be a bit different than this.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve:~# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk 
├─sda1 8:1 0 1007K 0 part 
├─sda2 8:2 0 512M 0 part /boot/efi
└─sda3 8:3 0 118.7G 0 part 
 ├─pve-swap 253:0 0 7.7G 0 lvm [SWAP]
 ├─pve-root 253:1 0 39.8G 0 lvm /
 ├─pve-data_tmeta 253:2 0 1G 0 lvm 
 │ └─pve-data-tpool 253:4 0 54.6G 0 lvm 
 │ ├─pve-data 253:5 0 54.6G 1 lvm 
 │ ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
 │ ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
 │ ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
 │ ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
 │ └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
 └─pve-data_tdata 253:3 0 54.6G 0 lvm 
 └─pve-data-tpool 253:4 0 54.6G 0 lvm 
 ├─pve-data 253:5 0 54.6G 1 lvm 
 ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
 ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
 ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
 ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
 └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you look at the &lt;code&gt;type&lt;/code&gt; column, you can see this machine has one &lt;em&gt;disk&lt;/em&gt;, with three &lt;em&gt;partitions&lt;/em&gt;, and the last partition has a heap of &lt;em&gt;logical volumes&lt;/em&gt;. Let&amp;rsquo;s plug the thumb drive in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve:~# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk 
├─sda1 8:1 0 1007K 0 part 
├─sda2 8:2 0 512M 0 part /boot/efi
└─sda3 8:3 0 118.7G 0 part 
 ├─pve-swap 253:0 0 7.7G 0 lvm [SWAP]
 ├─pve-root 253:1 0 39.8G 0 lvm /
 ├─pve-data_tmeta 253:2 0 1G 0 lvm 
 │ └─pve-data-tpool 253:4 0 54.6G 0 lvm 
 │ ├─pve-data 253:5 0 54.6G 1 lvm 
 │ ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
 │ ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
 │ ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
 │ ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
 │ └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
 └─pve-data_tdata 253:3 0 54.6G 0 lvm 
 └─pve-data-tpool 253:4 0 54.6G 0 lvm 
 ├─pve-data 253:5 0 54.6G 1 lvm 
 ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
 ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
 ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
 ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
 └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
sdb 8:16 1 14.5G 0 disk 
└─sdb1 8:17 1 14.5G 0 part 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There we are, down the bottom. Our disk is &lt;code&gt;sdb&lt;/code&gt;, and partition is &lt;code&gt;sdb1&lt;/code&gt;. So the OS knows it exists - it&amp;rsquo;s recognised, but to use it we need to &lt;code&gt;mount&lt;/code&gt; it to the file system somewhere. Mounting it will let us see and interact with the files on the drive.&lt;/p&gt;
&lt;p&gt;By convention, removable media is often mounted in &lt;code&gt;/media&lt;/code&gt; or &lt;code&gt;/mnt&lt;/code&gt;, but it can be wherever you like. Let&amp;rsquo;s make a directory for it in &lt;code&gt;/media&lt;/code&gt; and mount it there.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve:/# mkdir /media/external
root@pve:/# mount /dev/sdb1 /media/external
root@pve:/# ls /media/external
&amp;#39;02 Advance Australia Fair 1 verse vocal.mp3&amp;#39; &amp;#39;Year 3 Pack.pdf&amp;#39;
&amp;#39;Araw ng Kasarinl&amp;#39;$&amp;#39;\341&amp;#39;&amp;#39;n.mp4&amp;#39;	 &amp;#39;Year 4 Pack.pdf&amp;#39;
&amp;#39;System Volume Information&amp;#39;		 &amp;#39;Year 5 Pack.pdf&amp;#39;
&amp;#39;Year 1 Pack.pdf&amp;#39;			 &amp;#39;Year 6 Pack.pdf&amp;#39;
&amp;#39;Year 2 Pack.pdf&amp;#39;
root@pve:/# 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Success!&lt;/p&gt;
&lt;p&gt;If we do the lsblk again, you&amp;rsquo;ll see out mount point in the listing&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve:/# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk 
├─sda1 8:1 0 1007K 0 part 
├─sda2 8:2 0 512M 0 part /boot/efi
└─sda3 8:3 0 118.7G 0 part 

...

sdb 8:16 1 14.5G 0 disk 
└─sdb1 8:17 1 14.5G 0 part /media/external
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Of course, just as in Windows, we need to tell the OS when we want to remove a removable drive to ensure that any caches are flushed and that we don&amp;rsquo;t inadvertently lose data when we yank it out. This is the &lt;em&gt;unmounting&lt;/em&gt; process.&lt;/p&gt;
&lt;p&gt;We can unmount a drive with the &lt;code&gt;[umount](https://man7.org/linux/man-pages/man8/umount.8.html)&lt;/code&gt; command.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@pve:/# ls /media/external
&amp;#39;02 Advance Australia Fair 1 verse vocal.mp3&amp;#39; &amp;#39;Year 3 Pack.pdf&amp;#39;
&amp;#39;Araw ng Kasarinl&amp;#39;$&amp;#39;\341&amp;#39;&amp;#39;n.mp4&amp;#39;	 &amp;#39;Year 4 Pack.pdf&amp;#39;
&amp;#39;System Volume Information&amp;#39;		 &amp;#39;Year 5 Pack.pdf&amp;#39;
&amp;#39;Year 1 Pack.pdf&amp;#39;			 &amp;#39;Year 6 Pack.pdf&amp;#39;
&amp;#39;Year 2 Pack.pdf&amp;#39;
root@pve:/# umount /media/external
root@pve:/# ls /media/external
root@pve:/# 
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Configuring Proxmox for Free Use</title><link>https://blog.iankulin.com/configuring-proxmox-for-free-use/</link><pubDate>Thu, 16 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/configuring-proxmox-for-free-use/</guid><description>&lt;p&gt;I installed Proxmox on my second server last night, and tonight when I ran &lt;code&gt;apt update&lt;/code&gt; I ran into the error you get when you haven&amp;rsquo;t bought a license.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Err:5 https://enterprise.proxmox.com/debian/pve bullseye InRelease 
 401 Unauthorized [IP: 103.67.14.50 443]
Reading package lists... Done 
E: Failed to fetch https://enterprise.proxmox.com/debian/pve/dists/bullseye/InRelease 401 Unauthorized [IP: 103.67.14.50 443]
E: The repository &amp;#39;https://enterprise.proxmox.com/debian/pve bullseye InRelease&amp;#39; is not signed.
N: Updating from such a repository can&amp;#39;t be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even though I guess it was only a month ago (let that sink in people who think the raspberry Pi they just bought is going to be the last homelab hardware they buy 😊) since I set up my first Proxmox server, I&amp;rsquo;d already forgotten there&amp;rsquo;s a step to enable it to get updates without a subscription.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a couple of little steps for this. They are both &lt;a href="https://pve.proxmox.com/wiki/Package_Repositories#sysadmin_enterprise_repo"&gt;here on the Proxmox wiki&lt;/a&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list.d/pve-enterprise.list&lt;/code&gt; to comment out the single repository listed in there.&lt;/li&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list&lt;/code&gt; to look like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;deb http://ftp.debian.org/debian bullseye main contrib
deb http://ftp.debian.org/debian bullseye-updates main contrib

# PVE pve-no-subscription repository provided by proxmox.com,
# NOT recommended for production use
deb http://download.proxmox.com/debian/pve bullseye pve-no-subscription

# security updates
deb http://security.debian.org/debian-security bullseye-security main contrib
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then you&amp;rsquo;ll be good to go.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-07-at-8.41.15-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Moving a VM between two Proxmox hosts</title><link>https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/</link><pubDate>Thu, 16 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/</guid><description>&lt;img src="https://blog.iankulin.com/images/s-l640.jpg" width="264" alt=""&gt;
&lt;p&gt;So, the very small datacentre has undergone a major hardware upgrade today. The HP 800 G1 is joined by an HP 800 G2. Four core i7 vs the old two core i5. Double the RAM to 16GB, four times the disk. The old machine will become a dev/play machine - still virtualised, and the new machine will run the production apps, mostly in Docker containers.&lt;/p&gt;
&lt;p&gt;Since everything is containerised, I did consider running Unbuntu Server on the bare metal of the new machine, but running it on Proxmox will give me some flexibility, and since we&amp;rsquo;ve stepped up the underlying hardware resource so substantially, performance will be well in front anyway. Plus it will give me some flexibility if needed in the future.&lt;/p&gt;
&lt;p&gt;Another massive benefit of virtualisation is the ability to backup a VM to a single file.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve invested several hours in the old server - downloading ISOs, updating everything, installing Docker, adding my containers, reserving the IP addresses in DNS and so on. Wouldn&amp;rsquo;t it be amazing if I could stop my main VM, back it up, copy the backup to the new server, then boot it there and have every thing just work.&lt;/p&gt;
&lt;p&gt;In theory this should be entirely possible. So let&amp;rsquo;s give it a go.&lt;/p&gt;
&lt;p&gt;In the Proxmox web interface, you can execute a backup on a VM. There&amp;rsquo;s three flavours with &lt;code&gt;STOP&lt;/code&gt; being the most reliable as it actually stops the VM to grab it&amp;rsquo;s copy. On this system I can easily afford to stop everything for ten minutes so I&amp;rsquo;ll actually be shutting down my VM and doing this sort of back up. We do this by clicking on the VM, then selecting backup. At the top is a backup button.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-8.35.38-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-8.35.38-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve done your backup it appears in a couple of places in the web interface - in this backup screen associated with the VM, but also if you select the &lt;code&gt;local&lt;/code&gt; disk then backup.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-8.41.43-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s my VM nicely backed up into a single tarball, now I want to download it. I really feel the Proxmox interface should have buttons for Download and Upload on this screen - that would make this operation even easier. But it does not.&lt;/p&gt;
&lt;p&gt;The first problem is to find where these files are stored. Thanks to u/walalauw&amp;rsquo;s answer in &lt;a href="https://old.reddit.com/r/Proxmox/comments/jj6eqz/downloading_backups/"&gt;this reddit thread&lt;/a&gt;, it sounds like they are at &lt;code&gt;/var/lib/vz/dump&lt;/code&gt; I head there in FileZilla, and find:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-7.54.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-7.54.09-pm.png" width="826" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You only need the &lt;code&gt;.zst&lt;/code&gt; file, but neat freaks can grab the the &lt;code&gt;.notes&lt;/code&gt; as well. It contains the text you wrote for the backup - in the previous screenshot you can see I&amp;rsquo;d written &amp;ldquo;Ready to move&amp;rdquo; for this one.&lt;/p&gt;
&lt;p&gt;Copy this file somewhere - I copied it one to my local machine, then from there to the new Proxmox (same &lt;code&gt;/var/lib/vz/dump&lt;/code&gt; directory) since I was using FileZilla, but a hardcore scp user would have gone direct between the two servers and saved a bit of time.&lt;/p&gt;
&lt;p&gt;Now on the new server, I can see my backup! All you do then is select it and hit the &lt;code&gt;Restore&lt;/code&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-7.58.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A minute or two later, the VM &amp;ldquo;dockhost&amp;rdquo; is in the list. I press &lt;code&gt;Start&lt;/code&gt;, and it boots, my containers all start. And magically, amazingly it all works perfectly.&lt;/p&gt;
&lt;p&gt;If I wasn&amp;rsquo;t already sold on virtualization, this would definitely sell me on it. I understand there are other ways of moving VM&amp;rsquo;s between hosts, but this is hard to beat for simplicity if you can afford the downtime. This was the first time I&amp;rsquo;d ever done this, and I was stopping to screenshot things along the way. From the time I stopped the VM, to the time my last container went green was only nine minutes.&lt;/p&gt;</description></item><item><title>Uptime Kuma &amp; NTFY</title><link>https://blog.iankulin.com/uptime-kuma-ntfy/</link><pubDate>Wed, 15 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/uptime-kuma-ntfy/</guid><description>&lt;p&gt;&lt;a href="https://github.com/louislam/uptime-kuma"&gt;Uptime Kuma&lt;/a&gt; is a monitoring tool suitable for self-hosting, and as well as being a good tool for monitoring the status of your network and applications, it&amp;rsquo;s a nice smallish app to get started on Docker containers.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-6.41.24-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-6.41.24-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since it&amp;rsquo;s in a container, you need to create a volume for it and pass it in to persist your settings. Then it&amp;rsquo;s just a matter of adding each item you want to monitor. There&amp;rsquo;s a heap of fancy options for this, the only three I&amp;rsquo;ve used are ping - just pings an address, http(s) - requests a page and checks the header for a 200, and http(s) keyword - looks at the returned page for a keyword in the html.&lt;/p&gt;
&lt;p&gt;You choose the time intervals for all these. Additionally you can set up a notification for each. This is a great idea - I&amp;rsquo;m not sitting in my datacentre command room watching Uptime Kuma all day, I need to know on my phone if a CAT5 cable&amp;rsquo;s been pulled out inadvertently while I was vacuuming.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s lots of options for how to do this, including messaging platforms such as Telegram and Discord. I had a look on &lt;a href="https://www.reddit.com/r/selfhosted/comments/z0gpr2/free_push_service_for_uptime_kuma/"&gt;r/selfhosted&lt;/a&gt; to see what was recommended, and discovered &lt;a href="https://ntfy.sh/"&gt;NTFY&lt;/a&gt; which is an amazing little service. It has Android and iOS apps, in the app you subscribe to an endpoint, then a notification can be sent to your phone with a simple http get, for example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -d &amp;#34;This message will pop up on phone&amp;#34; ntfy.sh/ian_test
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-12.39.22-pm.png" alt=""&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4055.jpg" width="192" alt=""&gt;
&lt;p&gt;The &lt;code&gt;ian_test&lt;/code&gt; part of the url is called the &lt;em&gt;topic&lt;/em&gt;, and in the app you can subscribe to several topics. It&amp;rsquo;s worth noting this is all completely open. Anyone can send messages to the ian_test topic, and anyone can receive them. You should choose a topic name that&amp;rsquo;s likely to be unique, and be mindful that you&amp;rsquo;re leaking intelligence. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -d &amp;#34;CCTV offline - 12 George St&amp;#34; ntfy.sh/maquarie_bank
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;would not a be a good use case. Get something more secure for that application. It&amp;rsquo;s probably not going to be free.&lt;/p&gt;
&lt;p&gt;Speaking of which, NTFY is free, including the server. It is possible (and probably a good idea since then you could add a little security) to self host it. It&amp;rsquo;s such a great little tool, and just so immediately and completely achieved what I wanted with zero drama and low effort, I hit the &lt;a href="https://github.com/sponsors/binwiederhier"&gt;github sponsor button for it&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Netgear GS108E switch problem</title><link>https://blog.iankulin.com/netgear-gs108e-switch-problem/</link><pubDate>Tue, 14 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/netgear-gs108e-switch-problem/</guid><description>&lt;p&gt;I had a weird issue today that I wouldn&amp;rsquo;t have known about if I didn&amp;rsquo;t have an over-engineered home network monitoring system.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got a new &lt;a href="https://www.netgear.com/au/business/wired/switches/plus/gs108e/"&gt;GS108E managed switch&lt;/a&gt;, purchased in anticipation of connecting a NAS to the homelab - I want to have a solid 1Gb connection between the NAS and the servers, and also in anticipation of moving to VLANs before I start to expose self-hosted services to the internet.&lt;/p&gt;
&lt;p&gt;It sat plugged into the network for 24 hours with no load, then I moved the server over on to it. I hadn&amp;rsquo;t changed any configuration on it (it comes out of the box just configured as a switch). It&amp;rsquo;s IP address was set by DHCP, and I&amp;rsquo;d reserved that in my Netgear modem.&lt;/p&gt;
&lt;p&gt;It was all working perfectly, then a day later, it suddenly stopped responding to pings and the interface was unavailable. The weird bit was that the network was still working perfectly - I could ssh to the server through it, but it didn&amp;rsquo;t appear on the network. The leds on the connected ports where flashing away happily.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-7.18.51-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-7.18.51-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There are a couple of search results for this switch working, but the interface not reachable - but they all seem to involve complicated VLAN setups where people have locked themselves out rather than the default config suddenly having an issue.&lt;/p&gt;
&lt;p&gt;It will be disappointing if this is going to be a regular issue, but at least I&amp;rsquo;ll have good data to investigate it!&lt;/p&gt;</description></item><item><title>Local host names with Pi-hole</title><link>https://blog.iankulin.com/local-host-names-with-pi-hole/</link><pubDate>Mon, 13 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/local-host-names-with-pi-hole/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-5.46.22-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I run an instance of Pi-hole as a network-wide advert and surveillance blocker. It also has a setting to block individual domain which I use to force myself to really consider if 30 minutes of &lt;a href="https://old.reddit.com/r/homelab/"&gt;Reddit&lt;/a&gt; is a good idea when I should probably just be going to bed.&lt;/p&gt;
&lt;p&gt;As I&amp;rsquo;ve increased the number of real and virtual devices on my network, it&amp;rsquo;s getting to be a pain remembering all of their IP addresses. So I&amp;rsquo;d like to have DNS entries for them, for example I&amp;rsquo;d much rather:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh ian@vm100-dockhost
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;than&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh ian@192.168.100.29
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Luckily, since Pi-hole works by being a DNS server (who drops requests for domains it doesn&amp;rsquo;t like) it has the capability of handling these local names.&lt;/p&gt;
&lt;h4 id="how-to-set-up"&gt;How to set up&lt;/h4&gt;
&lt;p&gt;Down the left side of the Pi-hole interface, there&amp;rsquo;s a link for Local DNS | DNS Records. Click on that for this screen, then input the name you&amp;rsquo;d like to use and the IP address it should go to. I use the device host names so there&amp;rsquo;s less confusion, but there&amp;rsquo;s no rule for this - you can use whatever you like.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-6.05.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-6.05.02-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-6.08.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-6.08.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>ssh key login on VPS</title><link>https://blog.iankulin.com/ssh-key-login-on-vps/</link><pubDate>Sun, 12 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-key-login-on-vps/</guid><description>&lt;p&gt;Due to &lt;a href="https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/"&gt;potential brute force attacks&lt;/a&gt;, it&amp;rsquo;s a good idea to turn off password access via shh and instead rely on ssh keys. In this post, I&amp;rsquo;ll run through that process.&lt;/p&gt;
&lt;h4 id="generating-your-key"&gt;Generating your key&lt;/h4&gt;
&lt;p&gt;On a mac (or actually most *ix systems), your ssh keys live in the &lt;code&gt;.ssh&lt;/code&gt; directory inside the users home directory. Since it starts with a period, it&amp;rsquo;s a &amp;lsquo;hidden&amp;rsquo; directory. To see it in Finder press&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Shift|Command|.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;(that command includes the full stop). Here&amp;rsquo;s mine:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-31-at-6.05.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The keys are in pairs, there are two pairs of keys above: id_ecdsa and id_rsa. Each pair of keys includes a public key and a private key. It&amp;rsquo;s the public key we want to put on the server. The private key is precious - it should not be anywhere that others can access it. The public key can safely be provided to a server that can use it to securely authenticate the holder of the private key by doing some complicated stuff.&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t already have a key pair, we need to create one. On Mac or Linux that is a &lt;a href="https://www.makeuseof.com/ssh-keygen-mac/"&gt;straightforward process in a terminal&lt;/a&gt;, on Windows I think the recommendation is usually to use &lt;a href="https://www.ssh.com/academy/ssh/putty/windows/puttygen"&gt;PuTTY&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="installing-your-key-on-the-target-system"&gt;Installing your key on the target system&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s possible to create a file on the system we want to ssh onto and to paste the public key into in, but from a Mac or Linux machine, we can use the &lt;code&gt;ssh-copy-id&lt;/code&gt; command which will do all that for us in an error-free way. It has the format:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh-copy-id &amp;lt;username&amp;gt;@&amp;lt;host&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-1.51.49-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-1.51.49-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="turning-off-password-access"&gt;Turning off password access&lt;/h4&gt;
&lt;p&gt;Although it&amp;rsquo;s convenient to ssh in without a password (because we&amp;rsquo;re using keys), the main reason for doing this is to turn off passwords to make brute-force password attacks impotent. For that, we need to turn off passwords for &lt;code&gt;ssh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Note that I&amp;rsquo;m running on Unbuntu server 22.x so your mileage may vary. Certainly when I was reading around about this I found many slightly different approaches, but this is what I&amp;rsquo;ve done and tested so that ssh refuses to accept passwords.&lt;/p&gt;
&lt;p&gt;The configuration files for ssh on Ubuntu are at /etc/ssh/&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-3.57.15-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-3.57.15-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the &lt;code&gt;sshd_config&lt;/code&gt; we&amp;rsquo;re interested in (&lt;code&gt;ssh_config&lt;/code&gt; is for the client, we&amp;rsquo;re wanting to change the ssh server - daemon). You&amp;rsquo;ll also notice there&amp;rsquo;s a &lt;code&gt;sshd_config.d&lt;/code&gt; directory. The reason for this is that the config file has a line in it at the top that includes all the config files in that directory. This is a common pattern in Unbuntu - the main config file pulls in other config files. When you see that, you really shouldn&amp;rsquo;t edit the main config file as it&amp;rsquo;s possible that a future update will change it, you edit, or add to the files in the directory below.&lt;/p&gt;
&lt;p&gt;The way it words is that the commands in the files in the sub-directory will have priority over the defaults in the main config file (which is slightly counter-intuitive for me).&lt;/p&gt;
&lt;p&gt;The VPS I am using is on &lt;a href="https://blog.iankulin.com/your-own-aussie-server-on-binarylane/"&gt;binarylane&lt;/a&gt;, and they already have a config file in the subdirectory, so I&amp;rsquo;ll edit that.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-4.08.24-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;With a standard Unbuntu install, there are no files in there, so you&amp;rsquo;ll need to create one with &lt;code&gt;touch&lt;/code&gt;, then add the line &lt;code&gt;PasswordAuthentication no&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Many of the articles on the internet also talk about turning off &lt;code&gt;ChallengeResponseAuthentication&lt;/code&gt; and &lt;code&gt;UsePAM&lt;/code&gt;, but I found with my VPS and local Unbuntu servers, all that was needed was &lt;code&gt;PasswordAuthentication&lt;/code&gt; if my intention was to prevent these attacks.&lt;/p&gt;
&lt;p&gt;Note that once this config is activated, you won&amp;rsquo;t be able to log in via ssh with a password, so it would be foolhardy to do it without having set up &lt;em&gt;and&lt;/em&gt; tested your login with keys.&lt;/p&gt;
&lt;p&gt;This config won&amp;rsquo;t be active until the ssh daemon is restarted.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl reload ssh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now if someone attempts to ssh in they&amp;rsquo;ll see this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-4.16.37-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Not that turning off passwords like this is only for ssh. You&amp;rsquo;ll still be able to log in via the console if you&amp;rsquo;ve stuffed something up.&lt;/p&gt;</description></item><item><title>Save Proxmox password in Chrome</title><link>https://blog.iankulin.com/save-proxmox-password-in-chrome/</link><pubDate>Sat, 11 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/save-proxmox-password-in-chrome/</guid><description>&lt;p&gt;When I installed Proxmox, I&amp;rsquo;d used a secure, and therefore absurdly long and complicated root password. I do use a password manager, but don&amp;rsquo;t have it integrated into Chrome, so it was buggging me having to find it and paste it in each time - why wasn&amp;rsquo;t Chrome offering to save it for me?&lt;/p&gt;
&lt;p&gt;Well, you&amp;rsquo;d guess it was something to do with this. I feel like Chrome is trying to tell me something here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.06.49-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Seems like a certificate thing. &lt;a href="https://forum.proxmox.com/threads/how-can-i-save-pve-web-loginpassword-on-firefox-chrome.46180/"&gt;These peeps&lt;/a&gt; say that I need to import the CA from PVE, and one more &lt;a href="https://pve.proxmox.com/wiki/Import_certificate_in_browser"&gt;googlestep reveals&lt;/a&gt; the certificate is on the Proxmox machine at &lt;code&gt;/etc/pve/pve-root-ca.pem&lt;/code&gt; so we need to grab that.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/aint.jpg" width="90" alt=""&gt;
&lt;p&gt;A while ago, I wrote a post about &lt;a href="https://blog.iankulin.com/copying-a-file-via-ssh/"&gt;using scp to copy files over ssh&lt;/a&gt;, and you should totally know how to do that, but my daily drive for secure file copying is now &lt;a href="https://filezilla-project.org/"&gt;filezilla&lt;/a&gt;. Once you have a bundle of servers in VM&amp;rsquo;s and containers that you revisit and move stuff around all the time, its just a big productivity step-up to have that list of hosts and credentials a tap away, plus having the visual arrangement of nested folders works for my brain somehow.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.14.40-am-1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;On Mac, certificates need to live in the KeyChain, so you just drag the file into the certificates page. But it won&amp;rsquo;t be trusted, so you need to go in and manually do that. Where it says &amp;ldquo;Use System Defaults&amp;rdquo; change it to &amp;ldquo;Always Trust&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.19.54-am-1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It was annoying at this stage to find that Chrome was still saying it was insecure - even though it had changed to saying the certificate was valid.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.20.50-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Looking at the settings for the site in Chrome, there&amp;rsquo;s an option for &amp;ldquo;Insecure Content&amp;rdquo; I try changing that to &amp;ldquo;Allow&amp;rdquo;, but really I&amp;rsquo;m guessing by this stage.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.21.15-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;But it actually does help - I&amp;rsquo;ve got the little padlock. That wasn&amp;rsquo;t quite the end since Chrome still wasn&amp;rsquo;t offering to save the password, but clearing the cache fixed that.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.24.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.24.08-am.png" width="566" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Saved by the qemu_guest_agent</title><link>https://blog.iankulin.com/saved-by-the-qemu_guest_agent/</link><pubDate>Fri, 10 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/saved-by-the-qemu_guest_agent/</guid><description>&lt;p&gt;Literally an hour after I wrote the post &lt;a href="https://blog.iankulin.com/proxmox-qemu-guest-agent/"&gt;about installing the qemu guest agent&lt;/a&gt; in a VM and explaining how it can be used to inject root level commands into a VM, I had use of it due to a mistake.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d decided to add myself to the sudoers file. Since the last line in that file is a directive to include all the files in the /etc/sudoers.d directory, the accepted way to do that for local changes is to create a file in that directory with the necessary commands.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# User privilege specification
root	ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo	ALL=(ALL:ALL) ALL

# See sudoers(5) for more information on &amp;#34;@include&amp;#34; directives:

@includedir /etc/sudoers.d
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The format of this command is important to get right, since if you stuff it up, sudo will not work, and I don&amp;rsquo;t even have a root login for this server, so then I&amp;rsquo;d be in a pickle. It&amp;rsquo;s so important to not stuff this up that there is a special command for editing the files that won&amp;rsquo;t let you save them if you&amp;rsquo;ve made a mistake.&lt;/p&gt;
&lt;p&gt;Out of an abundance of caution, I decided to copy the system sudoers file to the directory as a starting point since it would have the correct format and be easy to edit. It didn&amp;rsquo;t occur to me that then the &lt;code&gt;@includedir&lt;/code&gt; at the end would become an infinite loop.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-2.06.12-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So here I am, logged in as ian, with no sudo, needing to edit or delete a protected file, and with no root login. Luckily, it&amp;rsquo;s a VM running the qemu user agent, so I can access it from Proxmox.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-2.04.37-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-2.04.37-pm.png" width="895" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Saved by over-engineering! Thank you open source contributors.&lt;/p&gt;</description></item><item><title>Proxmox - Qemu-guest-agent</title><link>https://blog.iankulin.com/proxmox-qemu-guest-agent/</link><pubDate>Thu, 09 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-qemu-guest-agent/</guid><description>&lt;p&gt;One of the strengths of having virtual machines (VMs) running inside a hypervisor like Proxmox is how they are isolated from each other and their host. This is a strength - if there is a problem with a particular VM nothing else should be affected by it.&lt;/p&gt;
&lt;p&gt;But this can also be a pain if the hypervisor needs access to a VM to control or monitor it in some way that&amp;rsquo;s only possible from inside the VM. Proxmox can use the &lt;a href="https://qemu-project.gitlab.io/qemu/interop/qemu-ga.html"&gt;Qemu Guest Agent&lt;/a&gt; for this purpose. To over simplify, this is a deamon that runs in the VM and opens a unix socket/virtual serial port to the hypervisor, and listens for commands on it. With Proxmox, the main use of this is to aid in orderly shutdowns and backups, but it also allows us to run commands in the VM from Proxmox - an obvious security compromise. You definitely would not want to install this daemon on a hosted VPS.&lt;/p&gt;
&lt;h4 id="installing-qemu-guest-agent"&gt;Installing Qemu-guest-agent&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;m running Unbuntu Server 22.4.1 inside Proxmox 7.3 for the following examples.&lt;/p&gt;
&lt;p&gt;Use apt (or whatever you distro uses) to install the agent inside the VM.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt install qemu-guest-agent
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will do the usual thing - build the list, ask your permission to use the disk space, then download and unpack everything.&lt;/p&gt;
&lt;p&gt;Some guides on the internet will tell you to either use &lt;code&gt;systemctl&lt;/code&gt; to start the agent now, or to reboot the VM. Don&amp;rsquo;t do either of those.&lt;/p&gt;
&lt;p&gt;Instead, shutdown the VM entirely from Proxmox. Then in Proxmox, with the VM selected, we need to go into &lt;code&gt;Options&lt;/code&gt; and find &lt;code&gt;QEMU Guest Agent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.21.27-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;To change an option you either double click on the line your are interested in, or select it and click edit up the top. So do that for &lt;code&gt;QEMU Guest Agent&lt;/code&gt; and select the box to enable it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.33.05-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done. We&amp;rsquo;ll select the VM and start it. If you watch the summary screen as it starts, you&amp;rsquo;ll be able to see if everything is working by watching the IP Address field. It will start off saying &lt;em&gt;Guest Agent not running&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.37.33-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;But then change once the boot gets to the stage of running all the daemons. This is an example of the hypervisor being able to use the agent to get information about what&amp;rsquo;s going on inside the VM.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.33.53-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you want to double check everything is working, you can &lt;code&gt;ssh&lt;/code&gt; into the VM, and have a look at the process with &lt;code&gt;systemctl status qemu-guest-agent&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-12.07.46-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-12.07.46-pm.png" width="938" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Or, we can look from the host. If you select the shell of the node - remember mine was called &lt;code&gt;pve&lt;/code&gt;, you have a console for the root node that owns all the virtual machines. We can run qm with &lt;a href="https://qemu.readthedocs.io/en/latest/interop/qemu-ga-ref.html"&gt;all sorts of options&lt;/a&gt; to accomplish different things. One of the most interesting is &lt;code&gt;qm guest exec&lt;/code&gt; which allows us to run whatever we&amp;rsquo;d like, as root, on the guest vm.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-12.13.17-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The number 101 in &lt;code&gt;qm guest exec 101 -- hostname&lt;/code&gt; is the Proxmox id for the server we want to access - it&amp;rsquo;s shown in the server view in the top left, and the text after &lt;code&gt;--&lt;/code&gt; is the command to execute. What&amp;rsquo;s returned is some JSON with the exit code and the output. This should be a chilling reminder that anyone with access to the proxmox account will also have root access to all your VM&amp;rsquo;s running the daemon.&lt;/p&gt;</description></item><item><title>SSH &amp; the scary warning</title><link>https://blog.iankulin.com/ssh-the-scary-warning/</link><pubDate>Wed, 08 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-the-scary-warning/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-8.41.11-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The first time you connect to a new server with ssh, it asks you something like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;➜ ~ &amp;gt; ssh ian@192.168.100.20 
The authenticity of host &amp;#39;192.168.100.20 (192.168.100.20)&amp;#39; can&amp;#39;t be established.
ED25519 key fingerprint is SHA256:ZcNTcOjO/0fOLC5iNChf8Q8MHN7z2d+VV0qz7XqH1g4.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added &amp;#39;192.168.100.20&amp;#39; (ED25519) to the list of known hosts.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you&amp;rsquo;ve said yes, it adds the server &amp;lsquo;fingerprint&amp;rsquo; to the known hosts file, then next time you ssh there, it feels safe - we know this server.&lt;/p&gt;
&lt;p&gt;But&amp;hellip;. if you&amp;rsquo;re playing around with virtual machines. Loading them, booting them, rebuilding them, cloning them etc. You might try and connect to a VM which is a different one from before, but which has the same ip address. SSH will not be happy:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;➜ ~ &amp;gt; ssh ian@192.168.100.20
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:ZcNTcOjO/0fOLC5iNChf8Q8MHN7z2d+VV0qz7XqH1g4.
Please contact your system administrator.
Add correct host key in /Users/user/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/user/.ssh/known_hosts:9
Host key for 192.168.100.20 has changed and you have requested strict checking.
Host key verification failed.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is right to be suspicious. From its point of view, it goes to Joe&amp;rsquo;s house each day, and it&amp;rsquo;s always Joe who answers the door. Today, it&amp;rsquo;s someone completely different but who says they are Joe. But since we know this is a different server, this is an expected result, so I&amp;rsquo;d like to ignore it.&lt;/p&gt;
&lt;p&gt;Although the message says to add the new fingerprint to the known_hosts file, it&amp;rsquo;s easier just to delete the old ones. Then when I try to connect to this server again, it will think it&amp;rsquo;s a new one and ask me to accept it. To delete this ip address (or hostname) out of the known hosts file, I just need to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;➜ ~ &amp;gt; ssh-keygen -R 192.168.100.20
# Host 192.168.100.20 found: line 7
# Host 192.168.100.20 found: line 8
# Host 192.168.100.20 found: line 9
/Users/user/.ssh/known_hosts updated.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And we&amp;rsquo;re good to go again. But thank you ssh for being so careful.&lt;/p&gt;</description></item><item><title>Proxmox - Installing a Virtual Machine</title><link>https://blog.iankulin.com/proxmox-installing-a-virtual-machine/</link><pubDate>Tue, 07 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-installing-a-virtual-machine/</guid><description>&lt;p&gt;Installing your first virtual machine (VM) in the Proxmox hypervisor is pretty straightforward. This post runs through those steps using Proxmox 7.3.&lt;/p&gt;
&lt;p&gt;You need an operating system for your virtual machine, I&amp;rsquo;m going to use &lt;a href="https://ubuntu.com/download/server"&gt;Ubuntu server&lt;/a&gt; in this example, but it could just as easily be &lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2016-essentials"&gt;Windows server&lt;/a&gt;, or regular windows, or one of the desktop Linux distributions. Whichever you decide, you&amp;rsquo;ll need to find and download the ISO for it. The ISO is a (usually quite large) file needed to install the operating system.&lt;/p&gt;
&lt;p&gt;Once, you&amp;rsquo;ve got the ISO for the operating system, you need to upload it into Proxmox via the web interface. The ISO will be stored in the &lt;code&gt;local&lt;/code&gt; directory style storage. If you click on it in Proxmox, you&amp;rsquo;ll see there&amp;rsquo;s actually a section for ISOs, as well as buttons there to upload an ISO from your machine, or to directly download it into ProxMox from a link.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-27-at-5.45.54-pm-copy.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Above you can seen I&amp;rsquo;ve now got two ISO images stored in my local storage. Once an image is there, you are ready to install it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.03.45-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In the top right of the Proxmox screen there are two blue buttons. One of them says &amp;ldquo;Create VM&amp;rdquo;, and that&amp;rsquo;s what we want to do. Now there will be a series of dialogs to click through and fill out. Most things we can just leave as defaults, but a few need some decisions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.06.56-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The node (your server) is already filled out. Mine is &lt;code&gt;pve&lt;/code&gt; since I just used the default name when I first installed Proxmox. The VM (virtual machine) ID is used by Proxmox to identify the server. You can change this to any three digit number you haven&amp;rsquo;t used. I&amp;rsquo;m keeping 100. Some people use this to separate their server types, for example all their production servers might be in the three hundreds.&lt;/p&gt;
&lt;p&gt;You need to come up with a name for this VM. These can only use letters and numbers - no punctuation. I like to keep them short, and describe the purpose of this VM, but perhaps you want to name yours after the OS you are using. I&amp;rsquo;m calling this one dockerhost because it&amp;rsquo;s going to host my Docker containers. Once you&amp;rsquo;ve decided, hit &lt;code&gt;next&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.15.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s where we choose the image, I&amp;rsquo;m going with the Unbuntu I downloaded earlier.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.16.46-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The System page - I&amp;rsquo;m just leaving all the defaults and hitting &lt;code&gt;next&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.18.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;On the Disks page, we go have a decision to make: how much drive space does this VM get. You&amp;rsquo;ll remember from our discussion about thin provisioning that we can allocate more disk than we have, but it&amp;rsquo;s not a good idea. The final decision about this is something you need to make considering the purpose of this VM and the space you&amp;rsquo;ve got available to you. You might need to google around for recommendations. It&amp;rsquo;s pretty easy to increase the disk size after your VM is created, but more difficult to reduce it.&lt;/p&gt;
&lt;p&gt;The Wizard has suggested 32GB for me, but the &lt;a href="https://linuxconfig.org/ubuntu-22-04-minimum-requirements"&gt;minimum spec is for 2.5GB&lt;/a&gt;. I am going to be downloading a few large containers, so 10GB seems like a good starting point for me.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.29.27-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Next is the CPU&amp;rsquo;s. Leave the defaults for everything, except you need to make a decision about the number of cores. My baby server only has two cores, but yours may have a eight or more. Proxmox will ration things out to some extent by time slicing - so you can easily run eight VM&amp;rsquo;s all allocated one core on a four core processor. And in fact, since a lot of them will probably just be sitting there waiting for something to happen, none of them will need to wait.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s probably a bad idea to allocate all of your cores to one VM, so I&amp;rsquo;m going to say &amp;lsquo;one&amp;rsquo; for mine, but you should also consider the processing needs of your VMs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.44.45-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Another important consideration is the amount of memory. Again, the needs will be determined by your use case. In my case, the minimum spec is for 1GB, but I&amp;rsquo;m planning on loading up some large containers and I have 8GB in hardware. So I&amp;rsquo;ll go with 4GB. The story with the minimum memory field is a &lt;a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_memory"&gt;little bit complicated&lt;/a&gt;, but basically, setting this lower than the max memory gives Proxmox a little bit of flexibility to share it around if you&amp;rsquo;re not using it all - which sounds like a good idea, so I&amp;rsquo;ll say my minimum is 2GB.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.47.21-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Networking in a visualized environment is a whole thing. But I have simple needs and only one hardware port, so all these defaults are fine for us.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.48.50-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The Confirm page is just a last chance to look over what we&amp;rsquo;ve chosen, then we can press &lt;code&gt;Finish&lt;/code&gt; to create our VM! A few seconds later it should be showing up in the server view. If we click on the VM in the server view, we can see the summary. It&amp;rsquo;s not very exciting yet because our machine is not running.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.57.42-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve highlighted the buttons we are going to use next in the image above. &lt;code&gt;Start&lt;/code&gt; is going to start the VM, and we&amp;rsquo;ll need to open the &lt;code&gt;Console&lt;/code&gt; to see what&amp;rsquo;s going on. Go ahead and click both of these now, and sit back in amazement.&lt;/p&gt;
&lt;p&gt;What happens next depends on what OS you are installing into this VM. You&amp;rsquo;ll just need to work your way through the questions accordingly. One point worth noticing though is that if is asks you questions like &amp;ldquo;Use the entire disk&amp;rdquo;, it&amp;rsquo;s talking about the virtual disk you allocated - not the physical disk.&lt;/p&gt;
&lt;p&gt;This operating system you&amp;rsquo;re installing now &lt;em&gt;doesn&amp;rsquo;t know&lt;/em&gt; it&amp;rsquo;s inside a virtual machine. Everything it sees - the machine bios, the screen, the memory - it&amp;rsquo;s all faked - and managed by Proxmox. You and Proxmox are playing god here. From the VM point of view, it could be installed directly on hardware. It doesn&amp;rsquo;t know the true nature of it&amp;rsquo;s world.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/pappademas_matrixkeanureeves.webp" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-4.15.24-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;While you are killing time waiting for your new OS to install, if you haven&amp;rsquo;t used noVNC before, it&amp;rsquo;s worth noticing the little slide in options on the left edge there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-6.45.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-6.45.34-pm.png" width="974" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I most commonly use it to force this window fullscreen, but in the &amp;ldquo;Extra Keys&amp;rdquo; button might be handy if you&amp;rsquo;re running a Windows OS and want the Windows key. I don&amp;rsquo;t love this console window - I&amp;rsquo;d rather SSH in and use my terminal, but it&amp;rsquo;s a handy tool that&amp;rsquo;s always going to work if the VM is running.&lt;/p&gt;</description></item><item><title>Chinese Hackers Want to steal my Hello World container</title><link>https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/</link><pubDate>Mon, 06 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/</guid><description>&lt;p&gt;A smart thing to do after setting up a server on the internet, is to set up SSH keys and then turn passwords off for SSH. The reason for this is that scanning for open port 22 on IP addresses, then brute forcing password files on them is pretty much hacker 101. So if you have passwords turned on, and especially if you have a weak password you are really inviting someone to take over your server as root and add it to their botnet army for liking Putin&amp;rsquo;s twitter posts or whatever.&lt;/p&gt;
&lt;p&gt;When I was writing &lt;a href="https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/"&gt;the post about looking for the sudo attempt&lt;/a&gt; &amp;lsquo;report&amp;rsquo;, you might have noticed some sshd timeouts:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.08.21-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what&amp;rsquo;s going on there. SSH has a timeout value of about a minute. I&amp;rsquo;d also guess those kex_exchange_identification messages are suspicious as well. I thought I&amp;rsquo;d google one of the IP addreses:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.18.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.18.14-pm.png" width="895" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Oh, so it&amp;rsquo;s China, and multiple people are reporting SSH brute force attacks:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.20.35-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Your own Aussie server on BinaryLane</title><link>https://blog.iankulin.com/your-own-aussie-server-on-binarylane/</link><pubDate>Sun, 05 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/your-own-aussie-server-on-binarylane/</guid><description>&lt;p&gt;Listening to podcasts, I&amp;rsquo;ve been jealous of US developers who seem to have masses of $5/month VPS (Virtual Private Server) options. When I looked for similar Australian offerings a few months ago, they all seem to start at around $35 which is outside of my &amp;lsquo;have a play with something&amp;rsquo; budget range.&lt;/p&gt;
&lt;p&gt;I could of course use one of the international options, but one of the main apps on my app ideas list needs to be hosted in Australia and work under Australian data privacy rules. That might be the case for Digital Ocean (or other US companies) if you select an AU server, but I&amp;rsquo;m not a lawyer. For the imaginary clients of my imaginary app, me being able to say that the hosting is with an Australian company in Australia would be a plus.&lt;/p&gt;
&lt;p&gt;I was having another look recently and discovered that &lt;a href="https://www.mammoth.com.au/"&gt;Mammoth&lt;/a&gt; (who are reputable Australian VPS providers) have a service branded &amp;ldquo;&lt;a href="https://www.binarylane.com.au/"&gt;binary lane&lt;/a&gt;&amp;rdquo; that is aimed at developers needing to quickly stand up test servers that are at this low end price point.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.binarylane.com.au/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.49.14-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I mean, &amp;ldquo;ex GST&amp;rdquo; is a bit sly, but still, this is definitely in the starting price category I&amp;rsquo;m interested in. Naturally the prices scale up as your needs do.&lt;/p&gt;
&lt;p&gt;I started hitting buttons to make an account, and true to the advertising, I was logged into an Ubuntu (you can chose from a heap of ISOs or upload your own) server that was live on the internet, hosted from Sydney, with it&amp;rsquo;s own IP address inside a minute. A few minutes after that, I&amp;rsquo;d done updates, installed Docker and had a website live on the internet.&lt;/p&gt;
&lt;p&gt;It has a nice panel interface in their web site with a console and some vital statistics and information.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-1.09.06-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Although convenient, the webpage console has a tiny lag I find unsettling, so as soon as I had the basics sorted out I switched to ssh from my MacBook terminal.&lt;/p&gt;
&lt;p&gt;This was a super painless experience, and super affordable. Since they are in the business of selling you more VPS capacity, it looks like the process of scaling up your virtual machine as needed is going to be painless as well.&lt;/p&gt;
&lt;p&gt;My plan for this VPS is to use it to learn how to add a domain, set up SSL, and eventually just keep it as a test server for apps and api endpoints.&lt;/p&gt;</description></item><item><title>sudo Incident Reports - where do they go?</title><link>https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/</link><pubDate>Sat, 04 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/</guid><description>&lt;p&gt;Even though it&amp;rsquo;s &lt;em&gt;my&lt;/em&gt; server, I still have a pang of guilt when this happens.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-10.40.43-am-copy.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I always imagine &lt;a href="https://en.wikipedia.org/wiki/Richard_Stallman"&gt;Richard Stallman&lt;/a&gt; (or someone with a similar 2000&amp;rsquo;s database administrator beard) looking at me disappointedly and shaking his head slowly.&lt;/p&gt;
&lt;p&gt;It does raise the question though - since it&amp;rsquo;s my server, shouldn&amp;rsquo;t I be getting a text message from CERN or something?&lt;/p&gt;
&lt;h4 id="where-is-this-report"&gt;Where is this report?&lt;/h4&gt;
&lt;p&gt;(&lt;a href="https://xkcd.com/838/"&gt;Relevant xkcd&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Like everything, the answer is &amp;lsquo;it&amp;rsquo;s logged&amp;rsquo;. We can use the &lt;code&gt;journalctl&lt;/code&gt; command to look at the logs, on this server that&amp;rsquo;s been running less than 20 hours, there&amp;rsquo;s already several thousand lines to look through if you just enter &lt;code&gt;journalctl&lt;/code&gt;, so I&amp;rsquo;m going to just send all the high priority logs to a file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;journalctl -p 3 &amp;gt; errors.txt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then since this just happened, it should be at the end of the file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;tail errors.txt
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Jan 28 12:10:40 enrico-rider sshd[5168]: fatal: Timeout before authentication for 110.41.153.190 port 41826
Jan 28 12:11:01 enrico-rider sshd[5170]: fatal: Timeout before authentication for 110.41.153.190 port 41856
Jan 28 12:23:15 enrico-rider sshd[5222]: fatal: Timeout before authentication for 61.177.173.39 port 29421
Jan 28 12:23:26 enrico-rider sshd[5223]: fatal: Timeout before authentication for 61.177.173.39 port 49692
Jan 28 12:23:37 enrico-rider sshd[5226]: fatal: Timeout before authentication for 61.177.173.39 port 10416
Jan 28 12:39:51 enrico-rider sshd[5517]: fatal: Timeout before authentication for 61.177.172.108 port 53867
Jan 28 12:50:06 enrico-rider sshd[5653]: error: kex_exchange_identification: Connection closed by remote host
Jan 28 13:03:53 enrico-rider sshd[5696]: error: kex_exchange_identification: Connection closed by remote host
Jan 28 13:24:58 enrico-rider sshd[5804]: fatal: Timeout before authentication for 61.177.173.39 port 46041
Jan 28 13:40:06 enrico-rider sudo[6077]: ian : user NOT in sudoers ; TTY=pts/0 ; PWD=/home/ian ; USER=root ; COMMAND=/usr/bin/docker ps
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There we go, it really has been reported!&lt;/p&gt;
&lt;h4 id="how-to-add-a-user-to-the-sudoers-file"&gt;How to add a user to the sudoers file&lt;/h4&gt;
&lt;p&gt;To avoid these terrible reports, it sounds like I need to add myself to the &amp;lsquo;sudoers file&amp;rsquo;. I won&amp;rsquo;t be able to do that as myself, so I&amp;rsquo;ll log back in as &lt;code&gt;root&lt;/code&gt; for a bit. The reason I don&amp;rsquo;t just operate as &lt;code&gt;root&lt;/code&gt; all the time is that I quite like the constant reminder that I&amp;rsquo;m about to do something administratory - so I should have a second thought before I sudo that shell command I just copied out of a stackoverflow answer.&lt;/p&gt;
&lt;p&gt;Since the error message says I&amp;rsquo;m not in the sudoers file, I should just add my name right? Well yes, and no. That is possible, but it&amp;rsquo;s slightly dangerous - it has a specific format, and if you stuff things up bad things can happen. For this reason there&amp;rsquo;s a special command to edit it (visudo) which refuses to save it if you make a mistake.&lt;/p&gt;
&lt;p&gt;The sudoers file is at &lt;code&gt;/etc/sudoers&lt;/code&gt;, if you &lt;code&gt;cat&lt;/code&gt; it, it has a heap of commented out stuff, but there&amp;rsquo;s be a section that looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# User privilege specification
root	ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo	ALL=(ALL:ALL) ALL

# See sudoers(5) for more information on &amp;#34;@include&amp;#34; directives:

@includedir /etc/sudoers.d
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That last line includes any files in the &lt;code&gt;/ect/sudoers.d&lt;/code&gt; as part of this one, so if we really did want to add &lt;code&gt;ian&lt;/code&gt; to this file, we&amp;rsquo;d do it there, but still by using the &lt;code&gt;visudo&lt;/code&gt; command to do it safely.&lt;/p&gt;
&lt;p&gt;But, we don&amp;rsquo;t need to. The &lt;code&gt;%admin&lt;/code&gt; and &lt;code&gt;%sudo&lt;/code&gt; lines are granting these permissions to groups, so all we need to do is add &lt;code&gt;ian&lt;/code&gt; to the &lt;code&gt;sudo&lt;/code&gt; group and those permissions will be granted, safely.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;usermod -a -G sudo ian
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Success:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@enrico-rider:~$ docker ps
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get &amp;#34;http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json&amp;#34;: dial unix /var/run/docker.sock: connect: permission denied
ian@enrico-rider:~$ sudo docker ps
[sudo] password for ian: 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
520ed656ef12 dockersamples/101-tutorial &amp;#34;nginx -g &amp;#39;daemon of…&amp;#34; 14 hours ago Up 14 hours 0.0.0.0:80-&amp;gt;80/tcp, :::80-&amp;gt;80/tcp pedantic_bartik
ian@enrico-rider:~$ 
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Proxmox - Storage Basics</title><link>https://blog.iankulin.com/proxmox-storage-basics/</link><pubDate>Fri, 03 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-storage-basics/</guid><description>&lt;p&gt;Once you&amp;rsquo;ve got Proxmox installed, you can point your web browser at the IP for the physical server, and use the port 8006. Log in as &lt;code&gt;root&lt;/code&gt; using the password you entered during the install. If you just accepted all the defaults during the install it will look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-7.52.16-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s discuss what you&amp;rsquo;re seeing in that &amp;lsquo;Server View&amp;rsquo; on the left there. &lt;code&gt;pve&lt;/code&gt; is the name of my &lt;em&gt;node&lt;/em&gt; - this installation of Proxmox on my physical server. If you named your server something different during the install, it will be show that name here.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Datacenter&lt;/code&gt; is just the idea of a container for all your nodes. I just have the one node, but if I had another physical server and set it up with Proxmox, it could be configured to appear in this dashboard along with my first node.&lt;/p&gt;
&lt;p&gt;Looking at my node, &lt;code&gt;pve&lt;/code&gt;, it has two storage items. Both are &amp;rsquo;local&amp;rsquo; which means they are physically on this machine. A common setup would be to have a Network Attached Storage (NAS) and have Proxmox use that for the Virtual Machine (VM) images. A big benefit of that would be the ability to move the VMs between nodes (physical servers) in your datacentre if needed - for example if a server failed.&lt;/p&gt;
&lt;p&gt;Since I only have local storage, you might be wondering why the installer set me up with two. Let&amp;rsquo;s click on the first one &lt;code&gt;local (pve)&lt;/code&gt; and look at the summary for it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-8.14.04-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So we can see in the summary at the right, that the type of this storage is &amp;lsquo;Directory&amp;rsquo;. Meaning that this is just a directory in the host (internally, Proxmox is just a specialised Linux distribution - in theory we could drop in to bash and look at this directory).&lt;/p&gt;
&lt;p&gt;The summary helpfully tells us the content for this storage as well, saying &lt;code&gt;VZDump backup file, ISO image, Container template&lt;/code&gt;. These are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VZDump backup file - backups of VMs or containers&lt;/li&gt;
&lt;li&gt;ISO image - the images that VMs are created from&lt;/li&gt;
&lt;li&gt;Container template - images that containers are created from. For the moment, you can just imagine containers as lightweight VMs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see from the graph that I&amp;rsquo;ve used a bit of this storage already. That because I have some ISO&amp;rsquo;s and container templates already downloaded to play with for the next post and stored in the local directory type storage.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s click on the other storage &lt;code&gt;local-lmv (pve)&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-8.28.41-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-8.28.41-pm.png" width="999" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We already discussed the &lt;code&gt;local&lt;/code&gt; part of the name &lt;code&gt;local-lvm (pre)&lt;/code&gt; means it&amp;rsquo;s on this machine/node. LVM stands for Logical Volume Manager. An LVM is an abstraction from the physical disk. A single LMV might actually be made of of a number of physical partitions, or even drives. Regardless of this, and LVM presents as a single volume at the software layer.&lt;/p&gt;
&lt;p&gt;This storage is used for disk for the VMs we&amp;rsquo;ll be running. If you look at the use graph, you can see that about 45 minutes ago, I had been using 10GB. That&amp;rsquo;s because I had a VM and a couple of containers configured. When I created them, part of that process is to specify how much disk storage each VM is allowed to use. Then that allocation is stored here.&lt;/p&gt;
&lt;p&gt;You can see in the summary, that the type of this storage is &lt;code&gt;LVM-Thin&lt;/code&gt;. The Thin part of this description means that although a hunk of storage is allocated, if it&amp;rsquo;s not actually used, then it&amp;rsquo;s still available to be allocated. For example, if you have a 100GB LVM, then you allocate 50GB to a VM, then on this display, you&amp;rsquo;ll see that 50 has been used up. But if the VM is only actually using 5GB, you&amp;rsquo;ll still effectively have 95GB to allocate.&lt;/p&gt;
&lt;p&gt;This is a great idea, until those VM&amp;rsquo;s &lt;em&gt;do&lt;/em&gt; start using up most of their allocation, because at that point the VM&amp;rsquo;s will start getting IO errors. Of course, since it&amp;rsquo;s an LVM, you&amp;rsquo;ll be able to add more storage to it before that happens if you&amp;rsquo;re keeping an eye on it. Thin provisioning was invented for companies that sell virtual server services. Their customers rarely use 100% of the hard disk space they pay for, so it&amp;rsquo;s highly profitable to use thin provisioning of storage and resell the same disk space multiple times.&lt;/p&gt;</description></item><item><title>Upgrade Cycle</title><link>https://blog.iankulin.com/upgrade-cycle/</link><pubDate>Thu, 02 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/upgrade-cycle/</guid><description>&lt;p&gt;Now that I&amp;rsquo;ve seen I can easily stand up VM&amp;rsquo;s on this baby server, it&amp;rsquo;s apparent the first limitation I&amp;rsquo;ll run into is RAM. It has two laptop sized memory slots that can take up to 8GB apiece. So it could easily be doubled, but at a cost of around $70.&lt;/p&gt;
&lt;p&gt;While I&amp;rsquo;m looking on eBay for RAM, the algorithm thinks I might be interested in this.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-3.47.47-pm-2.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/upgrades.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;While I&amp;rsquo;m looking at the specs (4 cores - the current one has 2, double the RAM, bigger disk), eBay is like &amp;ldquo;Hey, how about this 20% off discount code - is thAt soMetHing ThAt miGHt HeLp yoU deCiDe?&amp;rdquo;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/whynot.jpg" width="1000" alt=""&gt;
&lt;p&gt;The rationalisation is that even with both, I&amp;rsquo;ve only spent the same as a single Pi4 for a lot more computing power, and I could &lt;em&gt;in theory&lt;/em&gt; sell the first one (trying to get a licensed Windows back on to it could be an interesting challenge).&lt;/p&gt;
&lt;p&gt;And anyway, I don&amp;rsquo;t smoke, and if your deduct the cost of the RAM, it&amp;rsquo;s only the price of three packs of cigarettes&amp;hellip;.&lt;/p&gt;
&lt;p&gt;and really it&amp;rsquo;s an investment in my future job prospects&amp;hellip;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-4.07.13-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-4.07.13-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Proxmox Hypervisor</title><link>https://blog.iankulin.com/proxmox-hypervisor/</link><pubDate>Wed, 01 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-hypervisor/</guid><description>&lt;p&gt;I &lt;a href="https://blog.iankulin.com/pi-server/"&gt;mentioned a while ago&lt;/a&gt; that the price of the &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/specifications/"&gt;Raspberry Pi4&lt;/a&gt; was getting such that it&amp;rsquo;s smarter to purchase one of the little business workstations instead. Depsite having little need for such a thing, I went ahead and bought an &lt;a href="https://support.hp.com/au-en/document/c04266271"&gt;HP Elitedesk 800 G1&lt;/a&gt; &amp;ldquo;mini&amp;rdquo; PC. It has 8GB RAM (which is the max for the Pi4) as well as a 128GB SDD, the processor is an Intel i5.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-10.54.25-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This compares pretty well with the 8GB Pi4 which only has a fraction of the storage (on an SD card) at around $400. One area where the Pi would have an edge might be in power consumption - I expect it would be a bit less. One possible catch for young players is that the HP has a &amp;lsquo;display port&amp;rsquo; rather than HDMI for the screen connection, so pick up a $5 adapter if you&amp;rsquo;re getting one. The metal case and nice finishing on the HP actually looks really great in my office compared with my Pi 3b+ dev server that&amp;rsquo;s sort of hanging on the end of a cat5 cable.&lt;/p&gt;
&lt;p&gt;My reason (excuse) for getting the HP is that I&amp;rsquo;m quite interested in getting some experience with (having a play with) deploying web apps in Docker containers. I&amp;rsquo;m also thinking that having better Linux skills and some understanding of devops would be helpful for working in IT in any capacity.&lt;/p&gt;
&lt;p&gt;Virtualization (running several servers inside one physical server) has a number of benefits anyway, but when your main purpose is to fiddle around with things, it&amp;rsquo;s the perfect tool. How this works is that you have a layer between the hardware and the virtual machines (VMs) called the hypervisor. The hypervisor deals with the hardware, and allocates resources to the separate VMs it is hosting. It&amp;rsquo;s probably worth underlining separate in that sentence. The VMs can be set up to communicate via networking, or have access to shared storage, but they are running independently. If one of them crashes for some reason, the others are not affected.&lt;/p&gt;
&lt;p&gt;In practice this means that I can install and run a number of different operating systems on my server. They can be stopped, started, exported, deleted etc all without affecting each other or any &amp;lsquo;critical&amp;rsquo; systems I&amp;rsquo;ve got running in the same box. There&amp;rsquo;s a number of choices for virtualization software. Microsoft has Hyper-V, VMWare is probably the most famous and has a reduced feature, free version called ESXi. That would probably be a good choice if you want directly transferable skills as VMWare have a substantial profile in the commercial world.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://enlyft.com/tech/virtualization-platforms"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-12.40.24-pm.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But one of the missing features from ESXi is central management, and I want to play with my toys, and anyway, all the cool kids are using &lt;a href="https://www.proxmox.com/en/proxmox-ve"&gt;Proxmox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-12.25.13-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-12.25.13-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Like a couple of others on this list, Proxmox is built on Linux, and is a great choice for a home server setup. It is available for commercial use with different (&lt;a href="https://www.proxmox.com/en/proxmox-ve/pricing"&gt;paid&lt;/a&gt;) tiers of support.&lt;/p&gt;
&lt;h4 id="installing-proxmox"&gt;Installing Proxmox&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s quite a few guides around for installing and setting up Proxmox, I won&amp;rsquo;t rehash all the steps here, but rather just make a couple of points, especially in relation to the HP 800 as a host.&lt;/p&gt;
&lt;p&gt;I followed &lt;a href="https://www.youtube.com/watch?v=Flw_ycAwT3E"&gt;this video from Darin Wood&lt;/a&gt;. He did lead me a bit astray by fiddling around with the storage options using the command line. That&amp;rsquo;s probably great if you are going to use an external NAS, but form my situation it would have been better to just leave all the storage defaults as they where - so when Darin gets to those commands just skip over them.&lt;/p&gt;
&lt;p&gt;If Darin is a bit dry for you, a very enthusiastic alternative might be &lt;a href="https://www.youtube.com/watch?v=Flw_ycAwT3E"&gt;this series&lt;/a&gt; from Jeremy Cioara (&lt;a href="https://www.youtube.com/watch?v=Flw_ycAwT3E"&gt;Viatto&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Other points were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I used Balena Etcher to flash the USB thumbdrive with the 7.3 &lt;a href="https://www.proxmox.com/en/downloads/category/iso-images-pve"&gt;Proxmox ISO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;To get the BIOS settings on the HP, you mash the F10 key on start up, but if you just want to choose the boot device, F9 does a better job of that (because you don&amp;rsquo;t need to change it back later).&lt;/li&gt;
&lt;li&gt;A couple of places mentioned having to turn virtualization on in the BIOS of the HP - it&amp;rsquo;s in the &lt;a href="https://h30434.www3.hp.com/t5/Desktops-Archive-Read-Only/How-to-turn-on-the-virtualization-on-hp-elitedesk-800-g1/td-p/3958272"&gt;BIOS settings under security&lt;/a&gt;, but mine was already on, so perhaps it&amp;rsquo;s on by default. &lt;a href="https://en.wikipedia.org/wiki/X86_virtualization#Hardware-assisted_virtualization"&gt;AMD and Intel processors have some special features to support virtualization&lt;/a&gt;, so that&amp;rsquo;s probably what that&amp;rsquo;s about.&lt;/li&gt;
&lt;li&gt;There&amp;rsquo;s a couple of config files to edit so you can do the updates - this is the step to make your life a bit complicated for not paying for Proxmox support. They are well explained in all the guides.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apart from that, I pretty much just followed all the instructions and used the defaults (I used a made up email address, and &lt;code&gt;local&lt;/code&gt; as my hostname), and I was soon up and running. The only other thing I did was go into my router settings to reserve the IP address that the Proxmox machine had picked up from the DHCP server to prevent (the low chance of) it changing in the future.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-26-at-1.29.27-pm-copy.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Expired Packages Part II</title><link>https://blog.iankulin.com/expired-packages-part-ii/</link><pubDate>Tue, 31 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/expired-packages-part-ii/</guid><description>&lt;p&gt;Following on from the previous post&amp;hellip;&lt;/p&gt;
&lt;p&gt;I went the nuclear route - deleted the node_modules folder, package-lock.json and installed the packages from packages.json. I still had some errors, but the react app at least ran correctly. Also, the messages are a bit more intelligible, and all of them cascade from this one.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# npm audit report

nth-check &amp;lt;2.0.1
Severity: high
Inefficient Regular Expression Complexity in nth-check - https://github.com/advisories/GHSA-rp65-9cf3-cjxr
fix available via `npm audit fix --force`
Will install react-scripts@2.1.3, which is a breaking change
node_modules/svgo/node_modules/nth-check
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From my, admittedly ignorant, viewpoint, there&amp;rsquo;s a couple of weird things going on here.&lt;/p&gt;
&lt;p&gt;The first is how the hell is installing &lt;a href="mailto:react-scripts@2.1.3"&gt;react-scripts@2.1.3&lt;/a&gt; a good idea, when the &lt;a href="https://www.npmjs.com/package/react-scripts"&gt;current version is 5.0.1&lt;/a&gt;. That does not seem like a good solution.&lt;/p&gt;
&lt;p&gt;The second is that is that the currently installed version of nth-check seems like it is 2.1.1 which is the current version, and certainly &amp;gt;2.0.1 which is the complaint. My basis for this claim is this encouraging part of package-lock.json:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-23-at-1.39.10-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;But if I check the installed version using &lt;code&gt;npm list nth-check&lt;/code&gt;, I get this bad news:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-23-at-1.58.13-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So one version of css-select is using an old version of nth-check, likely this is the source of my troubles.&lt;/p&gt;
&lt;p&gt;As far as I can make out, the package-lock.json file&amp;rsquo;s purpose is to lock in particular versions of packages. If it&amp;rsquo;s committed with the rest of your code, it guarantees that when you rebuild the app, it will be the same as the one committed, without the need commit all the node modules you are depending on. Generally I don&amp;rsquo;t think you are meant to directly edit it, but it suddenly seems like a good idea.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s about five dependencies on this package in package-lock.json, and I notice all of them except this one, start with the ^caret.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;nth-check&amp;#34;: {
 &amp;#34;version&amp;#34;: &amp;#34;1.0.2&amp;#34;,
 &amp;#34;resolved&amp;#34;: &amp;#34;https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz&amp;#34;,
 &amp;#34;integrity&amp;#34;: &amp;#34;sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==&amp;#34;,
 &amp;#34;requires&amp;#34;: {
 &amp;#34;boolbase&amp;#34;: &amp;#34;~1.0.0&amp;#34;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In general, I think I should be doing this in packages.json, but the nth-check is not in there.&lt;/p&gt;
&lt;p&gt;After fruitlessly googling around and asking in a couple of discords, I check with ChatGPT to see what she thought.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-24-at-10.14.51-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-24-at-10.14.51-am.png" width="828" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, I&amp;rsquo;d done all that, and a couple of humans had already told me not to jigger with &lt;code&gt;package-lock.json&lt;/code&gt;, so that was my next stop - I edited it to add the caret and tried &lt;code&gt;npm install&lt;/code&gt; - it just changes it back, which make sense.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s another install script &lt;code&gt;npm ci&lt;/code&gt; which is supposed to use the &lt;code&gt;package-lock.json&lt;/code&gt; rather than doing it&amp;rsquo;s own check, that didn&amp;rsquo;t help either.&lt;/p&gt;
&lt;p&gt;I went back to googling, focusing on the react-scripts (which is basically responsible for building the template React app) and found &lt;a href="https://github.com/facebook/create-react-app/issues/11174"&gt;this issue&lt;/a&gt; on github.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/facebook/create-react-app/issues/11174"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-24-at-11.19.20-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Basically, it&amp;rsquo;s claimed to be a problem in how &lt;code&gt;npm audit&lt;/code&gt; works, and can&amp;rsquo;t be a real vulnerability since it&amp;rsquo;s just a tool being used during dev. That still doesn&amp;rsquo;t answer why they don&amp;rsquo;t just update their code to use the newer version on &lt;code&gt;nth-check&lt;/code&gt;, but in any case, it can be safely ignored.&lt;/p&gt;
&lt;p&gt;For people using CI tools that depend on an error free build, the react-scripts can be moved to a different section of the &lt;code&gt;packages.json&lt;/code&gt; file and an argument passed to npm audit to ignore those dependencies.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-24-at-11.15.54-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So, what have I learned from this? I should have done more looking for answers for the exact error, instead of logically coming up with solutions and then searching for and pursuing them. But also, I have a clear idea of what &lt;code&gt;packages.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; do now!&lt;/p&gt;</description></item><item><title>Expired packages</title><link>https://blog.iankulin.com/expired-packages/</link><pubDate>Mon, 30 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/expired-packages/</guid><description>&lt;p&gt;At several points in the &lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;Complete Web Developer&lt;/a&gt; course, deprecated packages have been used, with the slide before the video explaining what&amp;rsquo;s happening, and giving a work around, or sometimes - as is the case for the bit I&amp;rsquo;m just starting - exhorting the benefits of dropping you into a non-working mess and having you figure it out yourself.&lt;/p&gt;
&lt;p&gt;While this argument can be reasonably made - that figuring things out on your own is a valuable skill - it&amp;rsquo;s also a useful fig leaf to cover up the fact that they haven&amp;rsquo;t bothered to fix the course to make it work out of the box.&lt;/p&gt;
&lt;p&gt;A recent example of this was the particles.js library. The card preceding the video says the library used in the video is no longer available, but hey, this other one is pretty much the same. And, I mean it was very similar, but the instructions on it&amp;rsquo;s npm page for using it were different from what was in the video, the video instructions didn&amp;rsquo;t work with it, and following the install page instructions led to one of those repeated &lt;code&gt;npm audit fix --force&lt;/code&gt; cycles where you keep just breaking more things.&lt;/p&gt;
&lt;p&gt;I got it going eventually, but only by starting with a new create-react app with the particles template then slowly adding back my previous code from git a bit at a time and fixing errors as they came up. The whole process from watching the video to having the project working as per the video was probably four or five hours. Was this a good investment of learning time? Probably not.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_3996.jpg" width="600" alt=""&gt;
&lt;p&gt;Straight out of that experience, Andrei advises that the next section uses a deprecated api, that he&amp;rsquo;s persisting with because he wants to teach REST apis. In order to make it work, we need to downgrade the react-scripts version which he assures will not cause any problems. Naturally there is a list of critical warnings, and the server can&amp;rsquo;t start because of a heap of errors.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-23-at-11.14.08-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Starting the development server...

Error: error:0308010C:digital envelope routines::unsupported
 at new Hash (node:internal/crypto/hash:71:19)
 at Object.createHash (node:crypto:133:10)
 at module.exports (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/util/createHash.js:135:53)
 at NormalModule._initBuildHash (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/NormalModule.js:417:16)
 at handleParseError (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/NormalModule.js:471:10)
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/NormalModule.js:503:5
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/NormalModule.js:358:12
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/loader-runner/lib/LoaderRunner.js:373:3
 at iterateNormalLoaders (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
 at iterateNormalLoaders (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/react-scripts/scripts/start.js:19
 throw err;
 ^

Error: error:0308010C:digital envelope routines::unsupported
 at new Hash (node:internal/crypto/hash:71:19)
 at Object.createHash (node:crypto:133:10)
 at module.exports (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/util/createHash.js:135:53)
 at NormalModule._initBuildHash (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/NormalModule.js:417:16)
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/NormalModule.js:452:10
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/webpack/lib/NormalModule.js:323:13
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/loader-runner/lib/LoaderRunner.js:367:11
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/loader-runner/lib/LoaderRunner.js:233:18
 at context.callback (/Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/loader-runner/lib/LoaderRunner.js:111:13)
 at /Users/ianbailey/Developer/CWD/facerecognitionbrain/node_modules/babel-loader/lib/index.js:59:103 {
 opensslErrorStack: [ &amp;#39;error:03000086:digital envelope routines::initialization error&amp;#39; ],
 library: &amp;#39;digital envelope routines&amp;#39;,
 reason: &amp;#39;unsupported&amp;#39;,
 code: &amp;#39;ERR_OSSL_EVP_UNSUPPORTED&amp;#39;
}

Node.js v18.12.1
➜ ~/Developer/CWD/facerecognitionbrain &amp;gt; git:(main) ✗ 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Clearly this (or perhaps the next step) is a spot in the course that receives a few complaints, since Andrei goes to the effort of making a special video in front of it extolling the virtues of solving your own problems, and saying he has a script that runs weekly on his code for this section proving it does work. He also explains that after the app is built with the deprecated REST API, he&amp;rsquo;d got a new video using the new package, then after that we&amp;rsquo;ll be removing all this code anyway to move this functionality to a node server.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;ve got a few options in front of me.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Try and wind back all of the packages that are causing problems until the app runs again.&lt;/li&gt;
&lt;li&gt;Just watch the REST API content but don&amp;rsquo;t bother trying it.&lt;/li&gt;
&lt;li&gt;Clone Andrie&amp;rsquo;s project and see if he&amp;rsquo;s actually managed to get it going with the outdated script somehow and figure out how and bring that technique over to my project.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(2) is the logical option, but it&amp;rsquo;s frustrating since the point of this section is to learn REST API&amp;rsquo;s. In an ideal world, ZTM would have rewritten this part of the course to use a REST API that works. No doubt this would mean coming up with a new app and remaking some videos, but really that&amp;rsquo;s what they need to do.&lt;/p&gt;
&lt;p&gt;Slightly compounding the frustration is that support for the course is community based centred around a Discord, and there&amp;rsquo;s no channel for this course.&lt;/p&gt;
&lt;p&gt;I decide to watch the videos first then make a decision, and in the meantime revert back to the current version of react-script. Of course, when I do this, it suddenly has some critical vulnerabilities in webpack 🤦. I run the force, and now I&amp;rsquo;ve got 80 critical vulnerabilities and the server won&amp;rsquo;t run due to errors five deep in Babel somewhere.&lt;/p&gt;
&lt;p&gt;To be continued&amp;hellip;&lt;/p&gt;</description></item><item><title>CodePen</title><link>https://blog.iankulin.com/codepen/</link><pubDate>Sun, 29 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/codepen/</guid><description>&lt;p&gt;I think I&amp;rsquo;ve written about CodePen before, its a site that allows users to quickly put together HTML, CSS &amp;amp; JS and see the results as they edit. Users &amp;lsquo;pens&amp;rsquo; are public and can be tagged, so it also serves as a repository of examples.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to host incredibly complex artefacts, such as this &lt;a href="https://codepen.io/ricardoolivaalonso/pen/RwBZMGB"&gt;3D Sony Walkman&lt;/a&gt;, but what I mostly use it for is to work out simple things - like how to &lt;a href="https://codepen.io/IanKulin/pen/wvxrZxW"&gt;collapse a row of text into a column&lt;/a&gt; with a media query.&lt;/p&gt;
&lt;p&gt;The alternative to a site like this for those jobs would be to create a couple of scratch files and open up another instance of VS Code, but this is quicker, the results are immediately available, and are saved if I ever need to go back to them.&lt;/p&gt;
&lt;p&gt;The free tier more than covers my needs, but the paid tier (about $10 a month) includes being able to make your pens private and hosting image assets there. The private options would be nice to hide your tests if you were going to use it as a portfolio - which I don&amp;rsquo;t doubt some designers would.&lt;/p&gt;
&lt;p&gt;As a bonus, pens can easily be embedded in blog posts on WordPress.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://codepen.io/IanKulin/pen/BaPjmNv"&gt;View embed&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Using the Community</title><link>https://blog.iankulin.com/using-the-community/</link><pubDate>Fri, 27 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-the-community/</guid><description>&lt;p&gt;You can&amp;rsquo;t always successfully google problems when you&amp;rsquo;re starting out - usually because you don&amp;rsquo;t know the correct terminology for the issue or solution. Often you might still get a newbie StackOverflow hit, but when there&amp;rsquo;s not even that, you need a human to help out.&lt;/p&gt;
&lt;p&gt;One of the things &lt;a href="https://zerotomastery.io/"&gt;ZTM&lt;/a&gt; do with their courses is to have a Discord based community, then set tasks to encourage it&amp;rsquo;s use - for example one of the exercises I&amp;rsquo;ve already had was to go there and answer a question. Earlier ones were to introduce yourself and to find a partner to work with - both of which would have forced anyone not used to Discord to figure it out.&lt;/p&gt;
&lt;p&gt;As always when interacting with a community to get help, you need to have done your homework, and to have taken care to supply enough context that someone actually can answer it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-18-at-4.43.55-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the CodePen of exactly centering an image on a div.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://codepen.io/IanKulin/pen/rNrGREy"&gt;View embed&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Openlayers &amp; Vite</title><link>https://blog.iankulin.com/openlayers-vite/</link><pubDate>Thu, 26 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/openlayers-vite/</guid><description>&lt;p&gt;In Randy Pausch&amp;rsquo;s &lt;a href="https://www.youtube.com/watch?v=ji5_MqicxSo"&gt;last lecture&lt;/a&gt; he talks about the benefit of brick walls in our lives - they tell us how much we really want something. Software development is full of these brick walls - things we want to do, but there&amp;rsquo;s a barrier to achieving it. Will we persevere and accomplish the thing, give up, or some other compromise.&lt;/p&gt;
&lt;p&gt;In heroic tales, the protagonist overcomes all obstacles to achieve the goal. In life and especially in software development, that&amp;rsquo;s not always the smart thing to do - to stubbornly invest in an outcome, often disproportionately to the benefit. Here&amp;rsquo;s my brick walls from today.&lt;/p&gt;
&lt;p&gt;I had made a web page showing text of the updated lat/long of the ISS. It met the requirements, but was not very exciting. The obvious thing to do with this information would be to show it on a map.&lt;/p&gt;
&lt;h4 id="moving-map"&gt;Moving map&lt;/h4&gt;
&lt;p&gt;Obviously there are lots of options for this, none of them were as simple as I would have liked, and I wanted a free one. &lt;a href="https://openlayers.org/"&gt;OpenLayers&lt;/a&gt; sounded like it would do the job, and the download to showing a map in the demo app time was about five minutes - we&amp;rsquo;re off to a good start.&lt;/p&gt;
&lt;h4 id="wall-1---complicated-library"&gt;Wall 1 - complicated library&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;m not joking when I say OpenLayers is comprehensive. It appears to do everything you could possibly want. The flipside of this is complexity. I managed to get the map scrolling so the ISS position was the centre of the map, and discovered how to add a dot as a feature to represent the ISS, but then when I wanted to get that dot to move I was increasingly out of my depth.&lt;/p&gt;
&lt;p&gt;Of course, there&amp;rsquo;s nothing magical here - it&amp;rsquo;s a gigantic, well documented API, and there&amp;rsquo;s a smattering on answers on StackOverflow. I&amp;rsquo;m smart enough to get my head around it, find some repos using it and dissect them and so on. The question is one of cost/benefit. I&amp;rsquo;m essentially building a toy for my own amusement - would the time be better spent on moving on to the next part of my course?&lt;/p&gt;
&lt;h4 id="wall-2---overlapping-images"&gt;Wall 2 - overlapping images&lt;/h4&gt;
&lt;p&gt;I have a better idea anyway - since the ISS should be at the centre of the map, I can just stick a picture of the ISS there. I assume this is possible with CSS?&lt;/p&gt;
&lt;p&gt;This turns out to be a low wall. The CSS for my image is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;img {
 position: absolute;
 top: 50%;
 left: 50%;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The container (that holds the map and this image of the ISS) has &lt;code&gt;position:relative&lt;/code&gt; this all works on the first try, and my ISS png is positioned perfectly in the centre of the map at it&amp;rsquo;s correct location, and it changes correctly as the window is resized.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-17-at-6.25.04-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h4 id="wall-3---vite"&gt;Wall 3 - Vite&lt;/h4&gt;
&lt;p&gt;The process of creating the demo app from the Getting Started instructions for OpenLayers involves something called &lt;a href="https://vitejs.dev/"&gt;Vite&lt;/a&gt;. Apparently it&amp;rsquo;s a &amp;ldquo;Next Gen Frontend Tooling&amp;rdquo; which does not really tell me much. I&amp;rsquo;ve heard it mentioned in passing a couple of times, and know it&amp;rsquo;s pronounced veet. A bit like when I was using React, there&amp;rsquo;s a node build step that fills a directory with the distributed files - I&amp;rsquo;m guessing this is something to do with pulling in just the parts of the giant library I&amp;rsquo;m using - but there&amp;rsquo;s 260K of JavaScript and a 1.6MB .map in there, so I feel some handcrafting or a CDN should be involved.&lt;/p&gt;
&lt;p&gt;If the .map file is in fact the map, that&amp;rsquo;s actually pretty amazing for a map of the entire world that I can zoom down to individual street level - so it&amp;rsquo;s probably not, but either way it&amp;rsquo;s massive overkill for what this application needs. All I really need for this is a 1000x500 px image of a map of the world. Still here we are.&lt;/p&gt;
&lt;p&gt;So I don&amp;rsquo;t really understand Vite, but I can follow the instructions enough to get the live server running for development, and to build the distribution files.&lt;/p&gt;
&lt;h4 id="wall-4---github-pages"&gt;Wall 4 - GitHub Pages&lt;/h4&gt;
&lt;p&gt;I love GitHub pages, and have a couple of apps up on it &lt;a href="https://iankulin.github.io/calc/"&gt;here&lt;/a&gt; and &lt;a href="https://iankulin.github.io/todo001/"&gt;here&lt;/a&gt;. It works by specifying a branch (which can include main) in the repo to expose, or by starting a repo with your GitHub username - for example, mine would be &lt;a href="https://iankulin.github.io/"&gt;iankulin.github.io&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I had my original (non-map) version of the iss location working on Pages as a subdirectory of the main iankulin page. For the map version, I publish it directly from the docs directory of the main branch of it&amp;rsquo;s own repo. About this time, I realised that my original version was mysteriously not working. I would have been okay with that, but neither was the new version, which as usual &amp;lsquo;wOrKEd On mY mAChiNe&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;I can see now what the problem was with my original version. I had copied the source files of the non-map version to the /iss folder of my main github.io page, so it&amp;rsquo;s address had been &lt;a href="https://iankulin.github.io/iss/"&gt;https://iankulin.github.io/iss/&lt;/a&gt;. Meanwhile, I&amp;rsquo;d named the new map-version repo &amp;lsquo;iss&amp;rsquo; and published it from that repo - so its link was, you guessed it &lt;a href="https://iankulin.github.io/iss/"&gt;https://iankulin.github.io/iss/&lt;/a&gt; After a bit of shuffling around, the old one is now working correctly at &lt;a href="https://iankulin.github.io/cwd/iss/"&gt;https://iankulin.github.io/cwd/iss/&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="wall-5---vite-again"&gt;Wall 5 - Vite again?&lt;/h4&gt;
&lt;p&gt;Meanwhile, the new version, with the moving map was clearly loading the HTML, but no sign of the ISS image, the .css or .js&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m in unfamiliar territory here with Vite, but since I started with the bundled my-app app, I&amp;rsquo;m sort of expecting everything to just work, unless it&amp;rsquo;s something to do this the GitHub Pages hosting. I do go down a rabbit hole about how they are designed for Jekyll. But a simpler explanation might be the links to those resources in the HTML. They all had a forward slash in front of them, like&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/assets/iss.d6272ccf.png&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;where as I would have been expecting&lt;/p&gt;
&lt;p&gt;&lt;code&gt;assets/iss.d6272ccf.png&lt;/code&gt; or even &lt;code&gt;./assets/iss.d6272ccf.png&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;(a side note - I have no idea why *ix systems use &lt;code&gt;./&lt;/code&gt; to mean the current directory. It would be like prefixing everyone&amp;rsquo;s name with &lt;em&gt;parent&amp;rsquo;s child&lt;/em&gt; when you&amp;rsquo;re talking to them).&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/soanyway.jpg" width="74" alt=""&gt;
&lt;p&gt;So anyway, I just started manually editing the built code which doesn&amp;rsquo;t seem like great practice:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-18-at-10.16.35-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It didn&amp;rsquo;t immediately solve my problem, but I&amp;rsquo;m not sure what the update rate for Pages is, and when I looked the next morning, it was working.&lt;/p&gt;
&lt;h4 id="wall-6---the-last-90"&gt;Wall 6 - The last 90%&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Ninety%E2%80%93ninety_rule"&gt;Tom Cargill&lt;/a&gt; (Bell Labs in the 80&amp;rsquo;s) is credited with noticing that the first 90% of the code accounts for the first 90% of the development time and the remaining 10% accounts for the remaining 90%. It certainly feels right that solving the problems and getting the MVP/prototype up and working - aka the fun bit, is not even the half way point in a project.&lt;/p&gt;
&lt;p&gt;Even in this very simple app, with the shortcuts I&amp;rsquo;ve made, there&amp;rsquo;s still significant work to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I need to disable the user&amp;rsquo;s ability to scroll around the map - since it breaks the illusion that the ISS picture is map of the map (or correctly render it as a map layer).&lt;/li&gt;
&lt;li&gt;There needs to be a delay or something so the first render of the map is based on a fetched ISS position.&lt;/li&gt;
&lt;li&gt;It ISS picture is not quite in the centre - it&amp;rsquo;s not noticeable on a laptop, but on mobile it&amp;rsquo;s very evident.&lt;/li&gt;
&lt;li&gt;Actually, the whole mobile experience needs a bit of work. That lat/long display does not fit as written. Better to pop the elements into a grid so we can stack them when the screen gets narrow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Will I do these things? Is the trip worth the gas? I need to balance progressing in the course with the things I can learn by finishing everything off carefully. In this case, since I&amp;rsquo;m way, way past what the course exercise was, I&amp;rsquo;ll leave it as is. Eventually I&amp;rsquo;ll work back through my GitHub with a recruiter&amp;rsquo;s eye, and things like this will need fixed to production standards or made private.&lt;/p&gt;</description></item><item><title>APIs - http &amp; https Mixed Content error</title><link>https://blog.iankulin.com/apis-http-https-mixed-content-error/</link><pubDate>Tue, 24 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/apis-http-https-mixed-content-error/</guid><description>&lt;p&gt;&amp;lt;img src=&amp;quot;/images/screen-shot-2023-01-16-at-4.45.53-pm.jpg alt=&amp;ldquo;Mixed Content: The page at &amp;lsquo;&lt;URL&gt;&amp;rsquo; was loaded over HTTPS, but requested an insecure resource '&lt;/p&gt;
&lt;p&gt;Ran into a little bump today - I was calling a &lt;a href="http://open-notify.org/Open-Notify-API/ISS-Location-Now/"&gt;cool API&lt;/a&gt; that gives the current location of the International Space Station. In a classic case of &amp;ldquo;it worked on my machine&amp;rdquo; it worked perfectly in the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer"&gt;Live server&lt;/a&gt; in VS Code on my laptop, but when I pushed it up to my GitHub space, it didn&amp;rsquo;t work - throwing the error:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;script.js:5 Mixed Content: The page at &amp;#39;https://iankulin.github.io/iss/index.html&amp;#39; was loaded over HTTPS, but requested an insecure resource &amp;#39;http://api.open-notify.org/iss-now.json&amp;#39;. This request has been blocked; the content must be served over HTTPS.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It turns out, as a security measure, it&amp;rsquo;s not possible for a page served under an SSL certificate to call a non-secure endpoint. This makes sense since a user would be reassured by a https page knowing no data was being leaked in the URL or other calls - but if this could be circumvented by some JavaScript that would be bad.&lt;/p&gt;
&lt;p&gt;It worked fine on my machine since it it was being served as http and calling an http api, but when I pushed it up to GitHub Pages (which is https) I ran into the error.&lt;/p&gt;
&lt;p&gt;I tried changing the API call to https, but unfortunately that server doesn&amp;rsquo;t have the SSL certificate in place to allow that. I also tried requesting the whole page from GitHub Pages as http, but it won&amp;rsquo;t allow that. Googling around, there does not seem to be any way to disable this (which makes sense).&lt;/p&gt;
&lt;p&gt;Luckily, I found another api &lt;a href="https://wheretheiss.at/w/developer"&gt;wheretheiss.at&lt;/a&gt; which does allow https, so crisis averted.&lt;/p&gt;</description></item><item><title>React code is not HTML</title><link>https://blog.iankulin.com/react-code-is-not-html/</link><pubDate>Sun, 22 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/react-code-is-not-html/</guid><description>&lt;p&gt;I was looking at this ugly code in a React app:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;div style={{overflow: &amp;#39;scroll&amp;#39;, border: &amp;#39;1px solid black&amp;#39;, height: &amp;#39;600px&amp;#39; }}&amp;gt;
 { props.children }
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since I don&amp;rsquo;t need any of those CSS properties to change at any stage, I could just convert it to pure HTML/CSS right? Well no:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-09-at-4.26.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-09-at-4.26.54-pm.png" width="826" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The newbie trap I&amp;rsquo;ve fallen for here is that although that &lt;code&gt;&amp;lt;div style= tag&lt;/code&gt; looks like HTML, it&amp;rsquo;s actually not. It&amp;rsquo;s not a template that will be filled out in the build step, it&amp;rsquo;s React code that will be used to mutate the virtual DOM.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not clear to me why React even bothers with this faux-HTML. If it just committed to having developers work with some sort of set of React DOM objects in JS, it would eliminate a couple of layers of complexity - although at the cost of having to learn a new thing. Once you&amp;rsquo;ve committed to transpilation, you might as well go the whole way!&lt;/p&gt;
&lt;p&gt;It does make me wonder what React Native does about building a screen from elements, maybe they already have half of what they need to take that step with React. There seems to be about 500 JavaScript frameworks, so it&amp;rsquo;s entirely possible someone has already done what I&amp;rsquo;m thinking about with out any of React&amp;rsquo;s unarguable success.&lt;/p&gt;
&lt;p&gt;The caveat on these thoughts is the same as always, I&amp;rsquo;m at the very start of my journey, and often the reasons for things are revealed as I go!&lt;/p&gt;
&lt;p&gt;Edit: About five hours after writing the post above, I watched the video below. Turn out this disfigured HTML-eese was a &lt;em&gt;selling&lt;/em&gt; point of React at the time. And it seems like my idea of just having a better language to manipulate the DOM might have been tried and abandoned. 🤦‍♀️&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/Wm_xI7KntDs?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>De-structuring objects in JS</title><link>https://blog.iankulin.com/de-structuring-objects-in-js/</link><pubDate>Fri, 20 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/de-structuring-objects-in-js/</guid><description>&lt;p&gt;I&amp;rsquo;ve worked through my first React tutorial app, and obviously that&amp;rsquo;s a lot - I&amp;rsquo;m struct by how messy mixing HTML, JS and React is.&lt;/p&gt;
&lt;p&gt;One language feature that&amp;rsquo;s being used quite a bit, and that is apparently a JS ability I&amp;rsquo;d never seen is &amp;lsquo;destructuring&amp;rsquo; object properties. It&amp;rsquo;s very cool and obviously useful. It&amp;rsquo;s a way of extracting just the properties you need from an object and then using them without accesing them via the object. An example will make it clearer.&lt;/p&gt;
&lt;p&gt;Imagine we&amp;rsquo;ve got a couple of food objects, and we want to test them and provide some dietary advice.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const hotDog = {name: &amp;#39;hot dog&amp;#39;, calories: 290, colors: [&amp;#39;red&amp;#39;, &amp;#39;wheat&amp;#39;]};
const lettuce = {name: &amp;#39;lettuce&amp;#39;, calories: 0, colors: [&amp;#39;green&amp;#39;]};

function eatRecomendation(food) {
 if (food.calories &amp;gt; 200) {
 console.log(`Eat ${food.name} in moderation`);
 } else {
 console.log(`Eat ${food.name} as often as you please`); 
 }
}

eatRecomendation(lettuce);
// Eat lettuce as often as you please
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The parameter in eatRecomendation() could be destructured like so:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function eatRecomendation( {name, calories} ) {
 if (calories &amp;gt; 200) {
 console.log(`Eat ${name} in moderation`);
 } else {
 console.log(`Eat ${name} as often as you please`); 
 }
}

eatRecomendation(lettuce);
// Eat lettuce as often as you please
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s still called with the food object, but the destructuring creates just the properties we need as local variables. If you don&amp;rsquo;t love doing this as the object is passed in - I don&amp;rsquo;t because it arguably makes it harder to see from the signature what should be passed to this function - you can keep the object parameter and destructure it inside the function body:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function eatRecomendation(food) {
 const {name, calories} = food;
 if (calories &amp;gt; 200) {
 console.log(`Eat ${name} in moderation`);
 } else {
 console.log(`Eat ${name} as often as you please`); 
 }
}

evenBetterRecomendation(lettuce);
// Eat lettuce as often as you please
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Digital Color Meter</title><link>https://blog.iankulin.com/digital-color-meter/</link><pubDate>Wed, 18 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/digital-color-meter/</guid><description>&lt;p&gt;For the Calculator project, I needed to know the exact RGB values for the colours on the iOS calculator buttons so I could reproduce them. Assuming a tool for reading colours from the screen exisited, I googled it, and was surprised to find this exact tool is already installed by default on MacOS.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s called Digital Color Meter and just shows the RGB values for anything on the screen under the cursor.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-08-at-2.26.12-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In order to copy the values, hit &lt;code&gt;Command|L&lt;/code&gt; to freeze the current colour, then copy them from the Colour menu. Also in that menu you can choose to have the values shown as hex.&lt;/p&gt;
&lt;h4 id="cursor-in-screenshots"&gt;Cursor in screenshots&lt;/h4&gt;
&lt;p&gt;While I&amp;rsquo;m doing tips and tricks, to have the cursor showing in a Mac screenshot (which I needed for the image above), do &lt;code&gt;shift|command|5&lt;/code&gt; (which is the time delay whole screenshot) then turn it on in the option bar that appears before starting the timer.&lt;/p&gt;</description></item><item><title>Calculator</title><link>https://blog.iankulin.com/calculator-2/</link><pubDate>Mon, 16 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/calculator-2/</guid><description>&lt;p&gt;I&amp;rsquo;ve been doing a bit of driving during the holidays, which means a lot of podcast listening. An episode of &lt;a href="https://topenddevs.com/podcasts/javascript-jabber/episodes/splatty-doo-and-other-javascript-features-you-should-avoid-jsj-543"&gt;JavaScript Jabber about JS features you should never use&lt;/a&gt; sparked my interest in &lt;code&gt;[eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval)&lt;/code&gt;. &lt;code&gt;eval()&lt;/code&gt; takes whatever you pass it in a string and executes it in the JS engine. This is a crazy concept if you&amp;rsquo;ve come from complied languages, and has obvious security implications. As with dynamic typing, I&amp;rsquo;m trying to force myself out of my comfort zone to embrace JS&amp;rsquo;s unique talents so I was keen to try &lt;code&gt;eval()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-08-at-7.51.10-am.png" alt=""&gt;&lt;/a&gt;
&lt;em&gt;lol - challenged accepted.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My first idea for using &lt;code&gt;eval()&lt;/code&gt;was to write a calculator. Pressing the buttons would make build a string, this could just be passed off to &lt;code&gt;eval()&lt;/code&gt; and the return value displayed. It&amp;rsquo;s such an obvious idea I&amp;rsquo;m sure I&amp;rsquo;m not the first to have it.&lt;/p&gt;
&lt;p&gt;To ensure I&amp;rsquo;m growing my CSS skills, I also decided to steal the design of the iPhone calculator. That&amp;rsquo;s the first one below. The second is my current web app version.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_3911.png" width="577" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_02a92dbcfb55-1.jpeg" width="577" alt=""&gt;
&lt;p&gt;Since the calculator display is used for two asynchronous purposes - showing the calculation string as it&amp;rsquo;s being built, and showing a calculation result when we press equals, I&amp;rsquo;ve kept a state variable &lt;code&gt;inputState&lt;/code&gt; which is true when we&amp;rsquo;re building the string, and false when we&amp;rsquo;re displaying a result. &lt;code&gt;btnAddClick()&lt;/code&gt; is attached to all the buttons used to build the string - &lt;code&gt;0123456789()-+/*&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let inputText = &amp;#39;&amp;#39;;
let inputState = true;

function btnAddClick(event) {
 if (!inputState) {
 inputState = true;
 inputText = &amp;#34;&amp;#34;;
 }
 inputText = inputText + event.target.innerHTML;
 txtOutput.innerHTML = inputText;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The backspace key just slices off the last character in the input string.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function btnBackspaceClick() {
 if (inputState &amp;amp;&amp;amp; inputText.length &amp;gt; 0) {
 inputText = inputText.slice(0, -1);
 txtOutput.innerHTML = inputText;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Clear just empties the string and updates the display, then equals calls the dreaded &lt;code&gt;eval()&lt;/code&gt; and shows the output. To make it a bit fancy, I show the input for the calculation just above the result.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function btnEqualsClick() {
 inputState = false;
 let output = eval(inputText);
 txtPrevious.innerHTML = inputText+&amp;#34;=&amp;#34;;
 txtOutput.innerHTML = output;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s pretty much the entire code. Of course it doesn&amp;rsquo;t quite work like a conventional calculator, but I also didn&amp;rsquo;t have to learn anything about &lt;a href="https://en.wikipedia.org/wiki/Calculator_input_methods"&gt;Reverse Polish Notation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The big challenge - and you can see from the screenshots above, still ongoing - is the getting the CSS to work in a way that it looks correct on different devices. My iPhone is an SE, and I had it looking good on that, then sent it to a friend with a newer iPhone and the URL area would not hide. I&amp;rsquo;ll keep working at it, it has forced me to get a better understanding of grid.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m loving a browser developer tools to help with this. Both browsers have a &amp;ldquo;responsive mode&amp;rdquo; that allows you to resize the view to simulate phone like sizes without fiddling with your browser size all the time, and to still be able to see the tools. Dock your tools to the side, and look for the little phone/tablet button to get into responsive mode.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-08-at-8.31.49-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;One other thing I learned is that in Safari on iOS double clicking on a web page zooms it in a little. That&amp;rsquo;s a great feature I guess, but a pain if you just want to enter a number like 99 on a web calculator. The solution turned out to be setting the CSS property &lt;code&gt;[touch-action](https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action)&lt;/code&gt; on the buttons to &lt;code&gt;manipulation&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/Calc"&gt;source&lt;/a&gt; or &lt;a href="https://iankulin.github.io/calc/"&gt;try out the current version&lt;/a&gt;&lt;/p&gt;</description></item><item><title>CWD - 185 - Problem solving</title><link>https://blog.iankulin.com/cwd-185-problem-solving/</link><pubDate>Sat, 14 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/cwd-185-problem-solving/</guid><description>&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;Question 1: Clean the room function: given an input of [1,2,4,591,392,391,2,5,10,2,1,1,1,20,20], 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;make a function that organizes these into individual array that is ordered. For example 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;answer(ArrayFromAbove) should return: [[1,1,1,1],[2,2,2], 4,5,10,[20,20], 391, 392,591]. 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;*/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ctrFunction1&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;inputArray&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// copy the array since we&amp;#39;re mutating it
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;array&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [...&lt;span style="color:#a6e22e"&gt;inputArray&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;array&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;sort&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;number&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;of&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;array&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;number&lt;/span&gt;] &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;undefined&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// this property does not exist, so add it
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;number&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;number&lt;/span&gt;].&lt;span style="color:#a6e22e"&gt;push&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;number&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// object now contains arrays for each number, but the ones with a
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// single element need degloved
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;property&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;property&lt;/span&gt;].&lt;span style="color:#a6e22e"&gt;length&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;property&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;property&lt;/span&gt;][&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// now turn back to array 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Object.&lt;span style="color:#a6e22e"&gt;values&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;numberObject&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;array1&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;591&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;392&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;391&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;2&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;transformedArray1&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ctrFunction1&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;array1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;transformedArray1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// [1, 1, 1, 1], [2, 2, &amp;#39;2&amp;#39;], 4, 5, 10, [20, 20], 391, 392, 591]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="line-10"&gt;Line 10&lt;/h4&gt;
&lt;p&gt;When I&amp;rsquo;m looking at a function, I&amp;rsquo;d prefer not to also have to hold global state in my head - so I&amp;rsquo;m all for functional programming as far as that goes. I&amp;rsquo;m less concerned about side effects, so I wouldn&amp;rsquo;t always bother to copy a parameter like this, but the argument is stronger for an array than an object since in other languages an array might be a value type.&lt;/p&gt;
&lt;p&gt;The copy itself is noteworthy since I&amp;rsquo;m using the cool &lt;code&gt;[...x]&lt;/code&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax"&gt;spread syntax&lt;/a&gt;. This is one of the newish iterator tools which returns any iterate-able data as an array. Since this is right at the top of our function, we&amp;rsquo;re also indicating to the reader we&amp;rsquo;re expecting a one-dimensional array.&lt;/p&gt;
&lt;h4 id="line-11"&gt;Line 11&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;array.sort()&lt;/code&gt; works as expected, and in place - so our new array is mutated - hence the copying earlier. It can optionally be passed in an arrow function to determine the sort test, but if omitted assumes ascending according to the &amp;lt; and &amp;gt; rules. The format of this function is a bit different that in Swift. For the standard sort it is &lt;code&gt;array.sort((a, b) =&amp;gt; (a - b))&lt;/code&gt; whereas in Swift it would have been &lt;code&gt;( a &amp;gt; b )&lt;/code&gt;. This is because in JS the function result is compared against zero to see if the positions are swapped. This seems odd, but I&amp;rsquo;m sure there&amp;rsquo;s a reason.&lt;/p&gt;
&lt;h4 id="lines-13-20"&gt;Lines 13-20&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;for (x of y)&lt;/code&gt; syntax is a neat iterator loop for when you need the item (but not index) of a collection. I slightly regret using &lt;code&gt;number&lt;/code&gt; for the array element - since this is JS it could be number, string or anything.&lt;/p&gt;
&lt;p&gt;We check the new object we created to see if it has a property with the name of the current element. For example if this element of our array is 254, we check to see if the object has a property of that name - eg &lt;code&gt;numberObject.254&lt;/code&gt;. That&amp;rsquo;s the square brackets on the object. It&amp;rsquo;s a neat bit of meta that would be challenging in other languages.&lt;/p&gt;
&lt;p&gt;If there&amp;rsquo;s no property with that name we add it as an empty array. The value from the array is appended to this array in the object. So if we had an array of &lt;code&gt;[2, 2, 3, 4]&lt;/code&gt; we&amp;rsquo;d end up with an object.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;2 - [2, 2]
3 - [3]
4 - [4]
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="lines-22-28"&gt;Lines 22-28&lt;/h4&gt;
&lt;p&gt;Where there&amp;rsquo;s more than one of the same value (as in 2 above) we want an array, but if there&amp;rsquo;s only a single value, we want the raw value. So the next segment of code is to work through each property of the object and change any single values to just their values instead of a single element array. This is a great example of something that&amp;rsquo;s neat and clear in JS but would not be possible in a strictly typed language.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;for x **in** y&lt;/code&gt; this time to inspect each property of an object (rather than elements in an array).&lt;/p&gt;
&lt;h4 id="line-31"&gt;Line 31&lt;/h4&gt;
&lt;p&gt;values() is just a standard method that returns an array version of an object.&lt;/p&gt;</description></item><item><title>Types of Concern</title><link>https://blog.iankulin.com/types-of-concern/</link><pubDate>Fri, 13 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/types-of-concern/</guid><description>&lt;p&gt;I am still struggling with the dynamic typing of JS. I guess the benefit of such an approach is needing less characters - which makes sense in a scripting language like bash, but in real programming it opens us up to a whole class of avoidable errors.&lt;/p&gt;
&lt;p&gt;To program defensively in JS would mean loading the start of function with a series of type checks. I don&amp;rsquo;t see much of that in other people&amp;rsquo;s code, so I assume we just, don&amp;rsquo;t?&lt;/p&gt;
&lt;p&gt;Obviously TypeScript squarely addresses this concern, so that&amp;rsquo;s probably my future, but I was intrigued to hear Kyle Simpson (the &lt;a href="https://github.com/getify/You-Dont-Know-JS"&gt;You Don&amp;rsquo;t Know JS&lt;/a&gt; guy) talking on &lt;a href="https://www.stitcher.com/show/javascript-jabber/episode/jsj-438-you-dont-know-js-yet-with-kyle-simpson-special-announcement-at-the-end-73874583"&gt;JavaScript Jabber&lt;/a&gt; about how since JavaScript was designed from the ground up as dynamically typed that it&amp;rsquo;s better to embrace and understand this that to overlay it with a type system. That was a challenging thought I didn&amp;rsquo;t want to have, but the man knows his JavaScript so I&amp;rsquo;ll ruminate on it.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/O9F4K804XC8?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Lost in Translation</title><link>https://blog.iankulin.com/lost-in-translation/</link><pubDate>Wed, 11 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/lost-in-translation/</guid><description>&lt;p&gt;We&amp;rsquo;re in a pretty good place now (compared to a few years ago) in terms of being able to rely on JavaScript behaving the same on different platforms. There&amp;rsquo;s still some differences (mostly in when things are implemented) but overall, not to bad once you decide to no longer support Internet Explorer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/JavaScript"&gt;In times past, it was a lot more painful&lt;/a&gt;. A few of approaches to deal with this arose. One is to let a library, such as &lt;a href="https://jquery.com/"&gt;jQuery&lt;/a&gt; or a &lt;a href="https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills"&gt;polyfill&lt;/a&gt; deal with it, and the other is use a translation utility such as Babel to down convert (transpile) your modern JavaScript to something that will run in more browsers.&lt;/p&gt;
&lt;p&gt;Babel can be run from the command line, and therefore integrated into a toolchain, but if you want to have a play with it, they have an interactive version on their web site. Here&amp;rsquo;s a couple of examples of how arrow functions and backtick templates get converted to run on IE 6.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-02-at-3.09.56-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Functions in JavaScript</title><link>https://blog.iankulin.com/functions-in-javascript/</link><pubDate>Mon, 09 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/functions-in-javascript/</guid><description>&lt;p&gt;As with other languages, functions are a little lumps of code with their own scope. They can optionally take some arguments, and optionally return a value.&lt;/p&gt;
&lt;p&gt;In JavaScript they often have names, can be passed around as types and have a condensed form suitable for functional programming.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function addNums(a, b) {
 return a+b;
}

console.log(addNums(3,4)) // 7
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="scope"&gt;Scope&lt;/h4&gt;
&lt;p&gt;Arguments are passed in by value so they have local scope only in the function body.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function someFunction(firstNumber, secondNumber) {
 firstNumber = 7;
 return firstNumber+secondNumber;
}

let c = 5;
someFunction(c, 10);
console.log(c);
// 5
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Of course this won&amp;rsquo;t prevent the contents of reference types from being mutated:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function someFunction(numberArray) {
 numberArray[0] = 7;
}

let c = [5];
someFunction(c);
console.log(c);
// [7]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Functions can access values from the the scope they are called in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const a = &amp;#34;hello&amp;#34;;

function printA() {
 console.log(a);
}

printA();
// hello
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This always seems like a bad code smell to me. The tiny bit of extra work to pass it in is worth it. It makes for more readable and testable code. In a pinch, I might use capitalised const (reminiscent of &lt;code&gt;#define PI 3.147&lt;/code&gt;) but even they can usually be passed in. There&amp;rsquo;s an argument about performance, but not optimising for performance till you&amp;rsquo;ve hit a problem is a good rule.&lt;/p&gt;
&lt;p&gt;A variable declared inside a function should not exist outside of the function:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function someStuff() {
 const a = 5;
 let b = 2;
 return a+b;
}

console.log(a);
// Uncaught ReferenceError ReferenceError: a is not defined
console.log(b);
// Uncaught ReferenceError ReferenceError: b is not defined
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Don&amp;rsquo;t use &lt;code&gt;var&lt;/code&gt;, but I had the same result for it in Chrome/Firefox and node.js&lt;/p&gt;
&lt;p&gt;Functions can be nested inside other functions and that&amp;rsquo;s a good idea to avoid polluting the namespace if it meets your needs:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function addTwo(someNumber) {
 
 let a = addOne(someNumber);
 a = addOne(a);
 return a;

 function addOne(number) {
 return number+1;
 }
}

console.log(addTwo(7));
// 9

console.log(addOne(1))
// Uncaught Reference Error
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="first-class-citizens-and-arrows"&gt;First Class Citizens and arrows&lt;/h4&gt;
&lt;p&gt;We can assign a function to a variable, then use that to invoke it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function addNums(a, b) {
 return a+b;
}

const someMaths = addNums;

console.log(someMaths(3, 4));
// 7
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can also do that directly:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const someMaths = function addNums(a, b) { return a+b; };
console.log(someMaths(3, 4));
// 7
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we&amp;rsquo;re doing that, we don&amp;rsquo;t really need the old function name any more since we&amp;rsquo;re not using it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const someMaths = function (a, b) { return a+b; };
console.log(someMaths(3, 4));
// 7
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s also possible to make these even more compact in modern browsers by using &amp;ldquo;arrow functions&amp;rdquo;. Just use a fat arrow between the parameters and the function body:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const addTwoNums = (a, b) =&amp;gt; { return a+b; };
console.log(addTwoNums(3, 4));
// 7
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In a one-liner we can eliminate the curly braces and return since it&amp;rsquo;s implied:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const addTwoNums = (a, b) =&amp;gt; a+b;
console.log(addTwoNums(3, 4));
// 7
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If there&amp;rsquo;s only one argument, we could eliminate the parentheses as well:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const addFive = a =&amp;gt; a+5;
console.log(addFive(3));
// 8
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;What&amp;rsquo;s the point of being able to treat functions as variables? Mainly so we can pass them into other functions, or return them from functions. Here&amp;rsquo;s passing them in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const addTwoNums = (a, b) =&amp;gt; a+b;
const multiplyTwoNums = (a, b) =&amp;gt; a*b;

function doSomeMaths(c, d, process) {
 return process(c, d)
}

const e = doSomeMaths( 4, 2, addTwoNums);
console.log(e);
// 6

const f = doSomeMaths( 4, 2, multiplyTwoNums);
console.log(f);
// 8
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here&amp;rsquo;s returning a function.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function randomMathsFunc() {
 if (Math.random() &amp;gt; 0.5) {
 return (a, b) =&amp;gt; a+b;
 } else {
 return (a, b) =&amp;gt; a*b; 
 }
}

const someFunc = randomMathsFunc();
console.log(someFunc(3, 4));
// sometimes 7, sometimes 12
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Returning functions is not super common, but passing a function around happens all the time. We frequently want to pass functions as error handlers, or to respond to events that happen later.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also great for some functional flavour programming. For instance a common pattern is to iterate over an array to do something to the values and create a new array. It would be great if we could encapsulate that into a method on the array - it would avoid some horrible subscript off by one crashes for a start - but the operation we want to do on the array elements is going to vary from situation to situation. To address that, we could just pass in a function that had the code for the operation we wanted.&lt;/p&gt;
&lt;p&gt;In fact, there is an array method like this called &lt;code&gt;map&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const firstArray = [2, 3, 4, 5];
const plusTwo = a =&amp;gt; a+2;
const secondArray = firstArray.map(plusTwo);
console.log(secondArray);
//[4, 5, 6, 7]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you&amp;rsquo;ve got it clear in your head how the arrow functions work, it&amp;rsquo;s actually clearer to eliminate the superfluous variable and pass them directly:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const firstArray = [2, 3, 4, 5];
const secondArray = firstArray.map(a =&amp;gt; a+2);
console.log(secondArray);
//[4, 5, 6, 7]
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Classes in JavaScript</title><link>https://blog.iankulin.com/classes-in-javascript/</link><pubDate>Sat, 07 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/classes-in-javascript/</guid><description>&lt;p&gt;First lesson with classes today. First of all I was pleased to see they exists since we&amp;rsquo;ve just been plucking objects out of thing air like:&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const userIan = {name: &amp;#39;Ian&amp;#39;, language: &amp;#39;Indonesian&amp;#39;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;but with classes we can declare a class and instantiate an object of it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class User {
 constructor(name, language) {
 this.name = name;
 this.language = language;
 }
}

const ian = new User(&amp;#39;Ian&amp;#39;, &amp;#39;Indonesian&amp;#39;);
console.log(ian.name);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s (at least) single inheritance:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class User {
 constructor(name, language) {
 this.name = name;
 this.language = language;
 }
}

class Administrator extends User {
 constructor(name, language, permissions) {
 super(name, language)
 this.permissions = permissions
 }
}

const ian = new Administrator(&amp;#39;Ian&amp;#39;, &amp;#39;Indonesian&amp;#39;, &amp;#39;-rw-r--r--@&amp;#39;);
console.log(ian.name);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Of course it&amp;rsquo;s JS, so there&amp;rsquo;s no named arguments, and if you miss one off in a call to the constructor there&amp;rsquo;s no issue until you try to use it.&lt;/p&gt;
&lt;p&gt;Methods are declared without a keyword:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class User {
 constructor(name, language) {
 this.name = name;
 this.language = language;
 }

 asString() {
 return `name: ${this.name} language: ${this.language}`;
 }
}

const ian = new User(&amp;#39;Ian&amp;#39;, &amp;#39;Indonesian&amp;#39;);
console.log(ian.asString());
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or we could do that as. computed property using &amp;lsquo;get&amp;rsquo;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class User {
 constructor(name, language) {
 this.name = name;
 this.language = language;
 }

 get asString() {
 return `name: ${this.name} language: ${this.language}`;
 }
}

const ian = new User(&amp;#39;Ian&amp;#39;, &amp;#39;Indonesian&amp;#39;);
console.log(ian.asString);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You don&amp;rsquo;t have to have a constructor, just declare the fields:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class User {
 name;
 language;

 get asString() {
 return `name: ${this.name} language: ${this.language}`;
 }
}

const ian = new User;
ian.name = &amp;#34;Ian&amp;#34;
ian.language = &amp;#34;Indonesian&amp;#34;
console.log(ian.asString);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Probably a good idea to have default values in that case though:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class User {
 name = &amp;#39;&amp;#39;;
 language = &amp;#39;English&amp;#39;;

 get asString() {
 return `name: ${this.name} language: ${this.language}`;
 }
}

const ian = new User;
ian.name = &amp;#34;Ian&amp;#34;
console.log(ian.asString);
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Things I love about Swift after a week of JavaScript</title><link>https://blog.iankulin.com/things-i-love-about-swift-after-a-week-of-javascript/</link><pubDate>Fri, 06 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/things-i-love-about-swift-after-a-week-of-javascript/</guid><description>&lt;p&gt;So, a week into JavaScript, what am I missing? The techie in me wants to say things like Automatic Reference Counting, but actually, at my junior level, I don&amp;rsquo;t run into memory management issues on the day-to-day, so what really do I miss?&lt;/p&gt;
&lt;h4 id="determinism"&gt;Determinism&lt;/h4&gt;
&lt;p&gt;When I build an iOS app, it&amp;rsquo;s frozen in time. The functions inside are always going to stay the same. There might be a future version of iOS that won&amp;rsquo;t run it, but as long as it runs, any pure functions inside it will return the same value. The process of compiling it locks that in. Likewise, any libraries that are complied with it.&lt;/p&gt;
&lt;p&gt;As an interpreted language, that is not true for JavaScript. It can be interpreted differently. It might run differently on different versions of a browser, or browsers from different developers. For example, it would be possible to write a function that adds 1+1 that has a different result on the current version of the Opera Mini browser to Chrome do to the different way it deals with &lt;code&gt;const&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a fair point to say this also does not affect me on the day-to-day, but it does worry me that something I put out there might fail unpredictably in the future. I guess &lt;a href="https://webassembly.org/"&gt;web assembly&lt;/a&gt; fixes this, but then I could be writing in Swift!&lt;/p&gt;
&lt;h4 id="ide"&gt;IDE&lt;/h4&gt;
&lt;p&gt;I really love &lt;a href="https://code.visualstudio.com/"&gt;VS Code&lt;/a&gt;, it makes me feel differently (in a good way) about Microsoft. It&amp;rsquo;s fun to write code in, and has a amazing extensions that provide all sorts of good experiences, but it&amp;rsquo;s not a complete IDE. Likely there&amp;rsquo;s things I can set up to get closer to debugging in VS Code while running my apps in a browser and I just haven&amp;rsquo;t figured them out yet. When I&amp;rsquo;m just playing with JS using node is a good solution but I haven&amp;rsquo;t arrived at a smooth solution for browser. Currently I&amp;rsquo;m using lots of console.log()s and the developer console in the browsers.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s fair to say Swift developers don&amp;rsquo;t universally love the Xcode experience, and there is definitely room to improve the extension environment, but it is a complete package for developing iOS apps.&lt;/p&gt;
&lt;h4 id="deprecated--deleted"&gt;Deprecated = Deleted&lt;/h4&gt;
&lt;p&gt;When Swift kills off an old language feature in a big number version change, it stays dead. It&amp;rsquo;s not there any more, it haunt&amp;rsquo;s no one. That&amp;rsquo;s not possible with an interpreted language with millions of installed apps. Browsers have to support bad old ideas in JavaScript basically forever. I have to have &lt;a href="https://caniuse.com/"&gt;CanIUse&lt;/a&gt; open in a tab (although I guess there&amp;rsquo;s a VS Code extension for that!).&lt;/p&gt;
&lt;h4 id="in-javascripts-favour"&gt;In JavaScript&amp;rsquo;s favour&lt;/h4&gt;
&lt;p&gt;I love the low barrier to entry to JavaScript. Web development is open to anyone with an internet connection and a device. No MacBook, no Apple Developer subscription, no iPhone needed. Most anyone wanting to try it can just open the console in their browser.&lt;/p&gt;
&lt;p&gt;In theory an interpreted language has the advantage of not needing the build step. When I first encountered Ruby and Python this was part of their charm for me. This does apply to JavaScript - you can just pull up the console and start playing. In the intervening years, compiled languages have made up this space a bit though, so it&amp;rsquo;s not the big selling point it used to be. Swift in Xcode is compiled somehow as you type so you&amp;rsquo;re seeing errors flagged as they occur. Building the little apps I&amp;rsquo;ve written is very quick, and tools like Playgrounds fill part of this need.&lt;/p&gt;</description></item><item><title>CodePen.io</title><link>https://blog.iankulin.com/codepen-io/</link><pubDate>Thu, 05 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/codepen-io/</guid><description>&lt;p&gt;I quite often leave a link to a GitHub repo to share my source in these posts, and on a few recent ones, a link to a live version of a page on my github.io. In a recent installment of &lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;CWD&lt;/a&gt;, Andrei shared some previous students&amp;rsquo; solutions, and some were hosted on CodePen.io which I hadn&amp;rsquo;t seen before.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-28-at-10.20.02-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a cute concept, you can enter HTML, CSS &amp;amp; JS and see a live view of the page below. It looks super extensible - there&amp;rsquo;s mentions of SCSS, Typescript and preprocessors for JS in the settings.&lt;/p&gt;
&lt;p&gt;There are a few quirks - you don&amp;rsquo;t enter all the DOCTYPE/&lt;html&gt;&lt;body&gt; etc in your HTML- presumably that&amp;rsquo;s because they already need that for their page. There are settings for some of these for example if you need to add a clss to the &lt;html&gt;. I couldn&amp;rsquo;t see how to do that for the &lt;body&gt; though so I needed to change the JS for this project a bit.&lt;/p&gt;
&lt;p&gt;It seems pretty great - sort of like Playgrounds for web dev. It&amp;rsquo;s not quite a full debugging IDE - that doesn&amp;rsquo;t seem to exist for web dev. There&amp;rsquo;s no intellisense, but it was flagging problems in my JavaScript, so I&amp;rsquo;ll use it for a few of these tutorial pages and see how it goes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://codepen.io/IanKulin/pen/BaPjmNv"&gt;Here&amp;rsquo;s a link&lt;/a&gt; to the tutorial app above on CodePen.&lt;/p&gt;</description></item><item><title>Second Guessing</title><link>https://blog.iankulin.com/second-guessing/</link><pubDate>Thu, 05 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/second-guessing/</guid><description>&lt;p&gt;In the last post, I was pleased with myself for accidentally anticipating an improvement to a tutorial project which turned out to be the next task, today I&amp;rsquo;m pleased with myself for discussing the pros and cons of &lt;code&gt;onclick=&lt;/code&gt; vs &lt;code&gt;addEventListener()&lt;/code&gt; then having that same discussion turn up in the next tutorial. I take it as an indication that I am correctly immersing myself in the subject.&lt;/p&gt;
&lt;p&gt;My approach to this learning is to watch all the tutorials, and any that involve code, type along with them. Usually they are followed with a task to extend them so I do those or invent my own. I&amp;rsquo;ve also started trying to reproduce versions of other websites I see that I&amp;rsquo;m interested in.&lt;/p&gt;
&lt;p&gt;I also listen to a few podcasts about the subject. Often good podcasts seem to get more detailed and technical over time, so I search for any that do occasional episodes on beginner topics. Even if a lot of it is over my head, you do start to pick up on the industry buzz about things, and learn some of the names that can be helpful for searches later. I&amp;rsquo;m currently cherry picking old episodes of &lt;a href="https://topenddevs.com/podcasts/javascript-jabber"&gt;JavaScript Jabber&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The last thing is these posts. I&amp;rsquo;m fully aware that most likely they&amp;rsquo;ll never be read by anyone but me, but the process of writing them does help clarify my thoughts, and the knowledge of a potential audience gives me a sense of accountability I wouldn&amp;rsquo;t have if I was just saving them to a directory. They do even occasionally help me - I&amp;rsquo;ve been back to some of the git ones when I&amp;rsquo;ve forgotten things!&lt;/p&gt;</description></item><item><title>Step Ahead</title><link>https://blog.iankulin.com/one-step/</link><pubDate>Wed, 04 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/one-step/</guid><description>&lt;p&gt;I was a bit pleased with myself when I started the next content element in the Complete Web Developer course to find that one and a half of the extensions I&amp;rsquo;d made to the tutorial app for my own fun were specified as the next task.&lt;/p&gt;
&lt;p&gt;In my previous post, I&amp;rsquo;d talked about using a class to denote if an item was completed, and using a style to indicate this by crossing it out. What I haven&amp;rsquo;t discussed was that I&amp;rsquo;d captured right click events on the list items to make this delete them. I wasn&amp;rsquo;t entirely happy with that for a couple of reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It wasn&amp;rsquo;t obvious to the user how to delete if they wanted to, and perhaps worse, it might accidentally be invoked - never a good idea for a destructive action with no undo.&lt;/li&gt;
&lt;li&gt;It was accessibility un-friendly There was no way to tab through the available actions or to have them read out. This was also the case for crossing items off.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The new task was to add a delete button for each item, which is a much better idea. I decided to do both a &amp;ldquo;check off&amp;rdquo; and &amp;ldquo;delete&amp;rdquo; button to address the accessibility point above.&lt;/p&gt;
&lt;p&gt;The HTML for them looks like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;ul id=&amp;#34;ulItems&amp;#34;&amp;gt;
 &amp;lt;li&amp;gt;
 Sample Item
 &amp;lt;button type=&amp;#34;button&amp;#34; class=&amp;#34;btnCheck&amp;#34;&amp;gt;✔️&amp;lt;/button&amp;gt;
 &amp;lt;button type=&amp;#34;button&amp;#34; class=&amp;#34;btnDelete&amp;#34;&amp;gt;❌&amp;lt;/button&amp;gt;
 &amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I might have been a bit too clever using emoji. I assume they are well supported but not really sure. I also could not change the check mark to green which I would have liked to. I wasn&amp;rsquo;t sure how important the type=&amp;ldquo;button&amp;rdquo; was, but &lt;a href="https://www.w3schools.com/TAGs/att_button_type.asp"&gt;w3schools say&lt;/a&gt; to &amp;ldquo;Always&amp;rdquo; use it, so that&amp;rsquo;s good enough for me.&lt;/p&gt;
&lt;p&gt;Having two buttons complicated some of the handling code a bit. In the section where I attach the listeners to the buttons for any items that have been specified in the HTML (which could probably just be removed) it looks a bit hacky:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var links = document.getElementsByTagName(&amp;#39;li&amp;#39;);
for (var i = 0; i &amp;lt; links.length; i++) {
 var link = links[i];
 link.onclick = onListItemClick;
 console.assert(link.childNodes.length === 3)
 link.childNodes.item(1).onclick = onBtnCheck
 link.childNodes.item(2).onclick = onBtnDelete
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Using the indexes into the childNodes like this is quite fragile. For example, the indexes change if the buttons in the HTML are separated by a new line character, so something as simple as a linter in the build chain could break it. There&amp;rsquo;s probably a way to get child nodes by tagname (&lt;em&gt;edit: there is - the well named getElementByTagName()&lt;/em&gt;) but concerns about performance, along with laziness associated by knowing I&amp;rsquo;ll probably remove this whole code block prevented me from using it.&lt;/p&gt;
&lt;p&gt;I thought I&amp;rsquo;d reuse the function for clicking on an item for clicking on the check button, but of course the event contains a different caller. So they ended up like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function onListItemClick(event) {
 if (event.target.tagName === &amp;#34;LI&amp;#34;) {
 event.target.classList.toggle(&amp;#34;completed&amp;#34;) 
 }
}

function onBtnCheck(event) {
 event.target.parentNode.classList.toggle(&amp;#34;completed&amp;#34;) 
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The test in onListItemClick() for the tagname is to stop this being triggered as a side effect of clicking with either of the buttons. Worth noting is that the tagname seems always to be in capitals even if it&amp;rsquo;s lowercase in the HTML. Lowercase for tags seems to be the convention so that was surprising to me.&lt;/p&gt;
&lt;p&gt;The code for adding the buttons for new items is pretty straightforward:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function addNewItem() {
 if (txtItem.value.length &amp;gt; 0) {
 
 var btnCheck = document.createElement(&amp;#34;button&amp;#34;)
 btnCheck.innerText = &amp;#34;✔️&amp;#34;
 btnCheck.type=&amp;#34;button&amp;#34;
 btnCheck.classList.add(&amp;#34;btnCheck&amp;#34;)
 btnCheck.onclick = onBtnCheck

 var btnDelete = document.createElement(&amp;#34;button&amp;#34;)
 btnDelete.innerText = &amp;#34;❌&amp;#34;
 btnDelete.type=&amp;#34;button&amp;#34;
 btnDelete.classList.add(&amp;#34;btnDelete&amp;#34;)
 btnDelete.onclick = onBtnDelete

 var li = document.createElement(&amp;#34;li&amp;#34;);
 li.appendChild(document.createTextNode(txtItem.value));
 li.onclick = onListItemClick
 li.appendChild(btnCheck)
 li.appendChild(btnDelete)
 ulItems.appendChild(li);

 txtItem.value = &amp;#34;&amp;#34;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the code above I&amp;rsquo;ve used two different methods of inserting the text into an element. One by using the innerText property, and one by creating a TextNode and inserting it with the appendChild() method. In the CWD course, Andrei had commented &amp;ldquo;//Dangerous&amp;rdquo; next to innerText, but hasn&amp;rsquo;t discussed it yet. There&amp;rsquo;s a good discussion &lt;a href="https://marian-caikovski.medium.com/innerhtml-vs-appendchild-e74c763846df"&gt;here from Marian Čaikovski&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Another thing I&amp;rsquo;ve got two different versions of in this codebase is adding the event handlers for when things are clicked on. In some places I&amp;rsquo;ve got the succinct, clear onClick =, in others, addEventListener().&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;txtItem.addEventListener(&amp;#34;keydown&amp;#34;, onKeyPress)
btnItem.addEventListener(&amp;#34;click&amp;#34;, addNewItem)

var links = document.getElementsByTagName(&amp;#39;li&amp;#39;);
for (var i = 0; i &amp;lt; links.length; i++) {
 var link = links[i];
 link.onclick = onListItemClick;
 console.assert(link.childNodes.length === 3)
 link.childNodes.item(1).onclick = onBtnCheck
 link.childNodes.item(2).onclick = onBtnDelete
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It &lt;a href="https://www.geeksforgeeks.org/difference-between-addeventlistener-and-onclick-in-javascript/"&gt;sounds like&lt;/a&gt; onClick is better supported, but really only a marginal difference. addEventListener can support more than one handler for any particular event, and covers more events.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/iankulin.github.io/tree/main/todo001"&gt;source&lt;/a&gt; for this project, or &lt;a href="https://iankulin.github.io/todo001/"&gt;try it out here&lt;/a&gt;. I should also note that since I&amp;rsquo;m still working on this those might not exactly match the code above.&lt;/p&gt;</description></item><item><title>Web Reference</title><link>https://blog.iankulin.com/web-reference/</link><pubDate>Tue, 03 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/web-reference/</guid><description>&lt;p&gt;There is no shortage of places to reference material to help when developing web apps, including a gazillion tutorials and blog posts (like mine) of various quality, and more importantly - based on the state of play at the time they were written, which could be any time in the last twenty years. I keep bumping up against this - great, clear explanations addressing whatever I was googling, but which turn out to use out of date bits.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m loving a couple of good sources:&lt;/p&gt;
&lt;h4 id="w3schoolscom"&gt;&lt;a href="https://www.w3schools.com/"&gt;w3schools.com&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The search is good, but the navigation tools are excellent. There are sections for HTML, CSS, JavaScript and so on, but I usually end up in the &lt;a href="https://www.w3schools.com/howto/default.asp"&gt;How To&lt;/a&gt; section where there are really clear, minimal examples of how to accomplish some common task with the HTML, CSS &amp;amp; JavaScript for that task.&lt;/p&gt;
&lt;h4 id="mdm-web-docs"&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript"&gt;MDM Web Docs&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;m not clear on what the relationship between MDM and Mozilla is, but this site is at least hosted by them. This is what I&amp;rsquo;ve been using as a reference for specific things - for example if I want to know the exact methods of an Event, this is the place to go to get the full list. As a bonus, they are a bit opinionated. For example, the page for &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent"&gt;KeyboardEvent&lt;/a&gt; has a handy warning about the edge case of psychopaths using non-standard keyboard layouts and therefore making Keyboard.code complicated.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://css-tricks.com/almanac/"&gt;CSS Tricks&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Good place for details, especially when you know the property you&amp;rsquo;re interested in and want to see the values and any associated properties.&lt;/p&gt;
&lt;h4 id="can-i-use"&gt;&lt;a href="https://caniuse.com/?search=event.code"&gt;Can I Use&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Once you&amp;rsquo;ve found the language feature, tag or property you want to use, you need to know if it will work. This is where to find out.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-26-at-5.29.40-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Document Object Model - ToDo</title><link>https://blog.iankulin.com/document-object-model-todo/</link><pubDate>Mon, 02 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/document-object-model-todo/</guid><description>&lt;p&gt;I&amp;rsquo;m up to Section 12 of the Complete Web Developer course &amp;ldquo;DOM Manipulation&amp;rdquo; and it feels like we&amp;rsquo;re finally at the stage of pulling everything (HTML, CSS &amp;amp; JavaScript) together to make minimal web apps. Since the course is light on building challenges, I&amp;rsquo;ve set myself one - to make a simple todo list (the classic step up from &amp;ldquo;hello world&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;The Document Object Model is an entity representing the HTML with attached CSS for a page. The magic is that we can access this in JavaScript, and therefore change it, including hooking into events on it - such as a user pressing a button.&lt;/p&gt;
&lt;p&gt;For our ToDo app, we&amp;rsquo;ll need to allow the user to add an item by typing in some text and pressing a button to add it. There will be a list those items that grows as the user adds to it. As the user completes items, they click on them to signify they have been done - and the items are crossed out.&lt;/p&gt;
&lt;p&gt;So for HTML, we need a text input, a button, and a list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lang&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;charset&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;UTF-8&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;http-equiv&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;X-UA-Compatible&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;IE=edge&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;ToDo&amp;lt;/&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;Todo List&amp;lt;/&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;header&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;text&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;txtItem&amp;#34;&lt;/span&gt;&amp;gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;button&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;btnAddItem&amp;#34;&lt;/span&gt;&amp;gt;Add Item&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;header&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;ul&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;ulItems&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;Sample Item&amp;lt;/&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;script.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m not really seeing any consistent naming conventions for element ids in the code snippets I&amp;rsquo;ve seen around the web. Since everything is hard coded strings this seems like inviting disaster. I&amp;rsquo;ve adopted a VB/Delphi convention of abbreviating the type at the start of the name and camelcasing. So the button to add an item becomes &amp;ldquo;btnAddItem&amp;rdquo;. I wish (and perhaps there is) there was a VS Code extension to make the links between my HTML and JavaScript more obvious to reduce those potential errors.&lt;/p&gt;
&lt;h4 id="adding-an-item"&gt;Adding an item&lt;/h4&gt;
&lt;p&gt;The first problem, of catching the user input (pressing enter in the text, or clicking the button) was mostly solved for me by the CWD tutorial. It involves adding event listeners to both those elements.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var txtItem = document.getElementById(&amp;#34;txtItem&amp;#34;)
var btnItem = document.getElementById(&amp;#34;btnAddItem&amp;#34;)

txtItem.addEventListener(&amp;#34;keydown&amp;#34;, respondToKeyPress)
btnItem.addEventListener(&amp;#34;click&amp;#34;, addNewItem)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;document&lt;/code&gt; in those first two lines is the Document Object Model (DOM) object. It has a method for finding HTML elements by the ids we assigned them in the HTML. These ids should be unique in the file, but the browsers don&amp;rsquo;t enforce this. If you&amp;rsquo;ve used duplicate ids, getElementById() will probably return the first one it found.&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re using hard coded strings, an anticipatable error would be misspelling the id either in the HTML or JavaScript. If that happens, getElementByID will return null. I would have thought we should be testing for this, or at least asserting it, but I don&amp;rsquo;t see either happening in the code I&amp;rsquo;ve seen. There is a console.assert(), but of course it&amp;rsquo;s not removed for production builds by a compiler.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-26-at-8.59.27-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[addEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)&lt;/code&gt; does what it says on the box. You need to tell it which event (another string) and pass it the name of the handler function. It&amp;rsquo;s not specified here, but the handlers can have access to an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Event"&gt;Event&lt;/a&gt; object which turns out to be handy. Here&amp;rsquo;s the two handlers referenced in the code above:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function respondToKeyPress(event) {
 if (event.code === &amp;#34;Enter&amp;#34;) {
 addNewItem() 
 }
}

function addNewItem() {
 if (txtItem.value.length &amp;gt; 0) {
 var li = document.createElement(&amp;#34;li&amp;#34;);
 li.appendChild(document.createTextNode(txtItem.value));
 ulItems.appendChild(li);
 li.onclick = listItemClick
 txtItem.value = &amp;#34;&amp;#34;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first one just uses the Event object to check the user has pressed enter to add an item, and if so, calls the other handler to add the item. If the &amp;ldquo;Add Item&amp;rdquo; button is pressed, only the addNewItem() handler is called.&lt;/p&gt;
&lt;p&gt;After checking that there&amp;rsquo;s actually some text to add, it creates a new &lt;li&gt; item, appends the text to it, then appends the new item to our unordered list. The next line: &lt;code&gt;li.onclick = listItemClick&lt;/code&gt; adds an eventListener for &amp;ldquo;click&amp;rdquo; to this list new item. We&amp;rsquo;ll use this same handler later to detect clicks on any of the todo items in the list. This same event handler was also attached to any preexisting &lt;li&gt; elements at page load with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var links = document.getElementsByTagName(&amp;#39;li&amp;#39;);
for (var i = 0; i &amp;lt; links.length; i++) {
 var link = links[i];
 link.onclick = listItemClick;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The very last thing that addNewItem() does is to clear the text input ready for the next item.&lt;/p&gt;
&lt;h4 id="crossing-things-off-the-list"&gt;Crossing things off the list&lt;/h4&gt;
&lt;p&gt;The most satisfying thing to do with a list is to cross things off it. The UX here will be that the user clicks on an item, and it gets crossed off but stay&amp;rsquo;s visible. We probably need to be able to reverse that process to. If the user accidentally crosses it off, they should be able to clock on it again to make it appear un-crossed.&lt;/p&gt;
&lt;p&gt;First up, we need to capture the click event. We&amp;rsquo;ve already seen how to do that in the code snippets above. When the page is first loaded, a loop adds the listItemClick functionto the .onClick event of every &lt;li&gt; item, and the same function is added to each new &lt;li&gt; item that&amp;rsquo;s created in addNewItem().&lt;/p&gt;
&lt;p&gt;Since this single handler (addNewItem())is handling the clicks for every list item, but we need to cross them off individually, we need some way of accessing the current clicked &lt;li&gt; inside the handler so we can decorate it accordingly.&lt;/p&gt;
&lt;p&gt;We saw above that the handlers can access the Event that is instigating them. Event contains a property &amp;ldquo;target&amp;rdquo; that is the element that triggered the event, so we can just use that. Here&amp;rsquo;s my first attempt at the listItemClick() handler.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function listItemClick(event) {
 if (event.target.style.textDecoration === &amp;#34;line-through&amp;#34;) {
 event.target.style.textDecoration = &amp;#34;&amp;#34; 
 } else {
 event.target.style.textDecoration = &amp;#34;line-through&amp;#34;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Setting &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration"&gt;.textDecoration&lt;/a&gt; to &amp;ldquo;line-through&amp;rdquo; has the same effect as setting it in CSS - a line is drawn through the text &lt;del&gt;like this&lt;/del&gt;. This code works fine - if we click on it, the todo item has the line drawn through it, then if we click it again, the line disappears.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a couple of potential problems though. The first is we are mixing up the behaviour (which is rightly a JavaScript concern) with how things look (which should be a CSS concern). For example, perhaps the UI team want completed items to be faded rather than crossed out.&lt;/p&gt;
&lt;p&gt;The second problem is related to that. We&amp;rsquo;re testing if &lt;code&gt;event.target.style.textDecoration === &amp;quot;line-through&amp;quot;&lt;/code&gt; when really what we are interested in is if this item is completed. There are a number of textDecorations, for example it&amp;rsquo;s possible the textDecoration could be &amp;ldquo;line-through underline red&amp;rdquo; because of a CSS entry.&lt;/p&gt;
&lt;p&gt;The solution to both these issues would be to use a class to indicate the completed/uncompleted status of each list item, and let the UI team set how these should be displayed in the CSS. If we use the class name &lt;code&gt;completed&lt;/code&gt;, we could use some simple CSS like this to make a fat red line:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.completed {
 text-decoration-line: line-through;
 text-decoration-color: red;
 text-decoration-thickness: 3px;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And we could convert out listItemClick code to change the class with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function listItemClick(event) {
 if (event.target.classList.contains(&amp;#34;completed&amp;#34;)) {
 event.target.classList.remove(&amp;#34;completed&amp;#34;) 
 }
 else {
 event.target.classList.add(&amp;#34;completed&amp;#34;) 
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That works nicely. We&amp;rsquo;ve separated our concerns, and .completed items are clearly and satisfyingly crossed out.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-12-26-at-2.30.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-26-at-2.30.59-pm.png" width="543" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In fact, even this can be slightly improved on. There is a .toggle() method for turning a class off and on for an element, so we can eliminate some code by using that. As well as being simpler, we&amp;rsquo;ve removed the possibility of a classname typo the three times we use it. So:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function listItemClick(event) {
 event.target.classList.toggle(&amp;#34;completed&amp;#34;) 
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since our handler is down to a single line of code now, you might consider eliminating it by just passing the contents to the .onclick rather than a call to this function, however we do that in two places (the initial set up and again when each &lt;li&gt; is added) plus it would reduce our readability, so I&amp;rsquo;ll leave it like this.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/iankulin.github.io/tree/main/todo001"&gt;source&lt;/a&gt; or &lt;a href="https://iankulin.github.io/todo001/"&gt;try it out&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Are you okay JavaScript arrays?</title><link>https://blog.iankulin.com/are-you-okay-javascript-arrays/</link><pubDate>Sat, 31 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/are-you-okay-javascript-arrays/</guid><description>&lt;p&gt;As a visitor from sensible type-safe land, this makes me uncomfortable:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-23-at-8.52.06-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;As I keep learning, I&amp;rsquo;m interested to find out if JavaScript objects turn out to just be arrays. To get from here to there, you&amp;rsquo;d just need to be able to use some sort of self[2] notation to access properties from inside the functions.&lt;/p&gt;</description></item><item><title>Curse of Backwards Compatibility</title><link>https://blog.iankulin.com/curse-of-backwards-compatibility/</link><pubDate>Thu, 29 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/curse-of-backwards-compatibility/</guid><description>&lt;p&gt;I was listening to a JavaScript podcast today (&lt;a href="https://www.youtube.com/watch?v=O0fvMJcca3A"&gt;JavaScript Jabber&lt;/a&gt;) and in one of the discussions a point was made about how HTML, CSS and JavaScript have all had to maintain considerable legacy behaviors that compile-able languages do not have to. For instance, when Swift underwent some substantial changes from Swift 2 to Swift 3 - some code broke for developers and needed reworking because things had changed or been removed. Nothing broke for users - they could either still use their previously compiled applications, or they were delivered new ones from the app store.&lt;/p&gt;
&lt;p&gt;In web world - that&amp;rsquo;s not possible.&lt;/p&gt;
&lt;p&gt;Modern browsers need to be able to correctly render HTML from the birth of the web. I have a commercial site I last updated in 1996 that uses tables for some layout - it works fine in a modern browser.&lt;/p&gt;
&lt;p&gt;Having to bring forward all this functionality is great for web users (and people who don&amp;rsquo;t maintain their websites, but it weighs down the languages and makes learning them more difficult. This is related to my dilemma about ignoring block model and flex-box; in a compiled language they could have been deprecated in favour of grids, but in CSS they need to exist forever.&lt;/p&gt;
&lt;p&gt;This same theme was revisited in a &lt;a href="https://topenddevs.com/podcasts/javascript-jabber/episodes/jsj-421-semantic-html-with-bruce-lawson"&gt;later episode of the same podcast&lt;/a&gt;, this time in relation to semantic HTML and it&amp;rsquo;s benefits. The hosts wished that some improvements to web technologies &lt;em&gt;would&lt;/em&gt; break web-sites so people would be forced to update them.&lt;/p&gt;</description></item><item><title>Running Javascript in VS Code</title><link>https://blog.iankulin.com/running-javascript-in-vs-code/</link><pubDate>Tue, 27 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/running-javascript-in-vs-code/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-21-at-11.08.17-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been using the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer"&gt;Live Server&lt;/a&gt; plugin to see HTML &amp;amp; CSS updated as I edit, and that will also be useful when I start using Javascript for web development, but as you can see above, I&amp;rsquo;m not quite up to that. It seemed there should be a way to run JS in VS Code, and it turns out it&amp;rsquo;s easy.&lt;/p&gt;
&lt;p&gt;You just need something installed that can run Javascript. Node.js is the obvious choice, and you&amp;rsquo;re going to need it later in your development journey. Just i&lt;a href="https://nodejs.org/en/download/"&gt;nstall Node.js&lt;/a&gt; then the first time you try to run some JS in VS code, it will ask you what to use, select Node and you&amp;rsquo;re in business.&lt;/p&gt;
&lt;p&gt;I found out about this &lt;a href="https://linuxhint.com/javascript-visual-studio-code/"&gt;from here&lt;/a&gt;. I didn&amp;rsquo;t worry about Code Runner - just using Node.js worked for me without any fiddling beyond installing it (this is on Mac though - your Windows mileage may vary).&lt;/p&gt;
&lt;p&gt;While I&amp;rsquo;m doing handy hints for dev tools, I discovered last night that the baby webserver that Live Server is running, isn&amp;rsquo;t just available on the local machine, it&amp;rsquo;s available to anyone on your network. Instead of using 127.0.0.1:5500, use the IP address of your development machine (but still port 5500 if that&amp;rsquo;s what you&amp;rsquo;re using). It&amp;rsquo;s an excellent way to look at your layout on phones etc, or, I guess, to see what other devs at your company are working on :- )&lt;/p&gt;</description></item><item><title>99 CSS Layout Feedback</title><link>https://blog.iankulin.com/99-css-layout-feedback/</link><pubDate>Sun, 25 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/99-css-layout-feedback/</guid><description>&lt;p&gt;I&amp;rsquo;ve been in the swing with the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100DaysOfSwiftUI&lt;/a&gt; course of having frequent assignments to test my understanding of the course content up to that point, then watching the feedback video and reflecting on it here. So far, in the &lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;Complete Web Developer&lt;/a&gt; I&amp;rsquo;ve only had this single CSS assignment, so I was excited to see how I got on.&lt;/p&gt;
&lt;p&gt;I was a bit chuffed that one of Andrei&amp;rsquo;s first actions was to edit the html to make it more semantic - I&amp;rsquo;d used &lt;header&gt; for the top bit, he used &lt;nav&gt; which is probably better, and then I could have recycled &lt;header&gt; for the cover. Although in general, there was a lot of use of classes, where I had just used selectors carefully. I guess my thinking here was that the html should be free of information about how to display it - and we break that if we say add &lt;code&gt;class=&amp;quot;sticky&amp;quot;&lt;/code&gt; to the nav bar. The argument could be made the other way though - with my system I&amp;rsquo;m building dependency on a particular page structure into the CSS if I use a selector to pick out the last child in a list to apply a style to it.&lt;/p&gt;
&lt;p&gt;Andrei also used an unordered list for his links - I saw this in a CSS video so it might be a common idea. He used a flexbox, and an auto left-margin to move the last link to the right edge of the screen which was neater than the fiddling around I did - but I think that&amp;rsquo;s down to me as I could probably have done the equivalent trick i grid.&lt;/p&gt;
&lt;p&gt;There were a couple of new things introduced in the solution, which felt a bit unfair, although its a valid teaching approach I guess. One was vh units, and the other was using @media queries to change things based on the screen size. It wouldn&amp;rsquo;t have been possible for me to have used either since I hadn&amp;rsquo;t been taught them yet, but both were good in this context.&lt;/p&gt;
&lt;p&gt;Same with the sticky nav bar, I&amp;rsquo;d actually considered this in my design, but hadn&amp;rsquo;t figured out how to do it. It was done with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nav {
 position: fixed;
 top: 0;
 width: 100%;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But when I tried it in my page, I&amp;rsquo;d lost the vertical centering in the div below, so obviously I needed to do something to that to make it start at the bottom of the newly sticky nav bar which had been happening without any intervention before.&lt;/p&gt;</description></item><item><title>99 CSS Layout challenge</title><link>https://blog.iankulin.com/99-css-layout-challenge/</link><pubDate>Fri, 23 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/99-css-layout-challenge/</guid><description>&lt;p&gt;In the &lt;a href="https://zerotomastery.io/"&gt;Zero To Mastery&lt;/a&gt; &lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;Complete Web Developer&lt;/a&gt; course, I&amp;rsquo;m up to the first practical challenge - to use CSS to layout a reasonably standard looking web page using flex-box and grid to make it responsive.&lt;/p&gt;
&lt;p&gt;Frustratingly, both for writing this, and while I was trying to build the page, I&amp;rsquo;m unable to screenshot the example of the page I was supposed to be building, and instead had to keep opening the video and seeking the two second flash of the completed project, and eventually being reduced to photographing my laptop screen like a boomer relative sending me a meme:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_3601.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What you can&amp;rsquo;t see in that image (because it was never shown for this version) is a foooter bar containing a piece of centred text.&lt;/p&gt;
&lt;p&gt;The starting point was some html with a a handful of elements, and some unnecessarily complicated css colours.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-12-20-at-8.06.13-am-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-20-at-8.06.13-am-1.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;title&amp;gt;Layout Master&amp;lt;/title&amp;gt;
 &amp;lt;link rel=&amp;#34;stylesheet&amp;#34; type=&amp;#34;text/css&amp;#34; href=&amp;#34;./style.css&amp;#34;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
 &amp;lt;div class=&amp;#34;zone green&amp;#34;&amp;gt;Header&amp;lt;/div&amp;gt;
 &amp;lt;div class=&amp;#34;zone red&amp;#34;&amp;gt;Cover&amp;lt;/div&amp;gt;
 &amp;lt;div class=&amp;#34;zone blue&amp;#34;&amp;gt;Project Grid&amp;lt;/div&amp;gt;
 &amp;lt;div class=&amp;#34;zone yellow&amp;#34;&amp;gt;Footer&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The hint that was given, was to use a mix of flex-box and grid. Since it was stated in the same breath that everything you can do in flex, you can do in grid, I decided to just do grid. I&amp;rsquo;m not anticipating having to maintain anyone&amp;rsquo;s flex code, so I felt I could reduce the amount I need to learn by eliminating flex-box.&lt;/p&gt;
&lt;p&gt;I changed up the HTML a bit to make it more semantic, but still ended up with a couple of divs for the cover and projects.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lang&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;charset&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;UTF-8&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;http-equiv&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;X-UA-Compatible&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;IE=edge&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;style.css&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;Document&amp;lt;/&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;header&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;a&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;about.html&amp;#34;&lt;/span&gt;&amp;gt;About&amp;lt;/&lt;span style="color:#f92672"&gt;a&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;a&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;products.html&amp;#34;&lt;/span&gt;&amp;gt;Products&amp;lt;/&lt;span style="color:#f92672"&gt;a&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;a&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;ourteam.html&amp;#34;&lt;/span&gt;&amp;gt;Our Team&amp;lt;/&lt;span style="color:#f92672"&gt;a&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;a&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;contact.html&amp;#34;&lt;/span&gt;&amp;gt;Contact&amp;lt;/&lt;span style="color:#f92672"&gt;a&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;li&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;header&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;cover&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/undraw.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;projects&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/monitor_coding_2.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/desktop_analytics_2.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/files_2.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/data_storage_2_2.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/monitor_settings_2.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/server_2_2.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/server_3.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;img/server_safe_2.png&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;footer&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Made by IanKulin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;footer&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ll work through the CSS piece by piece. The general stuff was importing the font, the reset and setting the body defaults.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@import url(&amp;#39;https://fonts.googleapis.com/css?family=Roboto&amp;amp;display=swap&amp;#39;);

* {
 margin: 0;
 padding: 0;
 box-sizing: border-box;
}

body {
 color: white;
 font-family: &amp;#39;Roboto&amp;#39;, sans-serif;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The header/nav bar was actually the toughest job here - it needs four links; three left aligned and one right aligned. I have no doubt that this is not the most elegant solution.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;header {
 background: #33BCFF;
 height: 3rem;
 display: grid;
 align-content: center;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This grid and align was just to get the text vertically centered.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;header ul {
 display: grid;
 grid-template-columns: repeat(3, fit-content(150px)) 1fr;
 grid-gap: 10px;
 justify-items: end;
 padding: 20px;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The links are in an unordered list, so that&amp;rsquo;s made to be a grid with three columns closely fitted to the text size, and one more column that takes up the rest of the screen width. There&amp;rsquo;s a 10 pixel gap between the columns and the content in the grid is aligned to the end. That makes no difference for the first three links since they are the size of their containers, but the right end one is pushed to the edge of the screen minus the padding.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;header li {
 list-style-type: none;
}

header a:link, header a:visited {
 color: white;
 text-decoration: none; 
}

header a:hover {
 color: black;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even though it&amp;rsquo;s convenient to have the links in a list, we don&amp;rsquo;t want the bullet points, so &lt;code&gt;list-style-type: none;&lt;/code&gt; eliminates them. The &lt;code&gt;text-decoration: none;&lt;/code&gt; gets rid of the underline, then we add in a hover style so the user gets some feedback that this is, in fact, clickable.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.cover {
 height: 300px;
 display: grid;
 place-items: center;
}

.cover img {
 height: 280px;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The cover image was a bit more straightforward. I used a grid to centre it in both axes, and made the image a bit smaller than the div.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.projects {
 /* the grid container */
 background-color: #33BCFF;
 display: grid;
 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
 place-items: center;
}

.projects img {
 /* the grid items */
 height: 200px;
 width: 200px;
 margin: 1rem;
 background-color: #333;
 padding: 2rem;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The computer icons are displayed in another grid. This time the columns are auto-fill based on the browser width, but with a minimum width of the icons.&lt;/p&gt;
&lt;p&gt;Finally, the footer is yet another grid, just to center the text on both axes.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;footer {
 background-color: #F1948A;
 height: 3rem;
 display: grid;
 place-items: center;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://gist.github.com/IanKulin/7a30de3a7b4266850a9c16258604198b"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/eSpsqGfL9hM?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>HTML 002 - Tags for structure</title><link>https://blog.iankulin.com/html-002-tags-for-structure/</link><pubDate>Wed, 21 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/html-002-tags-for-structure/</guid><description>&lt;p&gt;I briefly mentioned &lt;a href="https://blog.iankulin.com/html-001/"&gt;earlier&lt;/a&gt; that our HTML tags should flag WHAT this part of the document is rather than how to display it (we&amp;rsquo;ll look at how to use CSS for making the content look how we want later). This idea is called semantic HTML. This post will look at some of the tags (often called &lt;a href="https://www.w3schools.com/html/html5_semantic_elements.asp"&gt;semantic tags&lt;/a&gt;) we use to convey knowledge of what part of each document an element is.&lt;/p&gt;
&lt;h4 id="why-do-i-care"&gt;Why do I care?&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s fair to ask &amp;ldquo;why bother?&amp;rdquo; If you know how you want your page to &lt;em&gt;look&lt;/em&gt;, why not just put that in the html and be done with it? There&amp;rsquo;s a couple of good answers to this:&lt;/p&gt;
&lt;p&gt;Maintainability - when we separate how the pages should look from what&amp;rsquo;s in them, that can be reused. If we have a single .css file that says which font paragraphs use, and what colour headings should be, it can be used across the site so all the pages look the same, and if the graphics designer changes their mind about the font it can be changed in a single line in one file instead of having to edit every .html file for the site.&lt;/p&gt;
&lt;p&gt;Accessibility - not every human user will be using a conventional web-browser to consume the pages, and even if they are, they might be using accessibility features to suit their abilities. I already mentioned the obvious issues for screen readers that describe things in audio, but imagine if you do not use a mouse, how helpful it might be to be able to skip from section to section in a document using a keyboard shortcut - that&amp;rsquo;s made more possible by dividing our content into sections.&lt;/p&gt;
&lt;p&gt;Skynet - Your web pages are not only consumed by humans. Search engine web crawlers and, increasingly, AI models hungry for knowledge will also use them as input. If the different parts of your pages are marked up semantically it will improve your search engine optimisation and further the rise of our robot overlords. For example, a top-level heading &lt;h1&gt; can be assumed by the machine to be a good indication of the content of the page. The text between the &lt;summary&gt; tags could be assumed to be good to show as a snippet in some search results.&lt;/p&gt;
&lt;h4 id="headings"&gt;Headings&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ve already met the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag which is intended to be the top level heading - there will probably only be one of these in your document and it will signify the content for the entire page. There&amp;rsquo;s a decreasingly important range of sub-headings from &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; down to &lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt;. The headings will be rendered differently to indicate their importance but, thinking semantically, The different levels should indicate the role of the document parts following them.&lt;/p&gt;
&lt;h4 id="body-parts"&gt;Body Parts&lt;/h4&gt;
&lt;p&gt;A number of semantic tags indicate different types of content, and many are self explanatory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;footer&gt;&lt;/footer&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;header&gt;&lt;/header&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;nav&gt;&lt;/nav&gt; - collection of navigation links
&lt;/li&gt;
&lt;li&gt;
&lt;main&gt;&lt;/main&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;summary&gt;&lt;/summary&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;/p&gt; - paragraph
&lt;/li&gt;
&lt;li&gt;
&lt;aside&gt;&lt;/aside&gt; - text that relates to, but is not essential to understanding the main text.
&lt;/li&gt;
&lt;li&gt;
&lt;main&gt;&lt;/main&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;figure&gt;&lt;/figure&gt; - illustrates a point in the text. Often it will contain an &lt;img&gt; and a &lt;figcaption&gt;
&lt;/li&gt;
&lt;li&gt;&lt;detail&gt;&lt;/detail&gt; - often contains some deeper explanation which can be hidden if not needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A couple of others are a bit vaguer, but still have semantic meaning.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;section&gt;&lt;/section&gt; - the exact meaning of a section will depend on the type of content. Maybe it's a chapter of a book, maybe a group of related auto-parts.
&lt;/li&gt;
&lt;li&gt;
&lt;article&gt;&lt;/article&gt; - could be a self-contained blog post, or you know, and article in a news page.
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="whats-not-semantic"&gt;What&amp;rsquo;s not semantic&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s a couple of tags you&amp;rsquo;ll see widely used for recent historical reasons, as well as web-dev inertia. They are containers that can be used to attach CSS styles to, but don&amp;rsquo;t convey any semantic meaning to the browser or future code maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;div&gt;&lt;/div&gt; - short for division. Used around blocks of text, usually with a class attribute so some style can be applied to it.
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;/span&gt; - similar usage as &lt;div&gt; but used for smaller, inline parts of texts.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>HTML 001</title><link>https://blog.iankulin.com/html-001/</link><pubDate>Fri, 16 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/html-001/</guid><description>&lt;p&gt;A HTML file is a text file that can be displayed in a web browser. It is &lt;em&gt;marked up&lt;/em&gt; in the sense that &lt;em&gt;tags&lt;/em&gt; are applied to the text to signify the purpose of that text in the structure of the document. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Greetings&amp;lt;/h1&amp;gt;
Hello Earthlings
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag tells the browser that &lt;code&gt;Greetings&lt;/code&gt; is a heading. The heading tag is &lt;em&gt;paired&lt;/em&gt;. There&amp;rsquo;s an opening tag &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; and closing tag &lt;code&gt;&amp;lt;/h1&amp;gt;&lt;/code&gt; that let the browser know where the heading starts and ends. Most tags are paired, but there are some &lt;em&gt;unpaired&lt;/em&gt; tags such as &lt;br&gt; which inserts a line break.&lt;/p&gt;
&lt;p&gt;If you want to go and try this out, &lt;a href="https://www.w3schools.com/html/tryit.asp?filename=tryhtml_intro"&gt;go here,&lt;/a&gt; and paste in the code above.&lt;/p&gt;
&lt;p&gt;HTML has a long complicated history we don&amp;rsquo;t care about. But in general, the tags in HTML are semantic - they are trying to describe &lt;em&gt;what&lt;/em&gt; this part of the document &lt;em&gt;is&lt;/em&gt;, rather than &lt;em&gt;how&lt;/em&gt; to &lt;em&gt;display&lt;/em&gt; it. For example, the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; heading above is usually displayed as large bold type on it&amp;rsquo;s own line. So it&amp;rsquo;s easy to think that&amp;rsquo;s what it does, but if a user is consuming this HTML document as audio the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; might be someone saying &amp;ldquo;Heading, Greetings&amp;rdquo;. The way it is expressed is different, but the meaning - that it&amp;rsquo;s a heading - is still there.&lt;/p&gt;
&lt;h4 id="tags"&gt;Tags&lt;/h4&gt;
&lt;p&gt;There are just about &lt;a href="https://www.w3schools.com/TAGS/default.asp"&gt;100 different tags&lt;/a&gt;. I&amp;rsquo;m not going to go through them all, and you&amp;rsquo;ll probably only end up using 20 or so regularly. But there&amp;rsquo;s a few that need some explanation right at the start.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;html&gt;&lt;/html&gt; - all of the document should appear between these two tags. We're saying this is HTML - hyper text markup language.
&lt;/li&gt;
&lt;li&gt;
&lt;head&gt;&lt;/head&gt; - contains some information about the document (ie metadata), for example, the &lt;title&gt;&lt;/title&gt; which is usually shown in the browser tab.
&lt;/li&gt;
&lt;li&gt;
&lt;body&gt;&lt;/body&gt; - every good HTML file should have one - the document goes in here.
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="nesting"&gt;Nesting&lt;/h4&gt;
&lt;p&gt;Tags can be nested inside other tags. So using the tags you&amp;rsquo;ve already met, we might build a simple web page like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;title&amp;gt;Greetings&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
 &amp;lt;h1&amp;gt;Greetings&amp;lt;/h1&amp;gt;
 Hello Earthling
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Later on, things will get much more nested. The line breaks and indents used here are entirely for clarity. The browser does not need them. It would be just as legal to write this exact same document as:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Greetings&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt; Greetings&amp;lt;/h1&amp;gt;Hello 
Earthling&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But, you know&amp;hellip; don&amp;rsquo;t do that.&lt;/p&gt;
&lt;h4 id="image-tag"&gt;Image tag&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://www.w3schools.com/TAGS/tag_img.asp"&gt;&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;&lt;/a&gt; - It you want to have an image in your web-page, you use the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag and include a link to the image. The image could be sitting in the same directory as your html file, or anywhere else on the internet. If our image is here, and called example.png, the image tag would look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;img src=&amp;#34;example.png&amp;#34;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let&amp;rsquo;s add it to our Greetings page:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;title&amp;gt;Greetings&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
 &amp;lt;h1&amp;gt;Greetings&amp;lt;/h1&amp;gt;
 Hello Earthling
 &amp;lt;img src=&amp;#34;example.png&amp;#34;&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; is one of those unpaired tags - there&amp;rsquo;s no closing tag. If the image was somewhere else on the internet, you just use the full URL as the source:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;title&amp;gt;Greetings&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
 &amp;lt;h1&amp;gt;Greetings&amp;lt;/h1&amp;gt;
 Hello Earthling
 &amp;lt;img src=&amp;#34;https://photojournal.jpl.nasa.gov/browse/PIA00114.gif&amp;#34;/&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Our web page now looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-14-at-8.51.41-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>CSS for Beginners</title><link>https://blog.iankulin.com/css-for-beginners/</link><pubDate>Thu, 15 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/css-for-beginners/</guid><description>&lt;p&gt;I mentioned a couple of days ago that the ZTM webdev course was skipping forwards too quick and that it would need to be supplemented. For CSS, I think the supplement for me is going to be this &lt;a href="https://www.youtube.com/playlist?list=PL0Zuz27SZ-6Mx9fd9elt80G1bPcySmWit"&gt;series&lt;/a&gt; from Dave Gray.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/0W6qz0-aDaM?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Who is Emmet?</title><link>https://blog.iankulin.com/who-is-emmet/</link><pubDate>Wed, 14 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/who-is-emmet/</guid><description>&lt;p&gt;&lt;a href="https://www.piqsels.com/en/public-domain-photo-ircsa"&gt;&lt;img src="https://blog.iankulin.com/images/css-hacks.jpg" width="728" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I knew there was some magical way of entering all the the &lt;HTML&gt; boilerplate in Visual Studio Code as I&amp;rsquo;d seen it happen in several videos, and assumed is was some sort of macro expansion thing in the editor. Fast forward a few blog post readings and youtube viewings and I keep seeing tangential references to someone called Emmet. Turns out they&amp;rsquo;re the same thing, and it&amp;rsquo;s pretty cool.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a new idea to have functionality in code editors to insert snippets of code. &lt;a href="https://docs.emmet.io/"&gt;Emmet&lt;/a&gt; goes a bit further than that - and like many tools made by programmers for programmers it goes way to technical to the point where you need to memorise ridiculous amounts of combos to to some awesome stuff (I&amp;rsquo;m looking at you whoever made it possible to use vi commands in VS Code). Nevertheless, Emmet is extremely handy even at my n00b level.&lt;/p&gt;
&lt;p&gt;The key thing to know, is that it borrows from the CSS selector syntax. So if you want to insert &lt;code&gt;&amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt; you enter &lt;code&gt;div&lt;/code&gt; and press tab.&lt;/p&gt;
&lt;p&gt;Want a div with a class named &amp;ldquo;container&amp;rdquo;?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;div.container&lt;/code&gt; becomes &lt;code&gt;&amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The same trick works for an id - Enter&lt;/p&gt;
&lt;p&gt;&lt;code&gt;div#emmet&lt;/code&gt; becomes &lt;code&gt;&amp;lt;div id=&amp;quot;emmet&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Would you like a div, with a heading inside? The greater than sign nests elements, so &lt;code&gt;div&amp;gt;h4&lt;/code&gt; becomes:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;div&amp;gt;
 &amp;lt;h4&amp;gt;&amp;lt;/h4&amp;gt;
&amp;lt;div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you&amp;rsquo;d like some text up in there, try div&amp;gt;h4{Hello world}&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;div&amp;gt;
 &amp;lt;h4&amp;gt;Hello world&amp;lt;/h4&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can repeat things numbers of times, so to create a list with three items, try &lt;code&gt;ul&amp;gt;li*3&lt;/code&gt; to get:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
 &amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s about as complex as I&amp;rsquo;d want to get, though of course it gets more complex. It&amp;rsquo;s a super handy feature that quickly becomes second nature.&lt;/p&gt;</description></item><item><title>ZTM - Complete Web Developer</title><link>https://blog.iankulin.com/ztm-complete-web-developer/</link><pubDate>Tue, 13 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ztm-complete-web-developer/</guid><description>&lt;p&gt;&lt;a href="https://zerotomastery.io/courses/coding-bootcamp/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-11-at-8.31.15-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I started my first Udemy a few days ago. I was watching one of those &amp;ldquo;&lt;a href="https://www.youtube.com/watch?v=cYNVVspXUdA"&gt;How I&amp;rsquo;d learn to code if I started over&lt;/a&gt;&amp;rdquo; YouTubes, mainly because I&amp;rsquo;d like to know enough JavaScript to write little REST API&amp;rsquo;s on Node.js, but also because I&amp;rsquo;m starting to think web development makes more sense for a couple of the applications I&amp;rsquo;ve got on my (ever growing) list of app ideas.&lt;/p&gt;
&lt;p&gt;The video recommended a &amp;ldquo;&lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;Zero to Mastery&lt;/a&gt;&amp;rdquo; course. When I googled it, I could see on Udemy it had a stack of people enrolled, had been updated recently, it had 40 hours of video content, good ratings (4.7 from 57K reviews), and claimed to cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML/HTML5&lt;/li&gt;
&lt;li&gt;CSS/CSS3&lt;/li&gt;
&lt;li&gt;SemanticUI&lt;/li&gt;
&lt;li&gt;Responsive Design&lt;/li&gt;
&lt;li&gt;Flexbox&lt;/li&gt;
&lt;li&gt;CSS Grid&lt;/li&gt;
&lt;li&gt;Bootstrap 5&lt;/li&gt;
&lt;li&gt;DOM Manipulation&lt;/li&gt;
&lt;li&gt;Javascript (including ES6/ES7/ES8/ES9/ES10/ES2020/ES2021/ES2022)&lt;/li&gt;
&lt;li&gt;Asynchronous JavaScript&lt;/li&gt;
&lt;li&gt;HTTP/JSON/AJAX&lt;/li&gt;
&lt;li&gt;React + Redux + React Hooks&lt;/li&gt;
&lt;li&gt;Git + Github&lt;/li&gt;
&lt;li&gt;Command Line&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Express.js&lt;/li&gt;
&lt;li&gt;NPM&lt;/li&gt;
&lt;li&gt;RESTful API Design&lt;/li&gt;
&lt;li&gt;PostgresSQL&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Scalable Infrastructure&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;Production and Deployment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which is a lot for $20!&lt;/p&gt;
&lt;p&gt;So far I&amp;rsquo;m up to section 9 (out of about 30) which is still in the 2nd item above - CSS, and I have a couple of observations, especially in comparison with my experience of Paul Hudson&amp;rsquo;s &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of Swift UI&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="video-v-text"&gt;Video v Text&lt;/h4&gt;
&lt;p&gt;The content in the ZTM course is more video orientated. Under each of Paul&amp;rsquo;s videos is a text version that very closely follows the video (although no dog snacks). If you&amp;rsquo;re a person who likes to follow along all the code, the text versions are valuable. I&amp;rsquo;ve found myself winding the ZTM videos back and forward a few times to see the code on their screen to figure out where I&amp;rsquo;ve gone wrong.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;ve been doing #100Days I&amp;rsquo;ve gone back and forwards a bit on watching the videos or reading the text. It probably just comes down to mood, but the option is very nice to have.&lt;/p&gt;
&lt;h4 id="size-of-the-bites"&gt;Size of the bites&lt;/h4&gt;
&lt;p&gt;The ZTM seems to go a bit faster, and leave the student to do some of their own research. I guess they&amp;rsquo;re covering a bigger topic (full stack web development including tools v SwiftUI) in less time - Paul is asking for 100 hours, ZTM is around 40. But I am used to leaving each of Paul&amp;rsquo;s sessions feeling like I really know it, whereas with the ZTM stuff it&amp;rsquo;s more like &amp;ldquo;I know that thing exists and I could probably remember it well enough to google it if a situation called for it&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;On the current topic - CSS, I am definitely going to have to spend some time on content outside of the course to get my head around it, and I think that&amp;rsquo;s likely to be the same with the JavaScript content.&lt;/p&gt;
&lt;h4 id="currency"&gt;Currency&lt;/h4&gt;
&lt;p&gt;Both of these courses are being kept up to date, which is crucial (and probably a pain for the course owners), but I feel Paul must do a better job of re-writing and re-shooting his content - I&amp;rsquo;ve never bumped into any out of date content yet. In the ZTM, they include some old stuff (for example &lt;a href="https://getbootstrap.com/"&gt;BootStrap&lt;/a&gt;) but prefix it with advice that it&amp;rsquo;s not current best practice, and several videos have either overlays about or are preceded with advice about how to make things work in current versions. It&amp;rsquo;s great they have those, but not as good as Paul&amp;rsquo;s system. Again, that might also just be easier with SwiftUI - everything comes from Apple each September, so it&amp;rsquo;s on a regular schedule as well as being a much smaller volume (than all the possible third party stuff going on in web development each month).&lt;/p&gt;
&lt;h4 id="student-activity"&gt;Student Activity&lt;/h4&gt;
&lt;p&gt;Paul seems to have more challenges along the way to check that you&amp;rsquo;ve actually absorbed the learning, and if you&amp;rsquo;re a hacking with swift+ member (I am) there&amp;rsquo;s a feedback video. I&amp;rsquo;m about 20% the way through the ZTM and just got to my first one - it&amp;rsquo;s been a more passive experience so far. That might change as the content get harder - so far it&amp;rsquo;s been pretty straight forward HTML, and I&amp;rsquo;m only now feeling challenged in all the CSS layout stuff, so this challenge and feedback is coming at the right time.&lt;/p&gt;
&lt;h4 id="cost"&gt;Cost&lt;/h4&gt;
&lt;p&gt;Although Paul&amp;rsquo;s 100 Days is completely free, it is so good, I felt obliged to pay for the + membership, and I bought a book I don&amp;rsquo;t have time to read in his Black Friday sale - so that&amp;rsquo;s about $340 total. The ZTM Complete Web Developer was &amp;ldquo;on special&amp;rdquo; for $18 when I bought it, but is now saying it&amp;rsquo;s back up to $27. I have a feeling if I visited it in incognito mode it might be on special again. I have heard a couple of people on podcasts mention they have bought more Udemy courses than they can every complete - it&amp;rsquo;s the Steam business model. So I&amp;rsquo;m sworn off any more until this one is finished.&lt;/p&gt;
&lt;p&gt;Overall, I&amp;rsquo;m very happy with this course so far. I&amp;rsquo;d heartily recommend it based on what I&amp;rsquo;ve done so far.&lt;/p&gt;</description></item><item><title>Visual Studio Code</title><link>https://blog.iankulin.com/visual-studio-code-2/</link><pubDate>Mon, 12 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/visual-studio-code-2/</guid><description>&lt;p&gt;I&amp;rsquo;ve gone over to the dark side a little. As I think about the sort of apps I want to make, I realise I am going to need to be able to do back-end web development. My apps are going to need a secure REST api to a database. I guess that means node.js. I&amp;rsquo;m also conscious that my ticket app needs to run on android, and a short cut around all of that might be to make the whole thing a web app from the start, but with the premium experience on iOS.&lt;/p&gt;
&lt;p&gt;So with that in mind, I dropped $18 on the &amp;ldquo;&lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;The Complete Web Developer in 2023: Zero to Mastery&lt;/a&gt;&amp;rdquo; course on Udemy. I&amp;rsquo;m not sure how I think I have the time for that, but anyway&amp;hellip;.&lt;/p&gt;
&lt;p&gt;They recommend, and their examples are using, the &lt;a href="https://www.sublimetext.com/"&gt;Sublime Text&lt;/a&gt; editor, which has an even simpler (and therefore cooler) look than VS Code, but I was already impressed with VSCode so I&amp;rsquo;ve been using that, and gotta say, love it. Things just keep working how I expect, and a fair bit of that is due to good plugins.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-10-at-2.55.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://zerotomastery.io/"&gt;ZTM&lt;/a&gt; course is a bit different to what I&amp;rsquo;ve been getting from the 100 Days of SwiftUI, but like that, it starts of by saying you need to do the things to be accountable or you&amp;rsquo;ll likely drop out along the way. That&amp;rsquo;s a fair point, and I thought about adding it to my posting, but really it&amp;rsquo;s more of a side project so I&amp;rsquo;m not sure you would see that here.&lt;/p&gt;
&lt;p&gt;I could (and might) start another blog for it, but I&amp;rsquo;ve committed to myself to post here on something (at least vaguely) iOS development related, and some week&amp;rsquo;s that&amp;rsquo;s a struggle, so I guess we&amp;rsquo;ll see.&lt;/p&gt;</description></item><item><title>ChatGPT's code writing</title><link>https://blog.iankulin.com/chatgpts-code-writing/</link><pubDate>Sun, 11 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/chatgpts-code-writing/</guid><description>&lt;p&gt;This week, the internet has been all about &lt;a href="https://chat.openai.com/chat"&gt;ChatGPT&lt;/a&gt;, the rather remarkable natural language AI with a very large model. If you&amp;rsquo;re a twitter user, you were probably amazed, but now eventually tired of seeing examples of it&amp;rsquo;s output. I&amp;rsquo;ll add to that with an example of a SwifUI CoreData based todo app it wrote for me from a single sentence prompt below. Rather than look at other people&amp;rsquo;s examples you should definitely go and play with it yourself - it is very impressive. Along with the image based AI&amp;rsquo;s it&amp;rsquo;s made 2022 into a historical year for AI.&lt;/p&gt;
&lt;p&gt;ChatGPT will write software. I can&amp;rsquo;t see it replacing developers, but it seems likely there&amp;rsquo;ll be a bunch of interesting tools to come over the next few years. I feel like the first big effect will be the volume of mediocre internet content will increase dramatically as it is pretty great at run of the mill copy-writing. I&amp;rsquo;d like to assure my readers that all my mediocre internet content will continue to be human written - with the exception of most of the art, and the code example below :- )&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;write swiftui code for a todo application where the items reoccur after a certain number of days. The items are shown in a list and the user can navigate to a detail view.&lt;/p&gt;
&lt;p&gt;me&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Here is an example of SwiftUI code for a todo application where the items reoccur after a certain number of days:&lt;/p&gt;
&lt;p&gt;ChatGPT&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TodoItem&lt;/span&gt;: Identifiable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; task: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; dueDate: Date
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; repeatInterval: Int &lt;span style="color:#75715e"&gt;// number of days until the task reoccurs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TodoList&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items = [TodoItem]()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List(items) { item &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationLink(destination: TodoDetail(item: item)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(item.task)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TodoDetail&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; item: TodoItem
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(item.task)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Due: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;item.dueDate, formatter: DateFormatter.mediumDate&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Reoccurs every: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;item.repeatInterval&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; days&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;In the &lt;code&gt;TodoList&lt;/code&gt; view, a &lt;code&gt;List&lt;/code&gt; is used to display the todo items. Each item in the list is wrapped in a &lt;code&gt;NavigationLink&lt;/code&gt; that takes the user to a &lt;code&gt;TodoDetail&lt;/code&gt; view when tapped. The &lt;code&gt;TodoDetail&lt;/code&gt; view displays the task, the due date, and the repeat interval for the selected todo item.&lt;/p&gt;
&lt;p&gt;You can modify this code to suit your specific needs, such as adding features to add or delete items, or changing the layout and appearance of the views.&lt;/p&gt;
&lt;p&gt;ChatGPT&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&amp;rsquo;s pretty cool. I don&amp;rsquo;t think that DateFormatter.mediumDate is a thing, so I changed that line to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Text(&amp;#34;Due: \(item.dueDate, style: .date)&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s okay ChatGPT, I get confused about the variety of ways to change dates too.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;great. show me the content view code that contains the TodoList and has a button to add items.&lt;/p&gt;
&lt;p&gt;me&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Here is an example of a &lt;code&gt;ContentView&lt;/code&gt; that contains a &lt;code&gt;TodoList&lt;/code&gt; and has a button to add items:&lt;/p&gt;
&lt;p&gt;ChatGPT&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items = [TodoItem]()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; TodoList(items: &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;items)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(action: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// add new todo item here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Add Item&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;In this code, the &lt;code&gt;ContentView&lt;/code&gt; contains a &lt;code&gt;TodoList&lt;/code&gt; and a &lt;code&gt;Button&lt;/code&gt;. The &lt;code&gt;TodoList&lt;/code&gt; is initialized with the &lt;code&gt;items&lt;/code&gt; array, which is marked as &lt;code&gt;@State&lt;/code&gt; so that it can be modified in response to user actions. The &lt;code&gt;Button&lt;/code&gt; has an action closure that is executed when the button is tapped. You can add code inside this closure to create a new &lt;code&gt;TodoItem&lt;/code&gt; and add it to the &lt;code&gt;items&lt;/code&gt; array.&lt;/p&gt;
&lt;p&gt;You can modify this code to suit your specific needs, such as changing the layout and appearance of the views, or adding features to delete items from the list.&lt;/p&gt;
&lt;p&gt;ChatGPT&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To let this compile, I commented out the Preview and changed the items property in the top of the TodoList view to a binding:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct TodoList: View {
 @Binding var items: [TodoItem]
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;thank you. what would the code for adding an item look like&lt;/p&gt;
&lt;p&gt;me&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Here is an example of code that you can use to add a new &lt;code&gt;TodoItem&lt;/code&gt; to the &lt;code&gt;items&lt;/code&gt; array when the button is tapped:&lt;/p&gt;
&lt;p&gt;ChatGPT&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items = [TodoItem]()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; TodoList(items: &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;items)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(action: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; newItem = TodoItem(task: &lt;span style="color:#e6db74"&gt;&amp;#34;New Task&amp;#34;&lt;/span&gt;, dueDate: Date(), repeatInterval: &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items.append(newItem)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Add Item&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;In this code, the &lt;code&gt;Button&lt;/code&gt; has an action closure that creates a new &lt;code&gt;TodoItem&lt;/code&gt; with the task &amp;ldquo;New Task&amp;rdquo;, a due date of the current date, and a repeat interval of 7 days. The new item is then appended to the &lt;code&gt;items&lt;/code&gt; array.&lt;/p&gt;
&lt;p&gt;You can modify this code to suit your specific needs, such as prompting the user for the task name and due date, or changing the default repeat interval. You can also add additional logic to handle errors or validation failures when creating the new item.&lt;/p&gt;
&lt;p&gt;ChateGPT&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This all runs and the add item works, but the NavigationLink to get to the detail view does not. Let&amp;rsquo;s wrap it in a NavigationView to fix that.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct ContentView: View {
 @State var items = [TodoItem]()

 var body: some View {
 NavigationView {
 VStack {
 TodoList(items: $items)
 Button(action: {
 let newItem = TodoItem(task: &amp;#34;New Task&amp;#34;, dueDate: Date(), repeatInterval: 7)
 items.append(newItem)
 }) {
 Text(&amp;#34;Add Item&amp;#34;)
 }
 }
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I went on to ask it to change this to core data for persistence, but as I wanted the app to be more and more complex, it needed me to fix more and more problems, and became less fun.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-10-at-2.47.12-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-10-at-2.47.16-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Sharing is caring</title><link>https://blog.iankulin.com/sharing-is-caring/</link><pubDate>Sat, 10 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sharing-is-caring/</guid><description>&lt;p&gt;Continuing on with the demo project from yesterday, in which we used the ImageRenderer class to turn a view into an image, today we want to let the user share it somehow.&lt;/p&gt;
&lt;p&gt;Typically, apps have a button using the square.and.arrow.up SF Symbol to share something from the current screen. It&amp;rsquo;s probably not an accident that it&amp;rsquo;s literally the first symbol in the app.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-05-at-9.23.33-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Pressing it generally opens the &amp;ldquo;share sheet&amp;rdquo; which has options for opening whatever is being shared in another app, printing it, saving it to photos, or whatever.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s our ticket app from a couple of days ago (the TicketView is unchanged). We&amp;rsquo;re still using ImageRenderer() to make the image version of the view in OnAppear(), but this time there&amp;rsquo;s a &amp;ldquo;sharelink&amp;rdquo;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; ticketView = TicketView()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; image: Image?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ticketView
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; image = image {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ShareLink(item: image, preview: SharePreview(&lt;span style="color:#e6db74"&gt;&amp;#34;View&amp;#34;&lt;/span&gt;,image: image)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Label(&lt;span style="color:#e6db74"&gt;&amp;#34;Share&amp;#34;&lt;/span&gt;, systemImage: &lt;span style="color:#e6db74"&gt;&amp;#34;square.and.arrow.up&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .onAppear {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; renderer = ImageRenderer(content: ticketView)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; uiImage = renderer.uiImage {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image = Image(uiImage: uiImage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we click on the share link, it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/simulator-screen-shot-iphone-14-pro-2022-12-05-at-21.10.20.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The little thumbnail and the word &amp;ldquo;View&amp;rdquo; at the top of the share sheet is from the preview parameter in our call.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ShareLink(item: image, preview: SharePreview(&amp;#34;View&amp;#34;,image: image)) {
 Label(&amp;#34;Share&amp;#34;, systemImage: &amp;#34;square.and.arrow.up&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since this is an image, perhaps you were expecting a &amp;ldquo;Save image&amp;rdquo; option? That&amp;rsquo;s the most likely thing we&amp;rsquo;d want to do with an image, but it&amp;rsquo;s not there. The reason is that saving an image to the users camera roll requires a specific permission. In the app settings, choose &amp;ldquo;Info&amp;rdquo; then right click on the entries and &amp;ldquo;Add Row&amp;rdquo;. Search for / add the key &amp;ldquo;Privacy - Photo Library Additions Usage Description&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-07-at-8.33.52-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The text you add in the description is what the app will show to the user when the dialogue pops up asking the user if it&amp;rsquo;s okay to give this app the permission to save photos.&lt;/p&gt;
&lt;p&gt;If we go back and and try again to share the ticket, we&amp;rsquo;ve now got the options to &amp;ldquo;Save Image&amp;rdquo;. If we choose that for the first time, we&amp;rsquo;ll be asked to grant permission to this app.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-07-at-8.41.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If the user chooses OK, then it will be saved to photos:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-07-at-8.42.04-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Web Development Links</title><link>https://blog.iankulin.com/web-development-links/</link><pubDate>Sat, 10 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/web-development-links/</guid><description>&lt;p&gt;Sites recommended by the Complete Web Developer course I&amp;rsquo;m doing on Udemy.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.w3schools.com/"&gt;https://www.w3schools.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://css-tricks.com/"&gt;https://css-tricks.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://paletton.com/"&gt;https://paletton.com/&lt;/a&gt; - web colour picker&lt;/p&gt;
&lt;p&gt;&lt;a href="https://animate.style"&gt;https://animate.style&lt;/a&gt;/ - css animations&lt;/p&gt;</description></item><item><title>Clean code</title><link>https://blog.iankulin.com/clean-code/</link><pubDate>Fri, 09 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/clean-code/</guid><description>&lt;p&gt;I&amp;rsquo;ve been listening to the &lt;a href="https://www.youtube.com/watch?v=YVrHPCZnC50"&gt;latest episode of the Empower Apps&lt;/a&gt; podcast, this one with &lt;a href="https://twitter.com/Jilsco9"&gt;Jill Scott&lt;/a&gt; talking about &amp;ldquo;Humane&amp;rdquo; development - in the sense of being humane to whoever (probably you) is going to be reading this code in the future. It helped me clarify my thoughts about a couple of things.&lt;/p&gt;
&lt;p&gt;None of these ideas are particularly new or groundbreaking, and although I think of them as my personal style, they are very common, and in Swift could be regarded as part of the culture. Some of these concepts support each other, some represent a trade off between two opposing ideas that require us to make a choice.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/onion-belt.jpg" alt=""&gt;&lt;/p&gt;
&lt;h4 id="the-custom-at-the-time"&gt;The Custom at the Time&lt;/h4&gt;
&lt;p&gt;If other people or bots are going to read your code, or you need to comprehend theirs, there is a lot of value in following the conventions in the language of community you work in. It helps in a couple of ways - 1) you are not expending energy deciding if equals signs should have a space each side, and 2) fluency of reading and writing will improve.&lt;/p&gt;
&lt;h4 id="natural-language"&gt;Natural Language&lt;/h4&gt;
&lt;p&gt;If it&amp;rsquo;s possible to make choices in a piece of code to make it read more like a description of what is happening, then usually do that. Swift (and probably other modern languages - I wouldn&amp;rsquo;t know) has some great language features to support this. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;answer = resultOf(6, plus: 7)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I also appreciate the Swift convention of using auxiliary verbs. I think &lt;code&gt;isPaused&lt;/code&gt; or &lt;code&gt;hasCompleted&lt;/code&gt; is clearer that &lt;code&gt;paused&lt;/code&gt; or &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="decomposed"&gt;Decomposed&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s probably a reason why paragraphs exist in writing, and are about the length they usually are, deep in the science of how memory works in humans and the interplay between working memory and what else goes on in comprehending something.&lt;/p&gt;
&lt;p&gt;I start to get uncomfortable if a chunk of code I&amp;rsquo;m trying to understand or write is more than a couple of 13&amp;quot; laptop screens long. I thought &lt;a href="https://davidstechtips.com/2012/05/folding-code-in-xcode/comment-page-1/"&gt;code folding&lt;/a&gt; would help but haven&amp;rsquo;t really found that. I aim to have each chunk (I&amp;rsquo;m using &amp;ldquo;chunk&amp;rdquo; for function, method, computed property etc) express a single idea. If part of it is getting long, I consider if that can be removed somewhere else and replaced with a helpful function name in the piece I&amp;rsquo;m working on.&lt;/p&gt;
&lt;h4 id="less-magic"&gt;Less Magic&lt;/h4&gt;
&lt;p&gt;When I&amp;rsquo;m working with some code, I don&amp;rsquo;t want it to need too much context to understand. This principle means anything used in this code should come in through the obvious interface. Global variables, environment variables, or stuff captured from the enclosing scope are undesirable. If I use them, I try and put them near the top since that&amp;rsquo;s where people (me) look when they encounter something part way through the code and don&amp;rsquo;t know where it came from.&lt;/p&gt;
&lt;p&gt;In an ideal world, I could grab a piece of code and paste it into a &lt;a href="https://gist.github.com/discover"&gt;gist&lt;/a&gt; to share here and it would be comprehensible.&lt;/p&gt;
&lt;h4 id="evaporated-comments"&gt;Evaporated Comments&lt;/h4&gt;
&lt;p&gt;I doubt I invented this, but I haven&amp;rsquo;t seen it mentioned anywhere else either. The way I most commonly use comments is to clarify my thoughts before I write any code, something like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func sendFile(fileName: String, link: NetworkTube) -&amp;gt; SendResultCode {
 // check link is operational
 // attempt to open file
 // step through each line sending it, wait for ack 
 // close file
 // return code
} 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then as I flesh out the function, I delete the comment if the code is straight forward. Usually this results in all of the comments being deleted. In my Swift code, comments are quite rare. Where there are comments, it&amp;rsquo;s probably a sign I need to name things better.&lt;/p&gt;
&lt;p&gt;I just went back through the code for that apps I actually use on my phone, and these are the only comments I could find outside of a header.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var fractionDue: Double {
 // when a habit is overdue, or due now, the fractionDue is 1.0
 // when it&amp;#39;s not due at all - just been done, the fractioDue is 0.0
 if isDueNow {
 return 1.0
 } else {
 let daysSinceDone = Date().timeIntervalSince(lastDone) / 86_400
 assert(daysBetweenCompletions &amp;gt; 0.0)
 return daysSinceDone / daysBetweenCompletions
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is a computed variable in a &amp;ldquo;Habit&amp;rdquo; struct. I wanted to have little ticks next to each habit when they were done, which would slowly fade to be completely gone when this habit was due again. To achieve this I needed to calculate an opacity value for the tick. I think it&amp;rsquo;s fair to say this needs re-working. I don&amp;rsquo;t recall if I wrote the comment first, or put it there later recognising the code wasn&amp;rsquo;t self explanatory - it could have been either.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure I could fix this to the point it wouldn&amp;rsquo;t need a comment at all. There&amp;rsquo;s a couple of name changes that would help. I think &lt;code&gt;daysSinceDone&lt;/code&gt; would be better as &lt;code&gt;daysSinceLastCompleted&lt;/code&gt;, and instead of calling the property &lt;code&gt;fractionDue&lt;/code&gt;, I might call it &lt;code&gt;freshness&lt;/code&gt;. Instead of dividing the time interval by 86,400 I could have a &lt;code&gt;millisecondsToDays()&lt;/code&gt;function.&lt;/p&gt;</description></item><item><title>ImageRenderer()</title><link>https://blog.iankulin.com/imagerenderer/</link><pubDate>Thu, 08 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/imagerenderer/</guid><description>&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/swiftui/imagerenderer"&gt;ImageRenderer&lt;/a&gt;() is a SwiftUI class that creates an image from a view. You just initialize it with the view, then extract a cgImage (Core Graphics) or uiImage that can be cast to a SwiftUI Image.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll need a view to work with, so here it is; a crude version of my behaviour ticket.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TicketView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ZStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Color(.cyan)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .frame(width: &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;, height: &lt;span style="color:#ae81ff"&gt;350&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Fred Bloggs&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.largeTitle)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Putting rubbish in the bin&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;trash&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .foregroundColor(.purple)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Green Faction&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;Date&lt;span style="color:#e6db74"&gt;()&lt;/span&gt;.formatted&lt;span style="color:#e6db74"&gt;())&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here it is, with a couple of buttons underneath:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/simulator-screen-shot-iphone-14-pro-2022-12-05-at-19.56.18.png" width="209" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve assigned the view to a property of my content view, then displayed it along with the buttons. The first button saves the image to an @State and the second one displays it if it exists.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; ticketView = TicketView()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; image: Image?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; showImage = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ticketView
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Save Image&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; renderer = ImageRenderer(content: ticketView)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; uiImage = renderer.uiImage {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image = Image(uiImage: uiImage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }.padding()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Toggle image&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; withAnimation {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showImage.toggle()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; showImage {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; image = image {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .resizable()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .scaledToFit()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we save the screen:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Button(&amp;#34;Save Image&amp;#34;) {
 let renderer = ImageRenderer(content: ticketView)
 
 if let uiImage = renderer.uiImage {
 image = Image(uiImage: uiImage)
 }
}.padding()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then it can just be inserted in the view same as any bitmap image by hitting the Toggle Image button.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/simulator-screen-shot-iphone-14-pro-2022-12-05-at-20.01.49.png" width="472" alt=""&gt;</description></item><item><title>SwiftUI provides</title><link>https://blog.iankulin.com/swiftui-provides/</link><pubDate>Wed, 07 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swiftui-provides/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_3476.png" width="284" alt=""&gt;
&lt;p&gt;A few hours after I speculated about pausing work on the tickets app because outputting the tickets was too far out of my expertise, a helpful instance of the &lt;a href="https://en.wikipedia.org/wiki/Frequency_illusion"&gt;Baader–Meinhof phenomenon&lt;/a&gt; threw up some help in the form of this tweet from &lt;a href="https://twitter.com/flowritescode"&gt;@FloWritesCode&lt;/a&gt;. It turns out this was an addition in iOS16 announced at WWDC that makes this straightforward.&lt;/p&gt;
&lt;p&gt;As soon as I googled around about it I also found good solutions that wrapped the old code to provide similar functionality. So that&amp;rsquo;s a lesson for me about not assuming something&amp;rsquo;s hard before I&amp;rsquo;ve spent some time investigating it. I took that lesson and applied it to rendering to a PDF, and of course, @twostraws &lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-render-a-swiftui-view-to-a-pdf"&gt;has a code example&lt;/a&gt; for that from three days ago!&lt;/p&gt;
&lt;p&gt;Obviously I&amp;rsquo;m going to have some fiddling around to lay it out and so on, and I still need to figure out the share behaviour (which I&amp;rsquo;m expecting to be straightforward) but it feels like SwiftUI is just going to help me express my intents in code. I&amp;rsquo;m really enjoying this language, framework and community!&lt;/p&gt;</description></item><item><title>Committed</title><link>https://blog.iankulin.com/committed/</link><pubDate>Tue, 06 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/committed/</guid><description>&lt;p&gt;I quite like logging into GitHub and seeing my commit history as the graph with the green dots. Once I get up to a year it would be a great thing to have on a T-Shirt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-03-at-7.36.29-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d expect to be seeing the busy weekends, but Tuesday nights seem to be oddly productive. It could just be a start of the week energy thing - I have some other community obligations on a couple of Monday nights a month.&lt;/p&gt;
&lt;p&gt;git is an amazing tool - better that the commercial tool I used when I was programming a long way back. I really admire what the many open source programmers have put in to it. I also owe some gratitude to Microsoft for making GitHub available to newbs like me at no cost - it&amp;rsquo;s an great service.&lt;/p&gt;
&lt;p&gt;According to the graph, my first commits were on July 10. My first blog post was on the 5th, and I&amp;rsquo;m on a 154 once per day posting streak - although there&amp;rsquo;s a few two per day scattered through there when I&amp;rsquo;ve been on holidays. I&amp;rsquo;m only up to day 64 of the 100 Days of SwiftUI - so I clearly I haven&amp;rsquo;t been getting in an hour each day on that religiously, but overall, I&amp;rsquo;m pretty happy with my commitment to myself.&lt;/p&gt;</description></item><item><title>Ticket to ride</title><link>https://blog.iankulin.com/ticket-to-ride/</link><pubDate>Mon, 05 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ticket-to-ride/</guid><description>&lt;p&gt;A &lt;a href="https://blog.iankulin.com/project-based-learning/"&gt;couple of days ago&lt;/a&gt; I was lauding the learning benefits of writing your own projects over completing tutorial projects - since your own projects push your boundaries further. Of course, its also the case that the project requirements might so completely exceed your current ability that it grinds to a halt. That&amp;rsquo;s the case with my &lt;a href="https://blog.iankulin.com/tickets-on-myself/"&gt;behaviour ticket app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The part of the app for collecting the data is pretty much done and how I imagined it, but the output needs to be pretty tickets that can be printed on paper. I managed to write the ticket data to a CSV file and export that to the files app with a .fileExporter, but really what I wanted is to have one of those share screens where you can chose to AirDrop, Print etc, and for the tickets to have been rendered to a PDF or series of images to be shared. That will have to wait. I&amp;rsquo;m just up to a bit in the #100Days about writing images so I&amp;rsquo;ll push on with that for a bit and come back to my app.&lt;/p&gt;
&lt;p&gt;In the meantime, it&amp;rsquo;s worth going over how to create and export the text file briefly.&lt;/p&gt;
&lt;p&gt;First of all, I stole this TextFile code from Paul Hudson (&lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-document-based-app-using-filedocument-and-documentgroup"&gt;here&lt;/a&gt;) that wraps some complexity neatly into a struct.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// TextFile.swift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Stolen from Paul Hudson @twostraws&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-document-based-app-using-filedocument-and-documentgroup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SwiftUI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UniformTypeIdentifiers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TextFile&lt;/span&gt;: FileDocument {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// tell the system we support only plain text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; readableContentTypes = [UTType.plainText]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// by default our document is empty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; text = &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// a simple initializer that creates new, empty documents&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;(initialText: String = &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; text = initialText
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// this initializer loads data that has been saved previously&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;(configuration: ReadConfiguration) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = configuration.file.regularFileContents {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; text = String(decoding: data, &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt;: UTF8.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; CocoaError(.fileReadCorruptFile)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// this will be called when the system wants to write our data to disk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fileWrapper&lt;/span&gt;(configuration: WriteConfiguration) &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; -&amp;gt; FileWrapper {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = Data(text.utf8)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; FileWrapper(regularFileWithContents: data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now here&amp;rsquo;s all the modifiers I&amp;rsquo;ve got attached to the list of tickets.&lt;/p&gt;
&lt;p&gt;The toolbar button just assigns a string to the instance of Paul&amp;rsquo;s TextFile(). The string is built by just stepping through the tickets in a for loop and appending csv strings for each ticket to the big string that becomes the file.&lt;/p&gt;
&lt;p&gt;The .fileExporter then does the heavy lifting. It slides up a view of the files in the &amp;ldquo;On My Phone&amp;rdquo; folder, lets the user name the file and saves it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Tickets&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .fileExporter(isPresented: &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;showingExporter, document: exportFile, contentType: .plainText) { result &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; result {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; .success(&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; url):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Saved to &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;url&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; .failure(&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; error):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(error.localizedDescription)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .toolbar {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exportFile.text = ticketsExportText()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showingExporter = &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;square.and.arrow.up&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding(.horizontal)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Pi Server</title><link>https://blog.iankulin.com/pi-server/</link><pubDate>Sun, 04 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/pi-server/</guid><description>&lt;p&gt;I have a a couple of Raspberry Pi&amp;rsquo;s on my home network. One is a radio interface on the &lt;a href="https://www.allstarlink.org/"&gt;AllStar network&lt;/a&gt;, and the other is just a toy server - I can&amp;rsquo;t actually recall why I bought it. Both of them are Model 3B&amp;rsquo;s - I&amp;rsquo;d love a 4, but they are scarce and expensive.&lt;/p&gt;
&lt;p&gt;This doesn&amp;rsquo;t have much to do with Swift, although it&amp;rsquo;s possible to run &lt;a href="https://lickability.com/blog/swift-on-raspberry-pi/"&gt;Swift on a Pi&lt;/a&gt;, or even &lt;a href="https://medium.com/@jhheider/installing-vapor-and-swift-on-the-raspberry-pi-45a6c7baef35"&gt;Vapor&lt;/a&gt;. Mine is set up as a generic web server that I use as the back end for my tiny projects. It runs &lt;a href="https://nodejs.org/en/about/"&gt;Node.js&lt;/a&gt;, &lt;a href="https://www.apache.org/"&gt;apache&lt;/a&gt; and &lt;a href="https://www.lighttpd.net/"&gt;lighttpd&lt;/a&gt; webservers, &lt;a href="https://www.php.net/"&gt;PHP&lt;/a&gt;, &lt;a href="https://www.mysql.com/"&gt;MySQL&lt;/a&gt;, &lt;a href="https://www.sqlite.org/index.html"&gt;SQLite&lt;/a&gt;, and, when I get to that stage of my programmming, &lt;a href="https://pimylifeup.com/raspberry-pi-postgresql/"&gt;Postgres&lt;/a&gt;. I could do all that on my MacBook, but it&amp;rsquo;s somehow more fun on the Pi.&lt;/p&gt;
&lt;p&gt;A recent addition is to run &lt;a href="https://pi-hole.net/"&gt;Pi-hole&lt;/a&gt; - a DNS sink to block advertisements on all devices on my network. It was a painless addition, and I enjoy looking at the stats as well as ads being blocked at the network level instead of depending on browser addons.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-02-at-7.17.04-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure that I&amp;rsquo;ll stick with Raspberry Pi. With the current shortage, it probably makes more sense to repurpose an old laptop, or buy a refurb mini pc which for a similar $400 price tag would be substantially more powerful. One con of that approach is you can spend several weekends getting everything running linux properly, whereas with the Pis that&amp;rsquo;s all done for you. Another is the idle power consumption would be much higher than my slow Pi 3Bs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-02-at-8.02.59-pm-1.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Towards MVVM</title><link>https://blog.iankulin.com/towards-mvvm/</link><pubDate>Sat, 03 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/towards-mvvm/</guid><description>&lt;p&gt;On one of the more mediocre &lt;a href="https://firesideswift.fireside.fm/96"&gt;episodes of Fireside Swift&lt;/a&gt;, McSwiftface and Zach talk about the &lt;a href="https://en.wikipedia.org/wiki/SOLID"&gt;SOLID principles&lt;/a&gt; of class design, although I don&amp;rsquo;t hold the principles as the article of religious fervour that many interviewers apparently do, they are a useful touchstone for considering class quality. OOP had been in swing (in a commercial way) for a few years by then - I was writing in Delphi and C++. The spaghetti code era was a long way behind us and the idea of separation of responsibilities was well established.&lt;/p&gt;
&lt;p&gt;I have been thinking about architecture a bit anyway - the introduction of Core Data into the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100Day&lt;/a&gt; apps I&amp;rsquo;m up to (day 63) means that there&amp;rsquo;s complicated looking code scattered around my views. In the &lt;a href="https://cs193p.sites.stanford.edu/"&gt;cs193p lectures&lt;/a&gt;, MVVM is right near the start, and I made &lt;a href="https://blog.iankulin.com/tags/mvvm/"&gt;some early forays&lt;/a&gt; into it, but so far no talk of architecture in 100Days (although I know it&amp;rsquo;s coming soon.&lt;/p&gt;
&lt;p&gt;I have certainly had the experience before of needing a layer between my apps and the database tech so they can be swapped out, and it&amp;rsquo;s basically a reflex to me to always wrap any commercial external code I&amp;rsquo;m introducing in any reasonable size program to abstract it a bit and make the (likely) task of having to replace it in the future a lot easier.&lt;/p&gt;
&lt;p&gt;Once again, YouTube serves me up a timely video.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/ehV2gp5uVhs?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;When I watch this, I can see why Paul Hudson might have left it for later in the course. There is a lot of complexity. It&amp;rsquo;s not simple code, it&amp;rsquo;s scalable code. This is good for real life apps, but it does not follow that it&amp;rsquo;s good for learning programming.&lt;/p&gt;</description></item><item><title>Deep Linking</title><link>https://blog.iankulin.com/deep-linking/</link><pubDate>Fri, 02 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/deep-linking/</guid><description>&lt;p&gt;I was listening to an &lt;a href="https://firesideswift.fireside.fm/100"&gt;old episode of Fireside Swift&lt;/a&gt; today discussing NFC tags. I have a bundle of this tags in a drawer here somewhere - I thought it would be cool to tap one as I came home to turn off the CCTV and some other home automation things. But it turns out my phone (an SE2) has the capability for this, but only inside an app - not just from anywhere, whereas the proper phones can just tap anytime, and if the NFC payload is set up correctly, follow a URL, including by &amp;ldquo;deep linking&amp;rdquo; into an app.&lt;/p&gt;
&lt;p&gt;Many years ago I worked on a project involving &lt;a href="https://www.trovan.com/en/RFID-FAQ/rfid-animals"&gt;injectable NFC animal tags&lt;/a&gt; in emus, and fancied have one in my arm. The standard&amp;rsquo;s moved on since then so probably good I didn&amp;rsquo;t. Even though it seems convenient to tag emu&amp;rsquo;s in their necks this is not a good idea as the tags move around in the fat layer and it can take some searching to find them with the scanner.&lt;/p&gt;
&lt;p&gt;The podcast made me wonder about deep linking, and because the YouTube algorithm reads minds now (and possibly because I&amp;rsquo;ve enjoyed other Swift Arcade videos). I got served up this video this afternoon.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/WmM4ryGcmSg?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;Unfortunately, that&amp;rsquo;s from UIKit days, for a more up to date look, &lt;a href="https://betterprogramming.pub/scalable-navigation-with-deep-links-in-swiftui-96cea1764994"&gt;this post&lt;/a&gt; by Riccardo Cipolleschi. Is a better bet. I&amp;rsquo;ll come back to it when I get a new phone ;-)&lt;/p&gt;</description></item><item><title>Project Based Learning</title><link>https://blog.iankulin.com/project-based-learning/</link><pubDate>Thu, 01 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-based-learning/</guid><description>&lt;p&gt;A couple of times in conversations on &lt;a href="https://firesideswift.fireside.fm/"&gt;Fireside Swift&lt;/a&gt; and &lt;a href="https://podcasts.apple.com/au/podcast/swift-over-coffee/id1435076502"&gt;Swift Over Coffee&lt;/a&gt; the presenters have talked about the danger of just doing more and more tutorials to learn programming, and the benefit, in contrast, of building your own real app. Although I am very much still benefiting from the 100DaysOfSwiftUI I have been seeing some of the upside of working on a real app in the last day and a half.&lt;/p&gt;
&lt;p&gt;From my search history, I&amp;rsquo;ve learned about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exporting files&lt;/li&gt;
&lt;li&gt;Regex&lt;/li&gt;
&lt;li&gt;Disabling autocorrect in a search box so the search doesn&amp;rsquo;t re-run incorrectly the second you click out of it&lt;/li&gt;
&lt;li&gt;You can&amp;rsquo;t use .onDelete without a ForEach&lt;/li&gt;
&lt;li&gt;There are good websites for converting CSV to JSON, and they mostly run in the browser which is good if you&amp;rsquo;re using sensitive data&lt;/li&gt;
&lt;li&gt;FileDocument&lt;/li&gt;
&lt;li&gt;URLs to the sandbox&lt;/li&gt;
&lt;li&gt;How UUIDs are really unique (spoiler, there&amp;rsquo;s only a tiny chance they&amp;rsquo;re not)&lt;/li&gt;
&lt;li&gt;The frustratingly large number of ways to format dates&lt;/li&gt;
&lt;li&gt;Sorting a FetchRequest in reverse order&lt;/li&gt;
&lt;li&gt;Change in initial values of pickers in iOS16&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s possible, but not as easy as I was imagining to add your own data to the Environment&lt;/li&gt;
&lt;li&gt;How to extract parts of strings&lt;/li&gt;
&lt;li&gt;The Array(Set(array)) method of eliminating duplicates - I actually knew, but couldn&amp;rsquo;t remember the details&lt;/li&gt;
&lt;li&gt;Using a toggle switch&lt;/li&gt;
&lt;li&gt;Jumping around in NavigationViews&lt;/li&gt;
&lt;li&gt;Making a bottom tab bar&lt;/li&gt;
&lt;li&gt;Dynamic filtering of a FetchRequest - revisited the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui"&gt;@twostraws method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Adding a search bar to a list&lt;/li&gt;
&lt;li&gt;Using url response to see why a fetch wasn&amp;rsquo;t working&lt;/li&gt;
&lt;li&gt;Refreshed the trick to convert from snake_case in JSON&lt;/li&gt;
&lt;li&gt;Copying files to a remote SSH&lt;/li&gt;
&lt;li&gt;Generating fake JSON data&lt;/li&gt;
&lt;li&gt;The range of an Int32&lt;/li&gt;
&lt;li&gt;Installing apache on the Pi&lt;/li&gt;
&lt;li&gt;Looked up what the mergeByPropertyObjectTrump rules actually are&lt;/li&gt;
&lt;li&gt;How to get the XCode preview back when you accidentally close it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think it&amp;rsquo;s significant that it&amp;rsquo;s a real app. When I&amp;rsquo;m just noodling around making a fun app, if something seems hard when you look into it, there&amp;rsquo;s the temptation to just do something else. In a real app you need to push through. For example I was bogged on having a search bar that live updated the list of results returned from Core Data as the user types. But this was a core part of the user experience required in this app, so I had to push through and learn the answers.&lt;/p&gt;</description></item><item><title>Regex to split a string with two different characters</title><link>https://blog.iankulin.com/regex-to-split-a-string-with-two-different-characters/</link><pubDate>Wed, 30 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/regex-to-split-a-string-with-two-different-characters/</guid><description>&lt;p&gt;I&amp;rsquo;m working on the behaviour tickets app, and wanted a visually functional version to share with stakeholders this week to get some feedback. As usual in this situation, I&amp;rsquo;m pressed for time so feeling the pressure to take some liberties with code quality that I&amp;rsquo;ll come back and fix one day.&lt;/p&gt;
&lt;p&gt;In a salient lesson of why that&amp;rsquo;s usually a bad idea, I&amp;rsquo;ve ended up googling to try and understand regex instead of writing code.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the problem I was trying to quickly solve. I&amp;rsquo;ve used a string for what should probably have been a struct. It looks like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;let myString = &amp;quot;Some behaviour (expectation)&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Its super easy to combine values together into a string, but substantially more difficult (and dangerous) to extract them out again. I want to get &lt;code&gt;&amp;quot;Some behaviour&amp;quot;&lt;/code&gt; and &lt;code&gt;&amp;quot;expectation&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is a String.split() function that takes a separator and returns an array of the strings split on that. That could work in this case, but I&amp;rsquo;d have to split a couple of times since I&amp;rsquo;m using two different separators. Swift 5.7 released in 2022 introduced a regex (regular expression) type, and this can be used as the argument for the split() method. Sounds perfect. Here&amp;rsquo;s the code that I ended up with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let myString = &amp;#34;Some behaviour (expectation)&amp;#34;
let regex = /\ \(|\)/

let splits = myString.split(separator: regex)

print(splits)
// [&amp;#34;Some behaviour&amp;#34;, &amp;#34;expectation&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I think it&amp;rsquo;s fair to say that regex is powerful, but not intuitive. There are many &lt;a href="https://regex101.com/"&gt;online tools&lt;/a&gt; to help with this. But let me step through building this expression to give you an idea of what&amp;rsquo;s going on.&lt;/p&gt;
&lt;p&gt;The first thing is that the expression is enclosed in two forward slashes. So if we just wanted to split on lower case &amp;lsquo;o&amp;rsquo; the expression would be &lt;code&gt;/o/&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let myString = &amp;#34;Some behaviour (expectation)&amp;#34;
let regex = /o/

let splits = myString.split(separator: regex)

print(splits)
//[&amp;#34;S&amp;#34;, &amp;#34;me behavi&amp;#34;, &amp;#34;ur (expectati&amp;#34;, &amp;#34;n)&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But we want to split on an opening bracket. You might think &lt;code&gt;/)/&lt;/code&gt; would work, but brackets are part of the regex syntax, so they have to be escaped. This is done with a back slash.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let myString = &amp;#34;Some behaviour (expectation)&amp;#34;
let regex = /\(/

let splits = myString.split(separator: regex)

print(splits)
[&amp;#34;Some behaviour &amp;#34;, &amp;#34;expectation)&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I don&amp;rsquo;t want that space at the end of &amp;ldquo;Some behaviour &amp;quot; so I&amp;rsquo;ll add that to the regex. Spaces are not allowed at the start of a regex, so that needs to be escaped too.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let myString = &amp;#34;Some behaviour (expectation)&amp;#34;
let regex = /\ \(/

let splits = myString.split(separator: regex)

print(splits)
// [&amp;#34;Some behaviour&amp;#34;, &amp;#34;expectation)&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To match the end bracket, we&amp;rsquo;ll need to add an OR to our expression. In regex, this is a | (pipe), and of course we need to escape the bracket again.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let myString = &amp;#34;Some behaviour (expectation)&amp;#34;
let regex = /\ \(|\)/

let splits = myString.split(separator: regex)

print(splits)
[&amp;#34;Some behaviour&amp;#34;, &amp;#34;expectation&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I played with this in an Xcode playground. I normally don&amp;rsquo;t use them because I love the iCloud sync that I get with the Playgrounds app to my iPad, but it seems like the app version is not on Swift 5.7 yet.&lt;/p&gt;</description></item><item><title>Copying a file via SSH</title><link>https://blog.iankulin.com/copying-a-file-via-ssh/</link><pubDate>Tue, 29 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/copying-a-file-via-ssh/</guid><description>&lt;p&gt;I have a Raspberry Pi on my home network that I purchased for some project that I can&amp;rsquo;t actually recall. It gets used for all sorts of completely unnecessary things such as playing with node.js or a private git server. To add to the list of things that I do on pi that could be more efficiently done on my MacBook I wanted to host my sample JSON from yesterday on it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d already downloaded the JSON to my laptop (although I could have skipped that step and used curl to download it directly to the pi) and just wanted to transfer it into the apache html directory.&lt;/p&gt;
&lt;p&gt;I feel like in 2022 I should just be able to SSH to the directory on the pi, then drag and drop the file from my mac, but that&amp;rsquo;s not possible (although it does usefully paste the filepath to the file which could be helpful). Of course there is a command for this, you just need to know it - &lt;code&gt;scp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My pi is on 192.168.100.13 and I log in as ian. The file I want to copy is called sample_students.json.&lt;/p&gt;
&lt;p&gt;In a terminal window, on your development machine:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;scp sample_students.json ian@192.168.100.13:/home/ian&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The first argument is the file to copy, and the second is the ssh accessible machine you are copying to with the path after the colon.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-26-at-7.57.13-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-26-at-7.56.32-am.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Mock Data</title><link>https://blog.iankulin.com/mock-data/</link><pubDate>Mon, 28 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/mock-data/</guid><description>&lt;p&gt;One of the things we need during app development is some data to play with. It would be unethical for me to use real student data to test my app, even if I wasn&amp;rsquo;t sharing screenshots of the development here, so I&amp;rsquo;ll need to build some mock data. The prospect of making 400 rows of data manually does not sound like a good use of time, so I started to think about generating it in Excel. I&amp;rsquo;d used an online &amp;ldquo;random address generator&amp;rdquo; for an earlier project, so I was contemplating pasting that sort of data into Excel workbooks and randomly selecting from it.&lt;/p&gt;
&lt;p&gt;It occurred to me someone probably already solved this problem, and a quick search confirmed there are hundreds of web based test data generating sites. They provide data in all sorts of formats as downloads or via APIs. Somewhat at random, I selected &lt;a href="https://www.mockaroo.com/"&gt;Mockaroo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.mockaroo.com/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-26-at-5.56.38-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In addition to letting you specify the fields, you control the type of data to go in them. They have a heap of options of specific formats such as ISBN&amp;rsquo;s, airport codes and so on. The whole thing took a couple of minutes to setup.&lt;/p&gt;
&lt;p&gt;As I was doing it, it occurred to me that a completely random date of birth didn&amp;rsquo;t make perfect sense - there really should be a connection between the year group of students and their date of birth. It&amp;rsquo;s completely immaterial for this project, so I didn&amp;rsquo;t bother with it, but in fact Mockaroo has ruby based scripting that allows you the flexibility to make one field somehow calculated from others so that would be possible&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-26-at-5.56.05-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It was a simple matter to download my data as JSON, but there&amp;rsquo;s also options for fetching it from a REST API.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-11-26-at-5.59.38-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-26-at-5.59.38-am.png" width="273" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll download and host this on my home Raspberry Pi server that I bought for a different project, but it&amp;rsquo;s possible just to pull it direct from their server.&lt;/p&gt;
&lt;p&gt;A small note of caution is that if you sign in to save your schema, it is saved, but it&amp;rsquo;s also public. That may not matter for your context, but in some situations it could be important. This could be from the data you provide as options, or even just the property names might provide intelligence useful to someone. As IT professionals we need to be mindful of the risks we&amp;rsquo;re taking with client or user information and to either eliminate or explain them.&lt;/p&gt;</description></item><item><title>Tickets on Myself</title><link>https://blog.iankulin.com/tickets-on-myself/</link><pubDate>Sun, 27 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/tickets-on-myself/</guid><description>&lt;p&gt;Way back on Day 47 I wrote a little habit tracking app. It was the challenge at the end of a JSON tutorial, so the persistence is done by writing the JSON to UserDefaults as a string. Basic as it is, it&amp;rsquo;s installed on my phone and I check it a couple of times a day, and haven&amp;rsquo;t missed a day of coding, or the weekly bin day since. It&amp;rsquo;s strangely motivating.&lt;/p&gt;
&lt;p&gt;I feel I&amp;rsquo;ve got enough Core Data skills now to write a small real app. This will be for teachers to record the reward tickets they issue for students. Two entities, Tickets and Students.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2792.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2793.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;School staff need to be able to search for a student, and add a ticket for them. The tickets are for behaviours in particular categories and sub-categories, and can have a text description.&lt;/p&gt;
&lt;p&gt;Eventually these tickets will have to be exportable somehow. Also it would be tedious to enter all the student data manually, so that needs to be importable. For the time being, the categories and sub-categories can just be a list that grows as they are entered, but eventually they should be importable as well. That data would be part of a bundle along with teacher names etc - again, importable somehow.&lt;/p&gt;
&lt;p&gt;All this importing could be from a simple webserver inside the school network for the moment - I learned how to download a JSON file in an earlier tutorial, and can setup apache or similar on my pi here at home for testing. That probably does not scale to a commercial solution, but it gets around having to build a real back end with authentication etc for now.&lt;/p&gt;
&lt;p&gt;Getting the data off is a similar problem. If I&amp;rsquo;m going to use the app for real at my school, it needs to integrate with an existing paper system, so exporting a PDF to my macBook would be lovely, or a text file to iCloud would be a good start.&lt;/p&gt;</description></item><item><title>FriendFace 61 Feedback</title><link>https://blog.iankulin.com/friendface-61-feedback/</link><pubDate>Sat, 26 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/friendface-61-feedback/</guid><description>&lt;p&gt;As usual after a challenge, I compare my efforts to Paul&amp;rsquo;s model solution. Just to quickly recap the app, it sucks up some data (Users who have multiple friends) and displays it. The change in this challenge was to convert it to add that data to a Core Data store so that if a future network error prevented accessing new data, it could still display the old.&lt;/p&gt;
&lt;h4 id="merge-policy"&gt;Merge Policy&lt;/h4&gt;
&lt;p&gt;The first difference is that Paul adds a merge policy. A Merge policy tells Core how to deal with any constraints defined in the data model. In this app, I&amp;rsquo;d defined the CachedUser.id as a constraint. The purpose of this is that under normal conditions the app would be picking up mostly the same data each time it started up. We don&amp;rsquo;t want scabs of duplicate data, so constraining users based on their unique id is smart.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-23-at-4.07.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://developer.apple.com/documentation/coredata/nsmergepolicy/merge_policies"&gt;merge policy&lt;/a&gt; tells Core Data how to deal with these conflicts. The policy Paul uses is mergeByPropertyObjectTrump - basically the memory version of an object (in this case the one just provided by the JSON) writes over a previously stored version.&lt;/p&gt;
&lt;p&gt;For testing, I had commented out my &lt;code&gt;try? moc.save()&lt;/code&gt; otherwise I might have been reminded to do this one. First round to Paul, again.&lt;/p&gt;
&lt;h4 id="data"&gt;Data&lt;/h4&gt;
&lt;p&gt;The data model and additions to the managed object source files were all pretty similar to mine, except that Paul left his id&amp;rsquo;s as UUID and it seemed to be happy with these being written to, so I need to look into that to understand what I was doing wrong,&lt;/p&gt;
&lt;p&gt;As far as copying the data over, Paul did it all in a function in the ContentView - I preferred my concept of having these as methods in CachedUser and CachedFriend. He mentions this, and it sounds like he agrees with me, but it&amp;rsquo;s a tiny project so no matter.&lt;/p&gt;
&lt;h4 id="mainactor"&gt;MainActor&lt;/h4&gt;
&lt;p&gt;When outlining the challenge, Paul went to a bit of trouble to explain the way to avoid a clash between updating the view (based on the data) and updating the data using &lt;code&gt;await MainActor.run&lt;/code&gt;. Without really knowing what I was doing, I put this call after the fetch for the JSON. In Paul&amp;rsquo;s version he has this inside the fetch when it&amp;rsquo;s complete. This actually makes more sense - with my version I might be building the Core Data copy before (or even while) it&amp;rsquo;s being fetched.&lt;/p&gt;</description></item><item><title>61 Done</title><link>https://blog.iankulin.com/61-done/</link><pubDate>Fri, 25 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/61-done/</guid><description>&lt;img src="https://blog.iankulin.com/images/green_tick.svg_.png" width="164" alt=""&gt;
&lt;p&gt;I think I&amp;rsquo;ve finally completed the minimum work for Day 61 of &lt;a href="https://www.hackingwithswift.com/100/swiftui/61"&gt;#100DaysOfSwiftUI&lt;/a&gt;. The task was to suck up some data in JSON, decode it. back it up into a Core Data graph then display the data from the Core Data.&lt;/p&gt;
&lt;p&gt;I got stuck on dealing with the one:many relationship and had to revisit that from a different source to get my head around it, after that it was straightforward. Other small problem I ran into was that I created the id in the CachedUser as a UUID from (newly formed) habit. Then when I went to copy it in from the JSON version, it wouldn&amp;rsquo;t let me. When I realised my mistake and changed it in the data model, I still could not figure out why it wasn&amp;rsquo;t working - but of course I hadn&amp;rsquo;t regenerated the code for the ManagedObject. I just had to change the property type in the already generated code from UUID to string and I was back in business.&lt;/p&gt;
&lt;p&gt;There are five ominous looking, indecipherable messages in the console log, which I assume are related to Core Data, and I think may have begun after I changed the data model and didn&amp;rsquo;t regenerate the NSManagedObjects. The app seems to work perfectly, but as usualy, this leaves me with an uneasy feeling of not understanding everything going on that I couldn&amp;rsquo;t tolerate in a production app.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m looking forwards to seeing Pauls solution for this one - I was out of my depth for a while, but think I&amp;rsquo;m on to it. I might even pause on the course and build a better add/delete/edit app with a one:many in Core Data just to ensure I&amp;rsquo;ve really got the basics of this topic under my belt.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/FriendFace/tree/main/FriendFace"&gt;Source&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Core Data basics – Part Three</title><link>https://blog.iankulin.com/core-data-basics-part-three/</link><pubDate>Thu, 24 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/core-data-basics-part-three/</guid><description>&lt;p&gt;If you&amp;rsquo;re just stumbling across this, perhaps have a look at Part 1 where I layout a simple master/detail app with the data held as arrays of structs, and Part 2 where I convert that into the simplest possible Core Data version. In this post, I&amp;rsquo;m going to add the mechanics for the one:many relationship - Each Garden can be associated with multiple Plants.&lt;/p&gt;
&lt;p&gt;I should also mention I figured out some of this with help from &lt;a href="https://www.youtube.com/watch?v=xgPlJXTfiNA"&gt;this video&lt;/a&gt; from &lt;a href="https://www.youtube.com/channel/UCxnCA5FBYRCFgIZWD0CKCVg/about"&gt;Jonathan Rasmusson&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-21-at-6.41.46-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;First step is another Paul hack. If you look in the &lt;code&gt;Garden+CoreDataProperties.swift&lt;/code&gt; file where the Garden properties are defined, you&amp;rsquo;ll see that the &lt;code&gt;plants&lt;/code&gt; variable has the type &lt;code&gt;?NSSet&lt;/code&gt; which is not straightforward to work with. We&amp;rsquo;d prefer it to be an array of Plant so we can easily turn it into a list in SwiftUI ways.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;extension Garden {

 @nonobjc public class func fetchRequest() -&amp;gt; NSFetchRequest&amp;lt;Garden&amp;gt; {
 return NSFetchRequest&amp;lt;Garden&amp;gt;(entityName: &amp;#34;Garden&amp;#34;)
 }

 @NSManaged public var address: String?
 @NSManaged public var id: UUID?
 @NSManaged public var name: String?
 @NSManaged public var plant: NSSet?
 
 var wrappedName: String {
 name ?? &amp;#34;Unknown&amp;#34;
 }
 var wrappedAddress: String {
 address ?? &amp;#34;Unknown&amp;#34;
 }
 
 var plantArray: [Plant] {
 let set = plant as? Set&amp;lt;Plant&amp;gt; ?? []
 return set.sorted {
 $0.wrappedName &amp;lt; $1.wrappedName
 }
 }

}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Using a computed variable (in this case plantArray), Paul likes to turn it into a Set of Plants, then convert that into an array by sorting it. We have to provide the predicate, but it&amp;rsquo;s a small price to pay to get the array back. I have concerns about this for a 100K array, but it&amp;rsquo;s no problem for this app.&lt;/p&gt;
&lt;p&gt;Back in our Garden DetailView, it&amp;rsquo;s now a simple matter to show the plants for a garden in a list under the other garden details.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct DetailView: View {
 var garden: Garden
 
 var body: some View {
 VStack {
 Text(garden.wrappedName)
 .font(.headline)
 Text(garden.wrappedAddress)
 List {
 ForEach(garden.plantArray, id: \.self) {plant in
 Text(plant.wrappedName)
 }
 }
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, in the button press to create our sample data, we need to create each of the plant instances then call a method on a garden to add them to that garden.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Button(&amp;#34;Sample Data&amp;#34;) {
 let garden1 = Garden(context: moc)
 garden1.id = UUID()
 garden1.name = &amp;#34;White Lodge&amp;#34;
 garden1.address = &amp;#34;72 Anderson Street \nRothwell QLD 4022&amp;#34;
 
 let plant1 = Plant(context: moc)
 plant1.name = &amp;#34;Rose&amp;#34;
 garden1.addToPlant(plant1)
 let plant2 = Plant(context: moc)
 plant2.name = &amp;#34;Palm Tree&amp;#34;
 plant2.garden? = garden1
 garden1.addToPlant(plant2)
 let plant3 = Plant(context: moc)
 plant3.name = &amp;#34;Lawn&amp;#34;
 plant3.garden? = garden1
 garden1.addToPlant(plant3)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The .addToPlant() accessor method is a Swift wrapper for some internal Objective C that was generated for us in the same file as the Garden properties. The bad name &amp;lsquo;AddToPlant&amp;rsquo; rather than &amp;lsquo;AddToPlants&amp;rsquo; is my bad. When I defined the relationship in Garden (in the data model) I should have called it &amp;lsquo;plants&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/core-data-basics-part-one/"&gt;Part 1&lt;/a&gt;, &lt;a href="https://blog.iankulin.com/core-data-basics-part-two/"&gt;Part 2&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Core Data basics - Part Two</title><link>https://blog.iankulin.com/core-data-basics-part-two/</link><pubDate>Wed, 23 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/core-data-basics-part-two/</guid><description>&lt;p&gt;Yesterday I roughed out a master/detail app with a list of gardens, and for each garden a detail screen including some plants. It used arrays of structs for the data. Today I&amp;rsquo;m going to convert that app to use Core Data, and explain my understanding of each step. This won&amp;rsquo;t be the entire app - I&amp;rsquo;m going to include the plants in my data structure, but not actually use them in this version. I&amp;rsquo;ll save that 1:many relationship stuff for another post.&lt;/p&gt;
&lt;p&gt;Once again I need to acknowledge Paul Hudson&amp;rsquo;s 100 Days of SwiftUI for most of my knowledge of Core Data - much of the code below is lifted directly from his examples. The explanations of what is going on, and any errors, are mine.&lt;/p&gt;
&lt;p&gt;Just to remind you, here&amp;rsquo;s what our working app should look like by the end of this post.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/simulator-screen-shot-iphone-14-pro-2022-11-19-at-19.40.30-1.png" width="433" alt=""&gt;
&lt;p&gt;The &amp;ldquo;Sample Data&amp;rdquo; button creates the gardens to display. The list above displays them.&lt;/p&gt;
&lt;p&gt;The first thing we need to do is to create the data model. This is the description of our data structures - &lt;em&gt;Garden&lt;/em&gt; and &lt;em&gt;Plant&lt;/em&gt; which in the previous version were structs. In this version they will become classes, but we don&amp;rsquo;t just write the classes, we define them in an XCode &lt;em&gt;data model&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-20-at-4.48.49-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s slightly different language. Instead of structs we have &lt;em&gt;Entities&lt;/em&gt; for Garden and Plant, and instead of properties they have &lt;em&gt;Attributes&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;While we&amp;rsquo;re editing the data model, there&amp;rsquo;s another couple of little jobs to do here. We define the relationship between Gardens and Plants (each Garden can have many Plants, each Plant only belongs to a single garden). We also need to turn Codegen to Manual/None. To see this setting, you need to View|Inspectors|Data Model. If it&amp;rsquo;s left on &amp;ldquo;Class Generation&amp;rdquo; XCode creates a secret file in the build folder that contains the class definitions for Garden and Plant. In this app, I want to change them so we need to have them as part of our project source, which is the next step.&lt;/p&gt;
&lt;p&gt;We are going to get XCode to explicitly generate the Managed Object code for our entities, and add it to our project so we can edit it - Editor | Create NSManagedObject. Select both entities and XCode will generate four files and add them to the project navigator. – Each entity has a file with an empty class definition, and a file with an extension containing the @NSManaged property definitions and the fetchRequest method.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-20-at-5.56.58-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Since the properties are (a slightly different to Swift version of) optionals, this file is a good place to make any computed properties to return safely unwrapped versions.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;extension Garden {

 @nonobjc public class func fetchRequest() -&amp;gt; NSFetchRequest&amp;lt;Garden&amp;gt; {
 return NSFetchRequest&amp;lt;Garden&amp;gt;(entityName: &amp;#34;Garden&amp;#34;)
 }

 @NSManaged public var address: String?
 @NSManaged public var id: UUID?
 @NSManaged public var name: String?
 @NSManaged public var plant: NSSet?
 
 var wrappedName: String {
 name ?? &amp;#34;Unknown&amp;#34;
 }
 var wrappedAddress: String {
 address ?? &amp;#34;Unknown&amp;#34;
 }

}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s our classes for the individual bits of data - analogous to the structs - taken care of. But what about the analog for the array? What&amp;rsquo;s our container?&lt;/p&gt;
&lt;p&gt;For this, we need an NSPersistentContainer. The way Paul does things, this is a property of a data controller obect. It links to the data model we defined before.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class DataController: ObservableObject {
 let container = NSPersistentContainer(name: &amp;#34;DataDemo&amp;#34;)

 init() {
 container.loadPersistentStores { _, error in
 if let error = error {
 print(&amp;#34;Core Data failed to load: \(error.localizedDescription)&amp;#34;)
 }
 }
 }

 deinit {
 }

}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the app definition, instantiate the dataController as an @StateObject, then insert its viewcontext into the Environment. The view context is what we’ll use elsewhere in the app to manipulate the data.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@main
struct DataDemoApp: App {
 @StateObject private var dataController = DataController()
 var body: some Scene {
 WindowGroup {
 ContentView()
 .environment(\.managedObjectContext, dataController.container.viewContext)
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the view where we want to access our data, extract the managed object context from the environment and express it as a property of the view. Also in the view, we need a property to access our collection of Gardens. This is another property, with the @FetchRequest property wrapper.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct ContentView: View {
 @Environment(\.managedObjectContext) var moc
 @FetchRequest(sortDescriptors: [
 SortDescriptor(\.name)
 ]) var gardens: FetchedResults&amp;lt;Garden&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To add data - new gardens, instantiate a new garden with the “context:” initialiser and pass in our managed object context – this is the link to all the persistence code – the data model and so on. When you’re ready for these to be committed to “disk” (so far they are in the collection, but only in memory) call the managed object context’s save() method.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Button(&amp;#34;Sample Data&amp;#34;) {
 let garden1 = Garden(context: moc)
 garden1.name = &amp;#34;White Lodge&amp;#34;
 garden1.address = &amp;#34;72 Anderson Street \nRothwell QLD 4022&amp;#34;
 let garden2 = Garden(context: moc)
 garden2.id = UUID()
 garden2.name = &amp;#34;Gordon Terrace&amp;#34;
 garden2.address = &amp;#34;95 Learmouth St\nTahara Vic 3301&amp;#34;
 
 try? moc.save()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that we are dealing with objects now – reference types – so we have to keep creating new objects to add them to the collection. This is contrasted with when I was loading up the sample data with the struct, I could keep recycling it and when I added it to the array each time it was value copied.&lt;/p&gt;
&lt;p&gt;To access our data, it’s identical to accessing any collection, except that I’ll use the unwrapped computed properties from earlier.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; @FetchRequest(sortDescriptors: [
 SortDescriptor(\.name)
 ]) var gardens: FetchedResults&amp;lt;Garden&amp;gt;
 
 var body: some View {
 NavigationView {
 VStack {
 List {
 ForEach(gardens, id: \.id) {garden in
 {
 HStack {
 Text(garden.wrappedName)
 Spacer()
 Text(garden.wrappedAddress)
 }
 }
 }
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s covered the &lt;em&gt;very&lt;/em&gt; basics. &lt;a href="https://github.com/IanKulin/DataDemo/releases/tag/v0.2"&gt;Here&amp;rsquo;s the source for this version&lt;/a&gt;. The next step will be to deal with the one-to-many relationship between the gardens and plants.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/core-data-basics-part-one/"&gt;Part 1&lt;/a&gt;, &lt;a href="https://blog.iankulin.com/core-data-basics-part-three/"&gt;Part 3&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Core Data basics - Part One</title><link>https://blog.iankulin.com/core-data-basics-part-one/</link><pubDate>Tue, 22 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/core-data-basics-part-one/</guid><description>&lt;p&gt;To help me get clear on the Core Data basics (&lt;a href="https://blog.iankulin.com/tough-day/"&gt;so I can master one of the #100Days challenges&lt;/a&gt;), I&amp;rsquo;ll write a simple master/detail app with arrays of structs, then convert it to Core Data listing out of the steps. Almost everything I know about Core Data, I learned from Paul Hudson&amp;rsquo;s &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of SwiftUI&lt;/a&gt; course - of which I&amp;rsquo;m up to day 61. So shout out to him. I highly recommend that course, and most of the code you&amp;rsquo;ll see in this post is either inspired by, or directly copied from 100 Days, except of course the errors - those are mine. This post - Part One - just describes the app and shows the struct/array version.&lt;/p&gt;
&lt;h3 id="app-overview"&gt;App Overview&lt;/h3&gt;
&lt;p&gt;The opening screen is a navigation view of the list of gardens, and a button. There’s a navigation link from each garden item to a detail view which shows the garden details, including a list of plants.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/simulator-screen-shot-iphone-14-pro-2022-11-19-at-19.40.30.png" width="138" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/simulator-screen-shot-iphone-14-pro-2022-11-19-at-19.40.40.png" width="138" alt=""&gt;
&lt;h3 id="struct-version"&gt;Struct version&lt;/h3&gt;
&lt;p&gt;First the data. Two structs, Plant, and Garden. Garden contains a list of plants. The gardens state variable lives in the main contentview and is an array of Gardens.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct Plant {
 var id = UUID()
 var name: String
}

struct Garden {
 var id = UUID()
 var name = &amp;#34;&amp;#34;
 var address = &amp;#34;&amp;#34;
 var plants: [Plant] = []
}

struct ContentView: View {
 @State private var gardens: [Garden] = []
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To create the sample data, a button creates the struct instances and adds them to the array.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Button(&amp;#34;Sample Data&amp;#34;) {
 var someGarden = Garden()
 someGarden.name = &amp;#34;White Lodge&amp;#34;
 someGarden.address = &amp;#34;72 Anderson Street \nRothwell QLD 4022&amp;#34;
 someGarden.plants.append(Plant(name:&amp;#34;Rose&amp;#34;))
 someGarden.plants.append(Plant(name:&amp;#34;Palm Tree&amp;#34;))
 someGarden.plants.append(Plant(name:&amp;#34;Lawn&amp;#34;))
 gardens.append(someGarden)
 someGarden.id = UUID()
 someGarden.name = &amp;#34;Gordon Terrace&amp;#34;
 someGarden.address = &amp;#34;95 Learmouth St\nTahara Vic 3301&amp;#34;
 someGarden.plants = []
 gardens.append(someGarden)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The main display of the data is a list in a navigation view. With a link to a detail page.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; gardens: [Garden] = []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(gardens, id: &lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.id) {garden &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationLink {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DetailView(garden: garden)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(garden.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(garden.address)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Sample Data&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; someGarden = Garden()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.name = &lt;span style="color:#e6db74"&gt;&amp;#34;White Lodge&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.address = &lt;span style="color:#e6db74"&gt;&amp;#34;72 Anderson Street &lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Rothwell QLD 4022&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.plants.append(Plant(name:&lt;span style="color:#e6db74"&gt;&amp;#34;Rose&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.plants.append(Plant(name:&lt;span style="color:#e6db74"&gt;&amp;#34;Palm Tree&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.plants.append(Plant(name:&lt;span style="color:#e6db74"&gt;&amp;#34;Lawn&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens.append(someGarden)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.name = &lt;span style="color:#e6db74"&gt;&amp;#34;Gordon Terrace&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.address = &lt;span style="color:#e6db74"&gt;&amp;#34;95 Learmouth St&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Tahara Vic 3301&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.plants =[]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens.append(someGarden)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.name = &lt;span style="color:#e6db74"&gt;&amp;#34;Powlett Cottage&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.address = &lt;span style="color:#e6db74"&gt;&amp;#34;11 Bayfield Street&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;White Beach 7184&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens.append(someGarden)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.name = &lt;span style="color:#e6db74"&gt;&amp;#34;Adams Garden&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden.address = &lt;span style="color:#e6db74"&gt;&amp;#34;71 Swanston St&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Kanya Vic 3381&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens.append(someGarden)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Data Demo&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The DetailView is passed the selected garden. It shows a couple of details from the garden, and a list of plants if there are any.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct DetailView: View {
 var garden: Garden
 
 var body: some View {
 VStack {
 Text(garden.name)
 .font(.headline)
 Text(garden.address)
 List {
 ForEach(garden.plants, id: \.id) {plant in
 Text(plant.name)
 }
 }
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So that&amp;rsquo;s our app. &lt;a href="https://github.com/IanKulin/DataDemo/releases/tag/v0.1"&gt;Source is on github&lt;/a&gt; here. Really, I could/should have done add/delete/edit but I just wanted something basic as a starting point for the Core Data stuff tomorrow.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/core-data-basics-part-two/"&gt;Part 2&lt;/a&gt;, &lt;a href="https://blog.iankulin.com/core-data-basics-part-three/"&gt;Part 3&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Something weird 'append</title><link>https://blog.iankulin.com/something-weird-about-append/</link><pubDate>Mon, 21 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/something-weird-about-append/</guid><description>&lt;p&gt;I&amp;rsquo;m noodling around making sure I understand how Core Data works. Thought I&amp;rsquo;d start with a master/detail app with an array of structs, then replicate it in a Core Data implementation. I&amp;rsquo;m using an array of this struct for my data:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct Garden {
 var id = UUID()
 var name = &amp;#34;&amp;#34;
 var address = &amp;#34;&amp;#34;
 var plants: [Plant] = []
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And I thought this code to load up some sample data was pretty sweet.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Button(&amp;#34;Sample Data&amp;#34;) {
 var someGarden = Garden()
 someGarden.name = &amp;#34;White Lodge&amp;#34;
 someGarden.address = &amp;#34;72 Anderson Street, \nRothwell QLD 4022&amp;#34;
 gardens.append(someGarden)
 someGarden.name = &amp;#34;Gordon Terrace&amp;#34;
 someGarden.address = &amp;#34;95 Learmouth Street\nTahara Vic 3301&amp;#34;
 gardens.append(someGarden)
 someGarden.name = &amp;#34;Powlett Cottage&amp;#34;
 someGarden.address = &amp;#34;11 Bayfield Street\nWhite Beach Tas 7184&amp;#34;
 gardens.append(someGarden)
 someGarden.name = &amp;#34;Adams Garden&amp;#34;
 someGarden.address = &amp;#34;71 Swanston Street\nKanya Vic 3381&amp;#34;
 gardens.append(someGarden)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But, no. This is what happens:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.00.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The reason I thought this was okay was that structs are value types in Swift. When I pass a struct in a function or method, it&amp;rsquo;s actually a copy at the other end. I could prove this by mutating the array copy and checking the original.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Button(&amp;#34;Sample Data&amp;#34;) {
 var someGarden = Garden()
 someGarden.name = &amp;#34;White Lodge&amp;#34;
 someGarden.address = &amp;#34;72 Anderson Street, \nRothwell QLD 4022&amp;#34;
 gardens.append(someGarden)
 gardens[0].name = &amp;#34;Mutated&amp;#34;
 print(&amp;#34;Array: \(gardens[0].name)&amp;#34;)
 print(&amp;#34;Local: \(someGarden.name)&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Prints:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Array: Mutated
Local: White Lodge
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So clearly I do understand structs/reference types correctly. They are different structs. Also, if structs were somehow reference types, they should have all had the data from the last garden, not the first one.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;d might be thinking (as I did), perhaps the error is in the view code. That&amp;rsquo;s easily checked. I forced the second one by creating a new struct, it works correctly and is in the correct position so clearly the view code is working.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.18.59-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The code mutating someGarden is getting run, so it&amp;rsquo;s not like the compiler&amp;rsquo;s eliminated those lines somehow. Also it is getting the number of entries correct - they are just somehow linking back to the first entry.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.31.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It seems impossible! This is always the way with good bugs.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m almost tempted to ask for help.&lt;/p&gt;
&lt;p&gt;Before one does that, it&amp;rsquo;s always a good idea to boil things down to the essence of the problem. Playgrounds is an excellent tool for such an endeavor. I just need the simplest version of an array of structs. Here&amp;rsquo;s what I came up with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import Foundation

struct Animal {
 var name: String = &amp;#34;&amp;#34;
}

var animals: [Animal] = []

var animal1 = Animal()
animal1.name = &amp;#34;Cat&amp;#34;
animals.append(animal1)
animal1.name = &amp;#34;Dog&amp;#34;
animals.append(animal1)

print(animals)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Produces the output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Page_Contents.Animal(name: &amp;#34;Cat&amp;#34;), Page_Contents.Animal(name: &amp;#34;Dog&amp;#34;)]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Grrr. It turns out Swift works perfectly.&lt;/p&gt;
&lt;p&gt;In the screenshot above, where I&amp;rsquo;ve broken on line 52, I couldn&amp;rsquo;t actually see how to inspect the gardens array. Perhaps I&amp;rsquo;d be better with the classic print() debugging instead.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[DataDemo.Garden(id: 0725A8FC-D410-4A2C-B925-C34340F25B05, name: &amp;#34;White Lodge&amp;#34;, address: &amp;#34;72 Anderson Street, \nRothwell QLD 4022&amp;#34;, plants: []), DataDemo.Garden(id: CF30FA66-A2F2-44FB-A6E0-A22E93492578, name: &amp;#34;Gordon Terrace&amp;#34;, address: &amp;#34;95 Learmouth Street\nTahara Vic 3301&amp;#34;, plants: []), DataDemo.Garden(id: 0725A8FC-D410-4A2C-B925-C34340F25B05, name: &amp;#34;Powlett Cottage&amp;#34;, address: &amp;#34;11 Bayfield Street\nWhite Beach Tas 7184&amp;#34;, plants: []), DataDemo.Garden(id: 0725A8FC-D410-4A2C-B925-C34340F25B05, name: &amp;#34;Adams Garden&amp;#34;, address: &amp;#34;71 Swanston Street\nKanya Vic 3381&amp;#34;, plants: [])]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Okay, so the bug I&amp;rsquo;ve been trying to fix, is not the bug at all. The array is working exactly as intended, it&amp;rsquo;s my view that&amp;rsquo;s the issue somehow.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.18.59-pm-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s not just a general problem with the view - it&amp;rsquo;s a problem with the view that goes away if I create a new Garden struct instead of recycling one&amp;hellip; Simultaneously I have these two thoughts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When I create a new struct, it gets a fresh UUID&lt;/li&gt;
&lt;li&gt;What was that console spam message I&amp;rsquo;ve been ignoring? Oh yeah, it was &lt;code&gt;ForEach, UUID, HStack&amp;gt;&amp;gt;: the ID 0725A8FC-D410-4A2C-B925-C34340F25B05 occurs multiple times within the collection, this will give undefined results!&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can guess the rest, my ForEach is using the UUID:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;List {
 ForEach(gardens, id: \.id) {garden in
 HStack {
 Text(garden.name)
 Spacer()
 Text(garden.address)
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If I fix the ids:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-8.16.17-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s how you waste a hour fixing a bug the framework warned you about in the first five minutes.&lt;/p&gt;</description></item><item><title>Tough Day</title><link>https://blog.iankulin.com/tough-day/</link><pubDate>Sun, 20 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/tough-day/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/bad-day-at-sea-uspaceken.jpg" alt=""&gt;
&lt;em&gt;&lt;a href="https://www.reddit.com/r/Art/comments/7i7crd/bad_day_at_sea_ii_30_x_40_oil/"&gt;Bad Day at Sea - reddit u/SpaceKen&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Day 61 of &lt;a href="https://www.hackingwithswift.com/100/swiftui/61"&gt;#100DaysOfSwiftUI&lt;/a&gt; is a tough day. It&amp;rsquo;s the first real big test of Core Data understanding, and I&amp;rsquo;m finding I didn&amp;rsquo;t actually understand how the code for Core Data works. For the first time, I&amp;rsquo;m thinking of going back and redoing the days leading up to it.&lt;/p&gt;
&lt;p&gt;To try and get it straight in my mind, here&amp;rsquo;s how I think Core Data works:&lt;/p&gt;
&lt;p&gt;1. it&amp;rsquo;s a way of persisting instances of objects - not a relational database (even though that&amp;rsquo;s what&amp;rsquo;s lying underneath it). I&amp;rsquo;m sure this is part of what is obstructing my thinking. Relational databases were my bread and butter for many years.&lt;/p&gt;
&lt;p&gt;2. Core Data won&amp;rsquo;t just persist any old objects, you need to define these objects in the Data Model. They will be &amp;ldquo;NSManagedObjects&amp;rdquo;&lt;/p&gt;
&lt;p&gt;3. In the Data Model you can specify the &amp;ldquo;CodeGen&amp;rdquo;. If it&amp;rsquo;s left on the default &amp;ldquo;Class Definition&amp;rdquo; a hidden source file you can&amp;rsquo;t access is generated deep in the build file so the objects exist - and therefore you can use them.&lt;/p&gt;
&lt;p&gt;4. If you set CodeGen to &amp;ldquo;Manual/None&amp;rdquo;, it is now your responsibility to create that file. You usually select this, then tell XCode to generate them for you and add them to your project navigator by going in to Editor | Create NSManagedObject subclass. This then allows you to edit those files.&lt;/p&gt;
&lt;p&gt;5. To do anything these objects, you need a managaged object context. I&amp;rsquo;m not real clear on what this is. You create it in the App file, from a data controller that contains an NSPersistantObject. Then you save it to some super global called the environment.&lt;/p&gt;
&lt;p&gt;6. To add objects to the collection, you instantiate them using an initialiser that accepts the managed object context as an argument. Then to really persist them (as opposed to just the memory version) you call save() on the object context.&lt;/p&gt;
&lt;p&gt;7. To access the objects, you build a fetch request access a collection of them.&lt;/p&gt;
&lt;p&gt;8. It gets tricky when there are relationships between the objects.&lt;/p&gt;
&lt;p&gt;My plan is to work through my own example of arrays of items vs a Core Data version of the same thing to ensure I&amp;rsquo;ve got it all straight in my head before coming back to the Day 61 challenge. I don&amp;rsquo;t feel too bad about this - Paul makes a point that its a tough challenge, and I don&amp;rsquo;t want to just &amp;ldquo;get it working&amp;rdquo; and move on with that uneasy feeling I might have code with errors I just haven&amp;rsquo;t discovered yet.&lt;/p&gt;</description></item><item><title>iOS 16 Developer Mode</title><link>https://blog.iankulin.com/ios-16-developer-mode/</link><pubDate>Sat, 19 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-16-developer-mode/</guid><description>&lt;p&gt;I updated my iPhone to iOS16 this morning, and tonight when I went to run one of my apps, it complained that it needed Developer mode. This is a new, probably wise, way to avoid dodgy apps being loaded on a phone. I don&amp;rsquo;t know exactly how you&amp;rsquo;d do that, but then I&amp;rsquo;m not a black hatted cyber terrorist.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_3319.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I had to google the setting, it&amp;rsquo;s in &amp;ldquo;Privacy and Security&amp;rdquo; down the bottom, and requires a reboot. When you open the phone there&amp;rsquo;s another dialog and you need to reauth.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_3320.png" width="577" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_3321.png" width="577" alt=""&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_3322.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>iOS Dev Twitter</title><link>https://blog.iankulin.com/ios-dev-twitter/</link><pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-dev-twitter/</guid><description>&lt;p&gt;One of &lt;a href="https://www.youtube.com/c/SeanAllen/videos"&gt;Sean Allen&amp;rsquo;s&lt;/a&gt; many pieces of excellent advice was to follow a few Swift/iOS dev people on Twitter. I took that advice, and it is now a source of joy to flick through every couple of days and get a feel for what&amp;rsquo;s happening, and to discover new things. It&amp;rsquo;s how I learned &lt;a href="https://iosdevweekly.com/"&gt;iOS Dev Weekly&lt;/a&gt; existed, and discovered &lt;a href="https://designcode.io/instructor/meng"&gt;Meng To&lt;/a&gt;, and put faces/ideas to Swift and iOS people that&amp;rsquo;s I&amp;rsquo;d heard mentioned or interviewed in podcasts such as &lt;a href="https://ericasadun.com/"&gt;Erica Sadun&lt;/a&gt; and &lt;a href="https://sarunw.com/"&gt;Sarun W.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Whether it&amp;rsquo;s illusory or not, it does feel like a community, and a pleasant one. I imagine there are thousands of little corners of Twitter like this, where people feel a sense of belonging based on some shared interest or ideas. No sane person would argue that Twitter has been 100% beneficial to the world, but it has enabled groups like iOS Dev Twitter to form and thrive, and it would be sad to lose that.&lt;/p&gt;</description></item><item><title>Clean Build Folder</title><link>https://blog.iankulin.com/clean-build-folder/</link><pubDate>Thu, 17 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/clean-build-folder/</guid><description>&lt;p&gt;Working on adding Core Data to the FriendFace app, and burnt up 20 minutes figuring out a bug. To set the scene, all I&amp;rsquo;ve changed in the app is to add a couple of core data entities. The plan is that when the JSON is fetched, and decoded into the objects, a copy of the graph will be persisted.&lt;/p&gt;
&lt;p&gt;Problem One was that I was getting a build errors saying the core data classes had been re-declared, and others saying that my class name was ambiguous. Since XCode had generated this code when I&amp;rsquo;d told it to &amp;ldquo;Create NSManagedObject subclass&amp;rdquo;. This is what you do when you want to be able to edit the NSManagedObject for example to created computed properties to unwrap the real properties. If you don&amp;rsquo;t need that flexibility, you just leave the default setting in the entity for XCode to create internally.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-11-15-at-7.52.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-15-at-7.52.05-pm.png" width="885" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Eventually I figured out my error - when I&amp;rsquo;d turned off the CodeGen in the data model inspector, I&amp;rsquo;d only done it for one of the two entities. Now the CodeGen version was duplicating the code in the &amp;ldquo;manually&amp;rdquo; generated version. Solved!&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;d just need to change the CodeGen setting to manual, and I should be able to build it - but no, same error. I deleted the generated files out of the Project Navigator and tried the build again. Now I was getting a different error, about a class not found. The weird thing was, the file the error was coming from didn&amp;rsquo;t seem to exist in my project directory. It was one of the data classes that I thought I deleted. By clicking around XCode, I found I could right click and ask to see the file in finder, only to discover it was deep in the XCode subfolders in the Library.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-15-at-8.00.49-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;My first instinct was to delete it, but then I remembered seeing &amp;ldquo;Clean Build Folder&amp;rdquo; in the Project menu. I ran that, then did a fresh build (which took longer than usual) and lo and behold, all good.&lt;/p&gt;
&lt;p&gt;So the answer to the question you&amp;rsquo;ve never asked &amp;ldquo;what happens when I leave my Core Data entities set to CodeGen?&amp;rdquo; is that it creates the swift for the NSManagedObject deep in the build folders - and it doesn&amp;rsquo;t delete it if you change the CodeGen setting to Manual.&lt;/p&gt;</description></item><item><title>Console spam - No wall clock alignment</title><link>https://blog.iankulin.com/console-spam-no-wall-clock-alignment/</link><pubDate>Wed, 16 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/console-spam-no-wall-clock-alignment/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-12-at-8.02.28-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;When I was working on the Day 60 app, I noticed I kept getting a message in the console &amp;ldquo;&lt;code&gt;No wall clock alignment provided at SwiftUI/ResolvableStringAttribute.swift:86&lt;/code&gt;&amp;rdquo; every time I went into the detail view. Via elimination by commenting bits out, I&amp;rsquo;ve narrowed it down to a date formatting call. Here is the code to reproduce it in Xcode Version 14.0.1, Swift 5.7.0.127.4&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct ContentView: View {
 var body: some View {
 Text(&amp;#34;Date: \(Date(), style: .date)&amp;#34;)
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s to do with the style - if I change it to .time or .relative the message does not appear.&lt;/p&gt;
&lt;p&gt;I assume an Apple programmer has checked for something on line 86 of ResolvableStringAttribute.swift and unexpectedly found it missing. If I search for that file on my system, it doesn&amp;rsquo;t appear to be here, so I guess the SwiftUI source is not part of the XCode installation.&lt;/p&gt;
&lt;p&gt;Googling the error only produces a single result - a &lt;a href="https://www.reddit.com/r/iOSProgramming/comments/vm90cf/no_wall_clock_alignment_provided_at/"&gt;reddit&lt;/a&gt; user asking for help on the message and being told not to worry about the noise coming from Apple frameworks if everything&amp;rsquo;s working okay. The question is from five months ago and has a different line number - so presumably from an earlier version of SwiftUI.&lt;/p&gt;
&lt;p&gt;Since it can be reproduced with such a tiny code snippet, and it occurs in every simulator I tried it on, it may be easy to reproduce, and therefore fix. So without really knowing the exact guidelines, but with encouragement from &lt;a href="https://developer.apple.com/bug-reporting/"&gt;Apple&lt;/a&gt;, I filed my first Radar.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-12-at-8.18.29-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>FriendFace Feedback</title><link>https://blog.iankulin.com/friendface-feedback/</link><pubDate>Tue, 15 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/friendface-feedback/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-12-at-4.38.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;After each app, I use my HackingWithSwift+ membership to view Paul&amp;rsquo;s version of the app as a way to judge my performance. Yesterday&amp;rsquo;s app was &amp;ldquo;&lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/5/3/challenge"&gt;FriendFace&lt;/a&gt;&amp;rdquo; - download some JSON of a number of people (including their friends) and display it.&lt;/p&gt;
&lt;h4 id="uuid"&gt;UUID&lt;/h4&gt;
&lt;p&gt;In my struct, I&amp;rsquo;d just specified the User.id as a string, Paul uses UUID - this makes no difference to the app as it stands, but is much better if we ever needed to add users.&lt;/p&gt;
&lt;h4 id="example"&gt;Example&lt;/h4&gt;
&lt;p&gt;For my details preview, I created a fake user in the preview. Paul does something slightly nicer - with a static let example = User(&amp;hellip;) as a property of the User struct. Again - not a biggy for this app, but handy if you need to use it for other previews.&lt;/p&gt;
&lt;h4 id="fetchusers"&gt;fetchUsers()&lt;/h4&gt;
&lt;p&gt;I had a test in the .task() to prevent the double loading of data, Paul puts his here inside a guard. I think mine is simpler to read and understand, but Paul&amp;rsquo;s would be better if there was a chance we might call fetchUsers() from other places in the app. If that&amp;rsquo;s likely, this would be the third time out of three differences where Paul is anticipating a bigger app - it might just be a reflex for such an accomplished developer. However, I&amp;rsquo;m going to award this one to me.&lt;/p&gt;
&lt;h4 id="docatch"&gt;do/catch&lt;/h4&gt;
&lt;p&gt;Paul&amp;rsquo;s JSON fetching and decoding is in a single do/catch block - as mine was in my first drafting, but then it didn&amp;rsquo;t work correctly so I changed it. I think we can assume Paul&amp;rsquo;s works - he&amp;rsquo;s a quality coder, and this code has had a lot of eyes on it by now. So this is something I need to understand better.&lt;/p&gt;
&lt;p&gt;Apart from the two items above, the rest of fetchUsers was essentially the same.&lt;/p&gt;
&lt;h4 id="list"&gt;List&lt;/h4&gt;
&lt;p&gt;Paul used little red or green circles to indicate the active status of users in his list - much nicer than my version of having the text. Apart from that, it was a NavigationView iwht a List with a NavigationLink to the detail view, so same same.&lt;/p&gt;
&lt;h4 id="userdetails"&gt;UserDetails&lt;/h4&gt;
&lt;p&gt;Instead of a form, Paul uses a List with a .listStyle of .grouped which probably follows the HIG better.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/BmPPhnIxoHM?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>FriendFace</title><link>https://blog.iankulin.com/friendface/</link><pubDate>Mon, 14 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/friendface/</guid><description>&lt;p&gt;The &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/5/3/challenge"&gt;Day 60 Milestone&lt;/a&gt; is a demo app that vacuums up some JSON and displays it in a list in a NavigationView that links to a details page. Nothing super strenuous, the steps were something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the JSON and have a look at the structure. Firefox has a simple JSON viewer built in, so it was straightforward to see this is an array of users, which along with some (mostly string) properties contains an array of tag strings, and another array of friends.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-12-at-3.23.28-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Build the structs for these.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Foundation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;User&lt;/span&gt;: Codable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; isActive: Bool
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; name: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; age: Int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; company: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; email: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; address: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; about: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; registered: Date
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; tags: [String]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; friends: [Friend]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Friend&lt;/span&gt;: Codable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; name: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The date you can see in the JSON is in the ISO-8601 format - and @twostraws gives the hint about using the dateDecodingStrategy for it.&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Fetch the data. I made this a .task attached to the list, I didn&amp;rsquo;t notice it loading multiple times, but Paul did caution about this, so the code checks if the array is empty before calling fetching the JSON.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.task {
 if users.isEmpty {
 await fetchUsers()
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;ol start="4"&gt;
&lt;li&gt;Process the data into our structs. Since they are codable, this is a bit of a joy. I&amp;rsquo;d made an error and the JSON wasn&amp;rsquo;t loading, but I could not see what it was. I fixed this by nesting my do/catch blocks. It seems a bit unwieldy - I feel like I should be able to do several risky steps and have the catch catch all the errors, but it didn&amp;rsquo;t seem to be. This code works fine, but I feel it could be improved.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetchUsers&lt;/span&gt;() async {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;guard&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; url = URL(string: &lt;span style="color:#e6db74"&gt;&amp;#34;https://www.hackingwithswift.com/samples/friendface.json&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Invalid URL&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decoder = JSONDecoder()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; decoder.dateDecodingStrategy = .iso8601
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; (data, &lt;span style="color:#66d9ef"&gt;_&lt;/span&gt;) = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; await URLSession.shared.data(from: url)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decodedUsers = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; decoder.decode([User].&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, from: data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; users = decodedUsers
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(error)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(error)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note the square brackets for [User] - we&amp;rsquo;re decoding an array of users.&lt;/p&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Display it in the list, with an NavigationLink to the UserDetails view.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; users = [User]()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List(users, id: &lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.id) { user &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationLink(destination: UserDetail(user: user)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack(alignment: .leading) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(user.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.headline)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(user.isActive ? &lt;span style="color:#e6db74"&gt;&amp;#34;Active&amp;#34;&lt;/span&gt; : &lt;span style="color:#e6db74"&gt;&amp;#34;Not active&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .task {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; users.isEmpty {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; await fetchUsers()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationBarTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;FriendFace&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="5"&gt;
&lt;li&gt;Show the user details. This is where I started going off script. The user is passed into the detail view. I used a Form because the layout looks a bit nice, and the Friends is a little list.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The script departure was wanting to have a profile pic. As described yesterday, I used an AsyncImage for this. I would have liked to have cached the image so it doesn&amp;rsquo;t re-fetch when you go out of a user and back in (this would have solved the problem of the random image I described yesterday as well) and fiddled around with trying to save a .snapshot of the AsyncImage view - but then decided I should be moving on instead of cracking that particular procrastination nut, especially because of this parting advice from Paul.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Tip:&lt;/strong&gt; As always, the best way to solve this challenge is to keep it simple – write as little code as you can to solve the challenge, and for you to feel comfortable that it works well.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserDetail&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; user: User
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Form {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Section {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(user.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.headline)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .frame(maxWidth: .infinity, alignment: .center)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AsyncImage(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url: URL(string: &lt;span style="color:#e6db74"&gt;&amp;#34;https://randomuser.me/api/portraits/men/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;nameHash&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.jpg&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scale: &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ) { image &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; image
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .resizable()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .scaledToFit()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } placeholder: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ProgressView()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Section {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Age: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;user.age&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Company: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;user.company&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;email: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;user.email&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Address: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;user.address&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Registered: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;user.registered, style: .date&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Section(header: Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Friends&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List(user.friends, id: &lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.id) { friend &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(friend.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; nameHash: Int {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user.name.utf8.reduce(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) { $0 &lt;span style="color:#f92672"&gt;+&lt;/span&gt; Int($1) } &lt;span style="color:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Profile Photo Rabbit Hole</title><link>https://blog.iankulin.com/profile-photo-rabbit-hole/</link><pubDate>Sun, 13 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/profile-photo-rabbit-hole/</guid><description>&lt;p&gt;I&amp;rsquo;m on &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/5/3/challenge"&gt;day 60 of #100Days&lt;/a&gt;, and have just wasted most of an evening&amp;rsquo;s coding time going down a rabit hole I didn&amp;rsquo;t need to. The app for this challenge is called &amp;ldquo;FriendFace&amp;rdquo; and is pretty straightforward: download a heap of JSON which is an array of users. Show it in a list that can be clicked through to see the details of that user.&lt;/p&gt;
&lt;p&gt;I did that, and instead of moving onto the next project, decided I&amp;rsquo;d like to show a profile picture of each person. There&amp;rsquo;s no data for that, so I&amp;rsquo;ll use a fake photo. I use the Stable Diffusion AI for many of the pictures in this blog, so I assumed there would be an API for grabbing a fake profile pic from somewhere. It turns out there are several, but they are paid services.&lt;/p&gt;
&lt;p&gt;Never mind, I know &lt;a href="https://unsplash.com/"&gt;unsplash&lt;/a&gt; is a great source of free images, and they definitely have a portrait category. I&amp;rsquo;ll get them from there. Instead of using a REST api, I just wanted to grab them from a URL. After a bit of googling around, it turns out you can just use the url &lt;a href="https://source.unsplash.com/collection/9948714?372"&gt;https://source.unsplash.com/collection/9948714?372&lt;/a&gt; and change the number after the question mark to get a different portrait.&lt;/p&gt;
&lt;p&gt;Now all I need is a way to generate a number 1-1000 such that it&amp;rsquo;s always the same for each individual user. That&amp;rsquo;s basically a hash, and Swift has a hashValue property on it&amp;rsquo;s string. I tried user.name.hashValue % 1_000, but kept getting different pictures. Pulled up a playground and tried some print() debugging, and the hashValue was different each run. It would be the same if I hashed the same string twice in a row in code, but not between runs. A few googles later, I&amp;rsquo;ve learned this is deliberate.&lt;/p&gt;
&lt;p&gt;So I need to write my own hash. This is not cryptography - I can just sum all the ascii values of the characters in the string and modulo them. I&amp;rsquo;m a bit hazy on how to get every character in a Swift string because of the unicode thing, but rmaddy has &lt;a href="https://stackoverflow.com/questions/51606011/4-bit-hash-from-string-in-swift"&gt;this answer&lt;/a&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;extension String {
 var fourBitHash: Int {
 return self.utf8.reduce(0) { $0 + Int($1) } % 16
 }
}

let colorIndex = &amp;#34;John R Smith&amp;#34;.fourBitHash
print(colorIndex)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Perfect. I adapt this into my code as:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func simpleHash(_ string: String) -&amp;gt; Int {
 string.utf8.reduce(0) { $0 + Int($1) } % 1000
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And we&amp;rsquo;re in business, but hang on, I&amp;rsquo;m still getting a different picture by repeatedly going into the view detail for the same user. I try pasting in the url a few times, and sure enough, unsplash are serving up random portraits for the same URL&amp;hellip;.&lt;/p&gt;
&lt;p&gt;Okay, so I need a different source for pics. More googling, and I discover &lt;a href="https://randomuser.me/"&gt;RandomUser.me&lt;/a&gt; They only have 100 profiles for men, and another 100 for women - but this app is only for me, and I&amp;rsquo;ll probably get bored after I&amp;rsquo;ve clicked on three or four so that will be fine. I throw that into my AsyncImage and we&amp;rsquo;re in business.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AsyncImage(
 url: URL(string: &amp;#34;https://randomuser.me/api/portraits/women/\(simpleHash(user.name)).jpg&amp;#34;),
 scale: 3
) { image in image
 .resizable()
 .scaledToFit()
} placeholder: {
 ProgressView()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;d prefer to show the correct gender (I&amp;rsquo;m not being deliberately binary, I&amp;rsquo;d just like to double the number of photos) , and there is no gender field in my data. So, I guess I&amp;rsquo;ll need an API that guesses gender based on a name input based on a giant lookup table or an AI model.&lt;/p&gt;
&lt;p&gt;More &lt;a href="https://stackoverflow.com/questions/1685559/find-the-gender-from-a-name"&gt;googling&lt;/a&gt;, and I find &lt;a href="https://stackoverflow.com/users/1608667/stromgren"&gt;stomgren&amp;rsquo;s&lt;/a&gt; genderize api. An input of:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[https://api.genderize.io/?name=ian](https://api.genderize.io/?name=ian)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;returns:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
 &amp;#34;count&amp;#34;: 306685,
 &amp;#34;gender&amp;#34;: &amp;#34;male&amp;#34;,
 &amp;#34;name&amp;#34;: &amp;#34;ian&amp;#34;,
 &amp;#34;probability&amp;#34;: 1
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://api.genderize.io/?name=kim&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
 &amp;#34;count&amp;#34;: 83361,
 &amp;#34;gender&amp;#34;: &amp;#34;female&amp;#34;,
 &amp;#34;name&amp;#34;: &amp;#34;kim&amp;#34;,
 &amp;#34;probability&amp;#34;: 0.7
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Perfect. So, I can just add this to my code to pull up the picture, or, you know what, I&amp;rsquo;m procrastinating and it&amp;rsquo;s time for bed.&lt;/p&gt;</description></item><item><title>git stash</title><link>https://blog.iankulin.com/git-stash/</link><pubDate>Sat, 12 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-stash/</guid><description>&lt;p&gt;When I was writing the blog post for the last project, I needed the &amp;ldquo;before&amp;rdquo; code to paste into the post. I&amp;rsquo;d committed that code, so a quick way to go back without losing my changes. I hadn&amp;rsquo;t committed the new code, so there is a super easy way to accomplish this.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git stash
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This grabs the code since the last commit and stashes it away, reverting the directory to the last committed version. I was able to copy the code I needed to the blog post, then to go back to my changes:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git stash pop
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-06-at-3.31.51-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It does get more complex than this - it&amp;rsquo;s git of course. It&amp;rsquo;s possible to stash multiple changes then pop them back, it&amp;rsquo;s also possible name them to selectively pop them, and to view the diffs. They are like little uncommitted branches - which can also be turned into branches. There&amp;rsquo;s a &lt;a href="https://www.atlassian.com/git/tutorials/saving-changes/git-stash"&gt;good outline here&lt;/a&gt; from Atlassian.&lt;/p&gt;</description></item><item><title>Project 12 Feedback</title><link>https://blog.iankulin.com/project-12-feedback/</link><pubDate>Fri, 11 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-12-feedback/</guid><description>&lt;p&gt;As usual, I watch Paul&amp;rsquo;s solution video, and compare it to mine.&lt;/p&gt;
&lt;h4 id="task-1"&gt;Task 1&lt;/h4&gt;
&lt;p&gt;This was passing in the predicate as a String. I passed the whole thing, but as I figured out along the way, Paul meant just the operator word. He also added some buttons to test it better, which I didn&amp;rsquo;t think of till Task 3 - it would have saved me some simulator runs.&lt;/p&gt;
&lt;h4 id="task-2"&gt;Task 2&lt;/h4&gt;
&lt;p&gt;This was changing to enums. As I mentioned, I am a fan. Meanwhile, my solution was exactly the same as Paul&amp;rsquo;s, down to where he put the enum.&lt;/p&gt;
&lt;h4 id="task-3"&gt;Task 3&lt;/h4&gt;
&lt;p&gt;Another FilteredList argument - this time the sort descriptors. I did get a bit bogged down in this. I followed the build error and was trying to use NSSortDescriptors and tying myself up in knots to get it to work. When I reread the task, I saw Paul&amp;rsquo;s hint to use SortDescriptor&lt;Singer&gt; then it all came together - and we had the same solution.&lt;/p&gt;</description></item><item><title>Project 12 Challenges</title><link>https://blog.iankulin.com/project-12-challenges/</link><pubDate>Thu, 10 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-12-challenges/</guid><description>&lt;p&gt;Project 12 was a series of code tutorials around developing CoreData concepts rather than a real app, but the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/core-data-wrap-up"&gt;challenges&lt;/a&gt; are based on a very small app that uses a subview to allow dynamic (ie changeable at runtime) filtering of a list of data. The reason this would be tricky is that the @FetchRequest is a property of a view - and therefore mutable. The trick is to have a subview to build that part of the view, and to pass parameters into it which build the fetchrequest using an underscore.&lt;/p&gt;
&lt;p&gt;The data is three recording artists - Taylor Swift, Ed Sheeran and Adel. The ContentView is a list of their names, with some buttons to filter it. Here&amp;rsquo;s the content view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Environment(&lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.managedObjectContext) &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; moc
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lastNameFilter = &lt;span style="color:#e6db74"&gt;&amp;#34;A&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// list of matching singers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FilteredList(filter: lastNameFilter)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Add Examples&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; taylor = Singer(context: moc)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; taylor.firstName = &lt;span style="color:#e6db74"&gt;&amp;#34;Taylor&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; taylor.lastName = &lt;span style="color:#e6db74"&gt;&amp;#34;Swift&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; ed = Singer(context: moc)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ed.firstName = &lt;span style="color:#e6db74"&gt;&amp;#34;Ed&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ed.lastName = &lt;span style="color:#e6db74"&gt;&amp;#34;Sheeran&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; adele = Singer(context: moc)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adele.firstName = &lt;span style="color:#e6db74"&gt;&amp;#34;Adele&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adele.lastName = &lt;span style="color:#e6db74"&gt;&amp;#34;Adkins&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? moc.save()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Show A&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastNameFilter = &lt;span style="color:#e6db74"&gt;&amp;#34;A&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Show S&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastNameFilter = &lt;span style="color:#e6db74"&gt;&amp;#34;S&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the subview that builds the FetchRequest and the filtered list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FilteredList&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @FetchRequest &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; fetchRequest: FetchedResults&amp;lt;Singer&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List(fetchRequest, id: &lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;) { singer &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;singer.wrappedFirstName&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;singer.wrappedLastName&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;(filter: String) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _fetchRequest = FetchRequest&amp;lt;Singer&amp;gt;(sortDescriptors: [], predicate: NSPredicate(format: &lt;span style="color:#e6db74"&gt;&amp;#34;lastName BEGINSWITH %@&amp;#34;&lt;/span&gt;, filter))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="task-1"&gt;Task 1&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Modify the FilteredList View we made to make it accept a string parameter that controls which predicate is applied. You can use Swift’s string interpolation to place this in the predicate.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The changes here are to the init() in the FilteredList. Just need another argument to replace the format parameter in the FetchRequest. I may have misunderstood something, because Paul hinted to use string interpolation, but I can&amp;rsquo;t see that it&amp;rsquo;s needed.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;init(format: String, filter: String) {
 _fetchRequest = FetchRequest&amp;lt;Singer&amp;gt;(sortDescriptors: [], predicate: NSPredicate(format: format, filter))
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then when we call it, just pass in the format string.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// list of matching singers
FilteredList(format: &amp;#34;lastName BEGINSWITH %@&amp;#34;, filter: lastNameFilter)
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="task-2"&gt;Task 2&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;m a fan of this task, because of my wariness of Hard Coded Strings.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Modify the predicate string parameter to be an enum such as &lt;code&gt;.beginsWith&lt;/code&gt;, then make that enum get resolved to a string inside the initializer.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The enum, with attached raw values:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;enum PredicateType: String {
 case beginsWith = &amp;#34;BEGINSWITH&amp;#34;
 case beginsWithIgnoreCase = &amp;#34;BEGINSWITH[c]&amp;#34;
 case contains = &amp;#34;CONTAINS&amp;#34;
 case equals = &amp;#34;==&amp;#34;
 case lessThan = &amp;#34;&amp;lt;&amp;#34;
 case greaterThan = &amp;#34;&amp;gt;&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The ContentView has a @State variable for it. The user clicks on buttons to change it, and it&amp;rsquo;s passed to the subview instead of the previous string:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Environment(&lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.managedObjectContext) &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; moc
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lastNameFilter = &lt;span style="color:#e6db74"&gt;&amp;#34;A&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; predicateType = PredicateType.beginsWith
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// list of matching singers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FilteredList(format: predicateType, filter: lastNameFilter)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Add Examples&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; taylor = Singer(context: moc)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; taylor.firstName = &lt;span style="color:#e6db74"&gt;&amp;#34;Taylor&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; taylor.lastName = &lt;span style="color:#e6db74"&gt;&amp;#34;Swift&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; ed = Singer(context: moc)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ed.firstName = &lt;span style="color:#e6db74"&gt;&amp;#34;Ed&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ed.lastName = &lt;span style="color:#e6db74"&gt;&amp;#34;Sheeran&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; adele = Singer(context: moc)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adele.firstName = &lt;span style="color:#e6db74"&gt;&amp;#34;Adele&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adele.lastName = &lt;span style="color:#e6db74"&gt;&amp;#34;Adkins&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? moc.save()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Begins with A&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastNameFilter = &lt;span style="color:#e6db74"&gt;&amp;#34;A&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; predicateType = .beginsWith
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Begins with S&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastNameFilter = &lt;span style="color:#e6db74"&gt;&amp;#34;S&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; predicateType = .beginsWith
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Contains n&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastNameFilter = &lt;span style="color:#e6db74"&gt;&amp;#34;n&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; predicateType = .contains
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then in the subview, we grab the rawValue and interpolate that into the string. I&amp;rsquo;m just now realising that for Task 1, Paul probably didn&amp;rsquo;t want the whole string passed in, just the &amp;ldquo;BEGINSWITH&amp;rdquo; operator or whatever.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FilteredList&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @FetchRequest &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; fetchRequest: FetchedResults&amp;lt;Singer&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List(fetchRequest, id: &lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;) { singer &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;singer.wrappedFirstName&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;singer.wrappedLastName&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;(format: PredicateType, filter: String) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _fetchRequest = FetchRequest&amp;lt;Singer&amp;gt;(sortDescriptors: [], predicate: NSPredicate(format: &lt;span style="color:#e6db74"&gt;&amp;#34;lastName &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;format.rawValue&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; %@&amp;#34;&lt;/span&gt;, filter))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="task-3"&gt;Task 3&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Make &lt;code&gt;FilteredList&lt;/code&gt; accept an array of &lt;code&gt;SortDescriptor&lt;/code&gt; objects to get used in its fetch request.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yet another change to the init in our subview. It&amp;rsquo;s getting kinda messy:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;init(format: PredicateType, filter: String, sortDescriptors: [SortDescriptor&amp;lt;Singer&amp;gt;]) {
 _fetchRequest = FetchRequest&amp;lt;Singer&amp;gt;(sortDescriptors: sortDescriptors, predicate: NSPredicate(format: &amp;#34;lastName \(format.rawValue) %@&amp;#34;, filter))
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in the subview call:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// list of matching singers
FilteredList(format: predicateType, filter: lastNameFilter, sortDescriptors: [
 SortDescriptor&amp;lt;Singer&amp;gt;(\.firstName)
])
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>You Can Take Big Steps When You Feel Safe</title><link>https://blog.iankulin.com/you-can-take-big-steps-when-you-feel-safe/</link><pubDate>Wed, 09 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/you-can-take-big-steps-when-you-feel-safe/</guid><description>&lt;p&gt;&lt;a href="https://www.deviantart.com/jhonair/art/Forest-of-giantess-604262747"&gt;&lt;img src="https://blog.iankulin.com/images/forest-of-giantess-jhonair.png" alt="" title="Forest-of-giantess By JhonAir"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/100/swiftui/58"&gt;Day 58&lt;/a&gt; of &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100Days&lt;/a&gt; feels like complex topics are being dropped in pretty fast. We tackle one:many data relationships and how to set them up in CoreData, using CoreData constraints and setting a merge policy to manage conflicts, and even the underscore to access the actual property inside a wrapped property struct (needed for dynamic filtering in a view).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/top-four-reasons-why-twostraws-is-a-good-teacher/"&gt;mentioned before&lt;/a&gt; that I think Paul Hudson is an excellent teacher, and an example of this is that even though this was a day with a lot of challenging material, I&amp;rsquo;m not worried. I followed the discussion and tried the code, and more importantly I&amp;rsquo;m anticipating these new skills will be practiced in the next app, and probably shortly after I&amp;rsquo;ll be writing an app using them.&lt;/p&gt;
&lt;p&gt;When learners feel safe and supported, they are comfortable taking bigger risks. This has the effect of growing their Zone of Proximal Development and allows faster learning.&lt;/p&gt;
&lt;p&gt;Some of the complexity around CoreData relates to it&amp;rsquo;s pre-SwiftUI age - it has a lot of power, and does a lot for the developer but is full of non-intuitive bits. The rest of the complexity is really just related to it&amp;rsquo;s job - any object graph persistence that&amp;rsquo;s going to allow us to think of, and work with, our data as native objects is going to have to expose some of the complexity of what&amp;rsquo;s happening underneath in order to provide the flexibility needed. What&amp;rsquo;s not so evident in this implementation is Swifts progressive disclosure of complexity. It&amp;rsquo;s easy to imagine a modern rewrite of a more Swift-like object persistence framework being less scary.&lt;/p&gt;
&lt;p&gt;Since CoreData is using SQLite underneath, an interesting question is what the same code would look like if you pulled in an SQLite library and handled things manually - to approach the same functionality - ie not refetching when a view is recreated if the data hasn&amp;rsquo;t changed, lazy list building etc. My guess is: a lot more complex.&lt;/p&gt;</description></item><item><title>git - Rollback to last commit</title><link>https://blog.iankulin.com/git-rollback-to-last-commit/</link><pubDate>Tue, 08 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-rollback-to-last-commit/</guid><description>&lt;p&gt;I&amp;rsquo;m on &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui"&gt;Project 12&lt;/a&gt; of the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100Days&lt;/a&gt; course, and like a number of earlier &amp;ldquo;projects&amp;rdquo; it&amp;rsquo;s not really a project, but a series of type-along tutorials. Often these have the same format - there&amp;rsquo;s a base amount of code to provide the setup, then this base is used to try each of the tutorial techniques. At the end of each technique, you delete all the new code you&amp;rsquo;ve done back to the original setup, and you&amp;rsquo;re ready for the next one.&lt;/p&gt;
&lt;p&gt;This is a perfect job for git. All I do is commit the code once the setup is done (and I&amp;rsquo;ve tested it). Then after I&amp;rsquo;ve mucked around, and want to go back.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git reset --hard
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In fact this is all so simple, it&amp;rsquo;s probably the perfect use case for getting started with git even if you&amp;rsquo;ve never used it. The steps would be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On a system with git installed (which includes macOS)&lt;/li&gt;
&lt;li&gt;Open a terminal window, and navigate to the directory you want to be able to rollback later&lt;/li&gt;
&lt;li&gt;&lt;code&gt;**git init**&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create all your content&lt;/li&gt;
&lt;li&gt;&lt;code&gt;**git add -A**&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;**git commit -m &amp;quot;Some message&amp;quot;**&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make all your disposable changes, then when you&amp;rsquo;re done &lt;code&gt;**git reset --hard**&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Bookworm Feedback</title><link>https://blog.iankulin.com/bookworm-feedback/</link><pubDate>Mon, 07 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bookworm-feedback/</guid><description>&lt;p&gt;I did so well on this one that it&amp;rsquo;s not going to make a very interesting post. My first two challenge solutions were pretty much character for character the same - so not much to report.&lt;/p&gt;
&lt;p&gt;On the third challenge, there was a minor difference in the display process. I had done this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let date = book.date ?? Date()
Text(date.formatted(.dateTime.day().month().year()))
 .foregroundColor(.secondary)
 .opacity(date == book.date ? 1 : 0)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But @twostraws went:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;if let date = book.date {
 Text(date.formatted(date:.abbreviated, time:.omitted))
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I agree the &lt;em&gt;if let&lt;/em&gt; is neater. I was heading a warning from earlier in the series about avoiding if statements when building views as they can cause the view to have to be destroyed and recreated if it contains different elements (as opposed to a property change - which I don&amp;rsquo;t fully understand since I also believed those modifier properties where causing the views to be recreated inside other views?), but I don&amp;rsquo;t know if that&amp;rsquo;s an issue at all for list elements - I&amp;rsquo;m guessing not if Paul is doing it this way. I have noticed (when playing with print statements in init methods) that SwiftUI is very good at only recreating the views that need recreated.&lt;/p&gt;
&lt;p&gt;I would stand by my use of the opacity to disappear the text but save the space though - this is a good trick if you want to have a piece of a view not there, but still reserve its space.&lt;/p&gt;
&lt;p&gt;Paul has also done better with his date format. Mine does not take into account the date formats for different locales, where as iOS will manage that with Paul&amp;rsquo;s use of .abbreviated.&lt;/p&gt;</description></item><item><title>Bookworm Challenges</title><link>https://blog.iankulin.com/bookworm-challenges/</link><pubDate>Sun, 06 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bookworm-challenges/</guid><description>&lt;p&gt;Another set of challenges for a &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100DaysofSwiftUI&lt;/a&gt; tutorial app. Project 11 was a book tracking app - the big new thing was using CoreData. Here&amp;rsquo;s the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/bookworm-wrap-up"&gt;challenges for it&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Right now it’s possible to select no title, author, or genre for books, which causes a problem for the detail view. Please fix this, either by forcing defaults, validating the form, or showing a default picture for unknown genres – you can choose.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The genre is already forced since it uses a picker, but I added a default (plain grey) image to deal with the situation that there&amp;rsquo;s no data for it in a record. It doesn&amp;rsquo;t make sense to provide defaults for the title or author, but some validation to ensure those fields are not empty is worthwhile.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Section {
 Button(&amp;#34;Save&amp;#34;) {
 let newBook = Book(context: moc)
 newBook.id = UUID()
 newBook.title = title
 newBook.author = author
 newBook.rating = Int16(rating)
 newBook.genre = genre
 newBook.review = review
 newBook.date = Date()

 try? moc.save()
 dismiss()
 }
}
.disabled(title.isEmpty || author.isEmpty || genre.isEmpty)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://github.com/IanKulin/Bookworm/commit/989942b228f96540f1f46e04e91e4816f9072a38"&gt;&lt;img src="https://blog.iankulin.com/images/github-mark-120px-plus.png" width="34" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Modify &lt;code&gt;ContentView&lt;/code&gt; so that books rated as 1 star are highlighted somehow, such as having their name shown in red.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Straightforward, with use of the ternary operator.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-04-at-6.17.12-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/Bookworm/commit/cc81d29a799e7bb97e9813c4ad17e66a64361e6a"&gt;&lt;img src="https://blog.iankulin.com/images/github-mark-120px-plus-1.png" width="35" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Add a new “date” attribute to the Book entity, assigning &lt;code&gt;Date.now&lt;/code&gt; to it so it gets the current date and time, then format that nicely somewhere in &lt;code&gt;DetailView&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Add the date field to the datamodel&lt;/li&gt;
&lt;li&gt;Set it to current date in the Save of the AddView&lt;/li&gt;
&lt;li&gt;Showing it in the Detail View was a little more interesting - adding a test to ensure it was in the database - since we&amp;rsquo;ve now got two versions of these records.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Text(book.author ?? &amp;#34;Unknown author&amp;#34;)
 .font(.title)
 .foregroundColor(.secondary)

let date = book.date ?? Date()
Text(date.formatted(.dateTime.day().month().year()))
 .foregroundColor(.secondary)
 .opacity(date == book.date ? 1 : 0)
 
Text(book.review ?? &amp;#34;No review&amp;#34;)
 .padding()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://github.com/IanKulin/Bookworm/commit/9ec21d4d827b1ae1799c34d65b9d2366e5c4ce36"&gt;&lt;img src="https://blog.iankulin.com/images/github-mark-120px-plus-2.png" width="36" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>@Binding - data between views</title><link>https://blog.iankulin.com/binding-data-between-views/</link><pubDate>Sat, 05 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/binding-data-between-views/</guid><description>&lt;p&gt;In C world, if we want to pass a parameter down into a functional call, and allow the receiving function to change it&amp;rsquo;s value, we&amp;rsquo;d pass a pointer to the variable. Something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

void increment(int* b) {
 *b=*b+1;
}

int main() {
 int a = 5;
 increment(&amp;amp;a);
 printf(&amp;#34;%d&amp;#34;, a);
 return 0;
}

// prints &amp;#39;6&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For youngsters, what&amp;rsquo;s happening is that we&amp;rsquo;ve set the value of a to 5, then passed the memory &lt;em&gt;address&lt;/em&gt; of a into the increment() function. That&amp;rsquo;s what the @a means.&lt;/p&gt;
&lt;p&gt;In the increment function we&amp;rsquo;re expecting an *int - ie the address of an int. Then we &lt;em&gt;dereference&lt;/em&gt; it with the asterisks to operate on the value stored in the address.&lt;/p&gt;
&lt;p&gt;The reason I mention all this ancient lore is because it&amp;rsquo;s what I imagine is happening with the @Binding in a subview - although I&amp;rsquo;m also sure the story underneath is more complex than that.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SwiftUI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; someInt = &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SubView(someValue: &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;someInt)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Content view &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;someInt&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SubView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Binding &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; someValue: Int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Subview &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;someValue&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;Inc&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someValue &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }.padding()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a couple of things to note here. In the SubView struct, we&amp;rsquo;ve used the @Binding property wrapper, and in the ContentView where we&amp;rsquo;ve passed the state variable from, we&amp;rsquo;ve marked it with the $ so indicate we know its a binding variable that can be mutated by whatever it&amp;rsquo;s being passed to.&lt;/p&gt;
&lt;p&gt;If we leave the $ off, the compiler will point out our error because it knows it&amp;rsquo;s bound in the other view.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-03-at-9.17.35-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>CoreData and the Preview</title><link>https://blog.iankulin.com/coredata-and-the-preview/</link><pubDate>Fri, 04 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/coredata-and-the-preview/</guid><description>&lt;p&gt;I&amp;rsquo;ve noticed Paul is inclined to ignore the preview and run his code in the simulator to check its operation. That&amp;rsquo;s valid, but it seems quicker, and reassuring, to see it in the preview as I type.&lt;/p&gt;
&lt;p&gt;This led to a small problem with &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui"&gt;Day 53&lt;/a&gt; that uses CoreData. When I added a student in the preview, it looked like this, and was immediately followed with a crash report.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-30-at-11.32.07-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It ran fine in the simulator, and in the text for Day 53 there was a comment about adding the managed object context to the preview, but without a hint about how.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-30-at-11.34.07-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I tried a few things - it felt like I should be able to get it from the @Environment somehow, but ended up using &lt;a href="https://www.hackingwithswift.com/forums/100-days-of-swiftui/day-53-question-how-to-set-up-a-managed-object-context-in-xcode-s-swiftui-previews/16686"&gt;this&lt;/a&gt; solution from fellow HWS student &lt;a href="https://www.hackingwithswift.com/users/Fly0strich"&gt;@Fly0strich&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-30-at-11.47.51-am.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Cupcake Corner Feedback</title><link>https://blog.iankulin.com/cupcake-corner-feedback/</link><pubDate>Thu, 03 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/cupcake-corner-feedback/</guid><description>&lt;p&gt;As usual, here&amp;rsquo;s my thoughts comparing my attempts at the challenges to Paul&amp;rsquo;s. Usually he&amp;rsquo;s better!&lt;/p&gt;
&lt;h4 id="1-whitespace"&gt;1) Whitespace&lt;/h4&gt;
&lt;p&gt;The task was to validate the order address properties, not just by checking they are not empty, but also that they don&amp;rsquo;t just contain spaces. I went the bruteforce route since there was no .isEmptyIncludingWhitespace method.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var hasValidAddress: Bool {
 let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
 let trimmedStreetAddress = streetAddress.trimmingCharacters(in: .whitespacesAndNewlines)
 let trimmedCity = city.trimmingCharacters(in: .whitespacesAndNewlines)
 let trimmedZip = zip.trimmingCharacters(in: .whitespacesAndNewlines)

 if trimmedName.isEmpty || trimmedStreetAddress.isEmpty || trimmedCity.isEmpty || trimmedZip.isEmpty {
 return false
 }
 return true
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As soon as Paul mentioned extending String, I facepalmed - of course, just create the method I want on string. Paul&amp;rsquo;s is a one line extension - neater, and Swiftyier.&lt;/p&gt;
&lt;h4 id="2-alert-for-post-fail"&gt;2) Alert for POST fail&lt;/h4&gt;
&lt;p&gt;Paul&amp;rsquo;s approach exactly the same as mine, with the addition of showing the localizedDescription of &lt;em&gt;error&lt;/em&gt; which is something that must exist in catch blocks.&lt;/p&gt;
&lt;h4 id="3-struct-wrapper"&gt;3) struct Wrapper&lt;/h4&gt;
&lt;p&gt;We had the same approach. I liked Paul&amp;rsquo;s naming better. He named the wrapper class &lt;em&gt;SharedOrder&lt;/em&gt;, then the instance &lt;em&gt;order&lt;/em&gt;. Then the instance of the struct &lt;em&gt;data&lt;/em&gt;. That way the name hierarchys in the code were something like &lt;em&gt;order.data.street&lt;/em&gt; which was better than mine, although they still bug me. Another difference I noticed was that the static enum for the cupcake types he put in the wrapper class whereas I had it in the order struct.&lt;/p&gt;
&lt;p&gt;Paul left the CodingKey enum in - no problem with that, but I can&amp;rsquo;t see that it&amp;rsquo;s needed.&lt;/p&gt;
&lt;h4 id="bombshell"&gt;Bombshell&lt;/h4&gt;
&lt;p&gt;You know how I was complaining that the class.struct.propertyname things were the main downside of the class wrapping a struct approach? Next Paul pulls a rabbit out of his hat with &lt;em&gt;@dynamicMemberLookup&lt;/em&gt; combined with &lt;em&gt;keyPaths&lt;/em&gt;. I&amp;rsquo;m not going to explain how these work, but the effect is that we can eliminate the struct name from our names so &lt;em&gt;order.data.street&lt;/em&gt; is just &lt;em&gt;order.street&lt;/em&gt; but it is still referencing our struct property wrapped in the class.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-29-at-9.16.01-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Cupcake Corner challenges</title><link>https://blog.iankulin.com/cupcake-corner-challenges/</link><pubDate>Wed, 02 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/cupcake-corner-challenges/</guid><description>&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/cupcake-corner-wrap-up"&gt;Day 52&lt;/a&gt; of &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100Days&lt;/a&gt; was the challenges to the Cupcake Corner app - an app that allows you to build a one-row order, encode it as JSON and submit it to an API with a URLSession. To allow the order to be passed around, it&amp;rsquo;s an @ObservedObject which meant that a few extra hoops needed to be jumped through to make it Codable.&lt;/p&gt;
&lt;h4 id="1-whitespace-validation"&gt;1) Whitespace validation&lt;/h4&gt;
&lt;p&gt;The tutorial app validates the order address by checking that each field is not empty, but it can be fooled by just entering some spaces. The first challenge was to fix that.&lt;/p&gt;
&lt;p&gt;The tutorial version of the app accomplished the checking with a computed property in the Order struct - which is a good place for it. Here it is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var hasValidAddress: Bool {
 if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
 return false
 }
 return true
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s no .isEmptyIfYouIgnoreWhiteSpace method, so I did this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;var hasValidAddress: Bool {
 let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
 let trimmedStreetAddress = streetAddress.trimmingCharacters(in: .whitespacesAndNewlines)
 let trimmedCity = city.trimmingCharacters(in: .whitespacesAndNewlines)
 let trimmedZip = zip.trimmingCharacters(in: .whitespacesAndNewlines)

 if trimmedName.isEmpty || trimmedStreetAddress.isEmpty || trimmedCity.isEmpty || trimmedZip.isEmpty {
 return false
 }

 return true
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://github.com/IanKulin/CupcakeCorner/commit/2bd1247d268c41b8cb83c07af55ca4bb6f291e81#diff-5d943085c2460b6ea685f488eec227df2a6fec0aa2155bc7ffa85d457604c91e"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="2-show-an-alert-if-the-post-fails"&gt;2) Show an alert if the POST fails&lt;/h4&gt;
&lt;p&gt;In the tute version, this is just a print statement. This challenge just involves adding a second alert to the checkout view. &lt;a href="https://github.com/IanKulin/CupcakeCorner/commit/cfb2347c3e48fd68ac47b4f2153cc1faa01149ee"&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="3-change-order-to-struct"&gt;3) Change order to struct&lt;/h4&gt;
&lt;p&gt;This is a bit more complicated. A reason for preferring a struct is that all of the extra work we did to make the class Codable is eliminated. We need the thing being passed around to be an object because we want a reference type that can be mutated inside the view hierarchy, and because we want it to be an @Observed Object. The change proposed here is sort of the best of both worlds - have the object, but it&amp;rsquo;s sole property is the struct.&lt;/p&gt;
&lt;p&gt;I created a wrapper class, with the struct as an @Published var.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class Wrapper: ObservableObject {
 @Published var order = Order()

 deinit {
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then I changed the Order class to a struct, removed @Published from all the properties and deleted the encode/decode code. Then in all the views, I had to go through and fix the names. I did not love the less readable names I was creating; for example &lt;code&gt;wrapped.order.streetAddress&lt;/code&gt; instead of just &lt;code&gt;order.streetAddress&lt;/code&gt;. But that all worked.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/CupcakeCorner/commit/c8559232b681f527c72ee9b2d89bfba997894d84#diff-5d943085c2460b6ea685f488eec227df2a6fec0aa2155bc7ffa85d457604c91e"&gt;source&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Top Four Reasons why @TwoStraws is a Good Teacher</title><link>https://blog.iankulin.com/top-four-reasons-why-twostraws-is-a-good-teacher/</link><pubDate>Tue, 01 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/top-four-reasons-why-twostraws-is-a-good-teacher/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-29-at-1.28.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-29-at-1.28.59-pm.png" width="241" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="good-questions"&gt;Good Questions&lt;/h4&gt;
&lt;p&gt;At various points in the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of SwiftUI&lt;/a&gt; course, you get asked sets of questions to check you&amp;rsquo;ve understood the preceding material. They&amp;rsquo;re usually presented as two different statements, one of which is true, and the other false. It&amp;rsquo;s actually a really good technique - the student feels like they&amp;rsquo;ve got a couple of opportunities to figure it out, plus they are forced to read both statements and think about them. Paul does a similar thing in the Unwrapped app - there, the questions are often presented as &amp;ldquo;Is this valid Swift code&amp;rdquo; and the user needs to scan through it all looking for mistakes. It&amp;rsquo;s checking your understanding, and making you a thoughtful debugger!&lt;/p&gt;
&lt;h4 id="zone-of-proximal-development"&gt;Zone of Proximal Development&lt;/h4&gt;
&lt;p&gt;You know how if something is too easy, it&amp;rsquo;s not engaging? It&amp;rsquo;s why you don&amp;rsquo;t choose Snap when you sit down to play cards. It&amp;rsquo;s so far below your skill level your brain is not interested in it. There&amp;rsquo;s a similar problem at the other end - if I ask you do try something that&amp;rsquo;s so hard for you that you&amp;rsquo;ll never be able to achieve it, you want want to do it again. For good learning to take place, it&amp;rsquo;s important to pitch the difficulty of activities just ever so slightly in advance of what the student can comfortably do. This is the zone where the most learning takes place in the shortest amount of time.&lt;/p&gt;
&lt;h4 id="learning-in-context"&gt;Learning in Context&lt;/h4&gt;
&lt;p&gt;Logically, you could teach iOS development with a semester of pure Swift teaching before you got to your first app. Probably there are courses that do that, but if you want to engage your learners do it with as much real life context and hands-on activity as possible. #100Days is all about this.&lt;/p&gt;
&lt;h4 id="beginner-mind"&gt;Beginner Mind&lt;/h4&gt;
&lt;p&gt;In the videos/lessons, Paul often anticipates how the learner might expect something to work, or how they might tackle a problem, before explaining the problem with that thinking or showing a better way of doing things. This is a great trait of a teacher. Often it&amp;rsquo;s difficult for experts (which Paul undoubtedly is) to recall how things looked to them as they were learning. Anticipating the state of mind of the learner, and moving them from that point is both comforting for the learner, and avoids confusion.&lt;/p&gt;
&lt;h4 id="currency"&gt;Currency&lt;/h4&gt;
&lt;p&gt;As soon as you start googling problems and reading blog posts or StackOverflow answers, it becomes apparent that the rapid development of Swift and SwiftUI has a downside - a lot of the helpful information put out there is out of date. Like everyone, I&amp;rsquo;m amazed at the work Paul puts in to producing his massive amount of content, and then keeping it up to date. If there&amp;rsquo;s a Hacking With Swift result in a search you&amp;rsquo;ve made, that&amp;rsquo;s the one to click on.&lt;/p&gt;</description></item><item><title>Codable when the keys don't match</title><link>https://blog.iankulin.com/codable-when-the-keys-dont-match/</link><pubDate>Mon, 31 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/codable-when-the-keys-dont-match/</guid><description>&lt;p&gt;A common issue when working with JSON that you vacuum up from internet APIs will be that the key names in the JSON don&amp;rsquo;t match your property names. The JSON de facto standard of using snake_case in key names could be one cause, or perhaps you just take &lt;a href="https://www.freshconsulting.com/insights/blog/development-principle-1-choose-appropriate-variable-names/"&gt;variable naming more seriously&lt;/a&gt; than the person who wrote the API.&lt;/p&gt;
&lt;p&gt;We saw yesterday how using codable and the JSONEncoder in Swift makes moving between an object/struct in the code and a stringish representation of it simple. With a couple of small changes, we can also deal with the mismatched key/property name issue.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s try yesterday&amp;rsquo;s decode approach with some different JSON:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Foundation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Person&lt;/span&gt;: Codable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id: Int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; firstName: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lastName: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; email: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; jsonString = &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;id&lt;span style="color:#e6db74"&gt;&amp;#34;: 1,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;what_I_call_them&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;Jeanette&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;surname&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;Penddreth&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;email&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;jpenddreth0@census.gov&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = Data(jsonString.utf8)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decoder = JSONDecoder()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; newPerson = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; decoder.decode(Person.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, from: data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// never gets here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(newPerson.lastName)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will fail at line 22. If you look at the JSON you&amp;rsquo;ll notice that the key names don&amp;rsquo;t match our property names. i.e. &lt;code&gt;what_I_call_them&lt;/code&gt; is not the same as &lt;code&gt;firstName&lt;/code&gt;. We could solve that in this example just by changing our property names, but Swift has a better way to help us - CodingKey.&lt;/p&gt;
&lt;p&gt;CodingKey is an enum that maps our property names to the key names used in the JSON. Here&amp;rsquo;s something that would work for the example above:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;enum CodingKeys: String, CodingKey {
 case id
 case firstName = &amp;#34;what_I_call_them&amp;#34;
 case lastName = &amp;#34;surname&amp;#34;
 case email
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Just adding this to our struct will make the decode process work perfectly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Foundation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Person&lt;/span&gt;: Codable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id: Int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; firstName: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lastName: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; email: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;enum&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CodingKeys&lt;/span&gt;: String, CodingKey {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; firstName = &lt;span style="color:#e6db74"&gt;&amp;#34;what_I_call_them&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; lastName = &lt;span style="color:#e6db74"&gt;&amp;#34;surname&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; email
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; jsonString = &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;id&lt;span style="color:#e6db74"&gt;&amp;#34;: 1,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;what_I_call_them&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;Jeanette&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;surname&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;Penddreth&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;email&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;jpenddreth0@census.gov&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = Data(jsonString.utf8)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decoder = JSONDecoder()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; newPerson = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; decoder.decode(Person.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, from: data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(newPerson.lastName)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Pendreth&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is an elegant solution, but for the more common situation - where the JSON returned by an API uses the JavaScript convention of snake_case instead of camelCase, there is an even easier approach. The JSONDecoder has a &lt;em&gt;.keyDecodingStrategy&lt;/em&gt; property. To deal with snake case, we just set that to .&lt;em&gt;convertFromSnakeCase&lt;/em&gt;. Note line 23.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Foundation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Person&lt;/span&gt;: Codable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id: Int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; firstName: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lastName: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; email: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; jsonString = &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;id&lt;span style="color:#e6db74"&gt;&amp;#34;: 1,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;first_name&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;Jeanette&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;last_name&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;Penddreth&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;email&lt;span style="color:#e6db74"&gt;&amp;#34;: &amp;#34;&lt;/span&gt;jpenddreth0@census.gov&lt;span style="color:#e6db74"&gt;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; data = Data(jsonString.utf8)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decoder = JSONDecoder()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;decoder.keyDecodingStrategy = .convertFromSnakeCase
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; newPerson = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; decoder.decode(Person.&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, from: data)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(newPerson.lastName)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Pendreth&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Codable &amp; JSON</title><link>https://blog.iankulin.com/codable-json/</link><pubDate>Sun, 30 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/codable-json/</guid><description>&lt;p&gt;If we mark a type with the protocol &lt;em&gt;Codable&lt;/em&gt;, we&amp;rsquo;re specifying that this type has the capability of having it&amp;rsquo;s properties encoded to some format, and decoded back again.&lt;/p&gt;
&lt;p&gt;So far in the #100Days this has been used to write and read data in UserDefaults, and to encode an object to send it as a URLRequest, then receive data back and create a new object from it. It&amp;rsquo;s a handy, powerful feature baked into Swift that just requires the developer to ensure any types that need this functionality comply with the &lt;em&gt;Encodable&lt;/em&gt; and &lt;em&gt;Decodable&lt;/em&gt; protocols that make up the Codable.&lt;/p&gt;
&lt;p&gt;I assume there&amp;rsquo;s some magic (by magic I guess I mean code created during the compile/build process - or perhaps a language feature that lets us enumerate the property names of a type? ) that allows Swift to step through each property in a composite type in order to encode or decode via the encoder/decoder&lt;/p&gt;
&lt;h4 id="making-a-type-codable"&gt;Making a type Codable&lt;/h4&gt;
&lt;p&gt;For simple situations, you just add the Codable protocol to your type. If all the properties inside that type are Codable, then your struct/class will be to. All the straightforward built in types are.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct Chair: Codable {
 var name: String = &amp;#34;&amp;#34;
 var legs: Int = 4
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since String and Int are codable, the Chair struct will be as well. I&amp;rsquo;ll deal with how you fix a type that&amp;rsquo;s not codable another day.&lt;/p&gt;
&lt;h4 id="encoding"&gt;Encoding&lt;/h4&gt;
&lt;p&gt;To encode our data we need an encoder. We&amp;rsquo;ll use the Foundation JSONEncoder, but there are others such as &lt;a href="https://github.com/ShawnMoore/XMLParsing"&gt;XML&lt;/a&gt; and so on.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct Chair: Codable {
 var name: String = &amp;#34;&amp;#34;
 var legs: Int = 4
}

let stool = Chair(name: &amp;#34;Stool&amp;#34;, legs: 3)
let encoder = JSONEncoder()

do {
 let jsonData = try encoder.encode(stool)
 print(String(data: jsonData, encoding: .utf8)!)
} catch {
 print(&amp;#34;Encoding Error&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This prints out:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{&amp;quot;name&amp;quot;:&amp;quot;Stool&amp;quot;,&amp;quot;legs&amp;quot;:3}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So the property names are used to build our data. The &amp;ldquo;data&amp;rdquo; returned by the JSONDecoder is a UTF8 string - this is specified by the JSON standard, and that&amp;rsquo;s why the print above is not just &lt;code&gt;print(jsonData)&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="decoding"&gt;Decoding&lt;/h4&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// use # to delimit a string that contains double quotes
let jsonString = #&amp;#34;{&amp;#34;name&amp;#34;:&amp;#34;Kitchen chair&amp;#34;,&amp;#34;legs&amp;#34;:4}&amp;#34;#

// convert our Swift string into the UTF-8 data type expected
let data = Data(jsonString.utf8)

let decoder = JSONDecoder()
let newChair = try decoder.decode(Chair.self, from: data)

print(newChair.name)
// Kitchen chair
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="wrap-up"&gt;Wrap up&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;ve used the simplest cases here to show how the basics work, but there&amp;rsquo;s a couple of common situations that require a bit more work. The most common is that the JSON you are trying to decode has non-swifty keys. It&amp;rsquo;s common for JSON keys to be snake_case rather than camelCase. Another issue is if our type needs a bit of work to make it codable. There&amp;rsquo;s some overlap in these situations that well look at another time.&lt;/p&gt;</description></item><item><title>Git - make all the commits into a single commit</title><link>https://blog.iankulin.com/git-make-all-the-commits-into-a-single-commit/</link><pubDate>Sat, 29 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-make-all-the-commits-into-a-single-commit/</guid><description>&lt;p&gt;When I&amp;rsquo;m following a tutorial app, I generally pause and type up the code as I go, and make local commits with appropriate messages. This is almost completely unnecessary, but it seems like a good habit and doesn&amp;rsquo;t cost me anything - I just tick the box for creating the git when I start the project, then it&amp;rsquo;s a couple of keystrokes (option-command-C) and I&amp;rsquo;m done.&lt;/p&gt;
&lt;p&gt;Most of the apps have a follow-along portion, then some challenges which involve minor changes to the app. When I get to the challenges I like to throw it up on Github - it&amp;rsquo;s conceivable it could help someone one day, or at the least, I&amp;rsquo;m helping to train &lt;a href="https://github.com/features/copilot"&gt;Microsoft&amp;rsquo;s AI&lt;/a&gt; to write shitty beginner code in exchange for free git server access.&lt;/p&gt;
&lt;p&gt;The early commits I do are no help to anyone, even me by that stage, and it feels somehow untidy to leave them in there, so I started to wonder if there was some branchy/rebasey way to eliminate them before I push it up.&lt;/p&gt;
&lt;p&gt;This and the related problems of just eliminating some of the recent commit history are clearly topics of interest - there&amp;rsquo;s many stackover flow posts and blog articles. But shout out to &lt;a href="https://stackoverflow.com/users/825/pat-notz"&gt;Pat Noz&lt;/a&gt;, for his suggestion - &lt;a href="https://stackoverflow.com/questions/1657017/how-to-squash-all-git-commits-into-one"&gt;just delete the .git directory and start over&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-27-at-8.39.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;When you &lt;code&gt;git init&lt;/code&gt;, a hidden folder is created in the directory you init in called &lt;code&gt;.git&lt;/code&gt; - you don&amp;rsquo;t normally see these hidden folders, but if you press &lt;code&gt;command-shift-.&lt;/code&gt; you can see it. This directory holds all the data that allows the magic of git to happen. If you delete just this directory and it&amp;rsquo;s contents, it&amp;rsquo;s like you never used git on this code.&lt;/p&gt;
&lt;p&gt;So in Pat&amp;rsquo;s words:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rm -rf .git
git init
git add .
git commit
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simple. Elegant. Obvious once you&amp;rsquo;ve had the suggestion.&lt;/p&gt;</description></item><item><title>Day 50 - @State vs @Observed again</title><link>https://blog.iankulin.com/day-50-state-vs-obseved-again/</link><pubDate>Fri, 28 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-50-state-vs-obseved-again/</guid><description>&lt;p&gt;Way back when, I was unclear about @StateObject and @ObservedObject (&lt;a href="https://blog.iankulin.com/simple-mvvm/"&gt;here&lt;/a&gt;, and &lt;a href="https://blog.iankulin.com/observedobject-v-stateobject/"&gt;here&lt;/a&gt;). I still am.&lt;/p&gt;
&lt;p&gt;But in &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/taking-basic-order-details"&gt;today&amp;rsquo;s tutorial&lt;/a&gt; video, Paul clearly says that the @StateObject is for the single place in your app where the object is created, then everywhere else, use @ObservedObject. Trouble is, I just know from the Simple MVVM app I made if you wrap the single instance of the data model with the @ObservedObject, it still works.&lt;/p&gt;
&lt;p&gt;Half way through my #100Days and this mystery is still not solved for me. I imagine now it never will be until I can figure out how the property wrappers differ. When you hold down command, and click over a keyword, you get a list of options. One of these options is &amp;ldquo;Jump to definition&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-26-at-6.28.33-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So I should be able to do that with @StateObject and @ObservedObject then just diff the code and see right?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-26-at-6.26.19-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Obviously, I&amp;rsquo;m not comprehending all of this, but it looks like the main difference is this extra Wrapper struct in the ObservedObject:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@dynamicMemberLookup @frozen public struct Wrapper {
 public subscript&amp;lt;Subject&amp;gt;(dynamicMember keyPath: ReferenceWritableKeyPath&amp;lt;ObjectType, Subject&amp;gt;) -&amp;gt; Binding&amp;lt;Subject&amp;gt; { get }
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Yeah. I&amp;rsquo;m not going to be able to unpack that bad boy. I&amp;rsquo;ll just file it in &amp;ldquo;probably something to do with reference counting, and come back to it when I&amp;rsquo;m smarter.&lt;/p&gt;</description></item><item><title>Day 47 - Habits App</title><link>https://blog.iankulin.com/day-47-habits-app/</link><pubDate>Thu, 27 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-47-habits-app/</guid><description>&lt;p&gt;I&amp;rsquo;ve been mucking around with the Habits app too long - it&amp;rsquo;s started to look like procrastination. It already meets the &lt;a href="https://www.hackingwithswift.com/100/swiftui/47"&gt;specification&lt;/a&gt;, so I&amp;rsquo;m calling it an MVP and moving on.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/Habitual"&gt;&lt;img src="https://blog.iankulin.com/images/github-mark-32px.png" width="32" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_3110.png" width="266" alt=""&gt;
&lt;p&gt;This is the first app of mine I&amp;rsquo;ve loaded onto my phone and started using, and there are a couple of things I&amp;rsquo;d like to do with it. It currently just lets you specify how many days between an activity repeating - so if you say you should go to the gym every second day, and you complete that activity on Monday, &amp;ldquo;Gym&amp;rdquo; will make it&amp;rsquo;s way to the top of the list on Wednesday. While it&amp;rsquo;s waiting in the list for Wednesday to come around, it will show the &amp;ldquo;Due&amp;rdquo; time as being exactly 48 hours after you last pressed &amp;ldquo;done&amp;rdquo; on it. But if the habit you want is to go to the gym after work at 6:00pm that&amp;rsquo;s when you want it to be due. I&amp;rsquo;d like that.&lt;/p&gt;
&lt;p&gt;This does overlap a bit with the idea of a &amp;ldquo;ToDo&amp;rdquo; list, so maybe it&amp;rsquo;s a feature creep it shouldn&amp;rsquo;t have. Espeically since the reason I wanted it was because in the process of adding test activities I added &amp;ldquo;Take out the bins&amp;rdquo; and that has to be done on Wednesday nights.&lt;/p&gt;
&lt;p&gt;Paul&amp;rsquo; has moved away from all the data being in an @State in the View to having a data model object in a separate file. It feels like only a few steps until he jumps out from behind a bush and says &amp;ldquo;You&amp;rsquo;ve been writting MVVM already!&amp;rdquo; although currently its a bit more like just MV.&lt;/p&gt;
&lt;p&gt;The list of things I&amp;rsquo;m going to come back to is growing - I also have still not finished writing the custom picker so I can match the design work I paid for on the &lt;a href="https://blog.iankulin.com/design-help/"&gt;times table app&lt;/a&gt;. Nevertheless, I&amp;rsquo;ve so far spent 120 days on the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100DaysOfSwiftUI&lt;/a&gt; and I&amp;rsquo;m only up to day 47, so time to get moving.&lt;/p&gt;</description></item><item><title>Why?</title><link>https://blog.iankulin.com/why/</link><pubDate>Wed, 26 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/why/</guid><description>&lt;p&gt;Why do I have to resize this preview window every time I open Xcode?&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/y4imP93Czmc?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Updating stored JSON due to a struct change</title><link>https://blog.iankulin.com/updating-stored-json-due-to-a-struct-change/</link><pubDate>Tue, 25 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/updating-stored-json-due-to-a-struct-change/</guid><description>&lt;p&gt;I mentioned yesterday &amp;ldquo;&lt;em&gt;I could use a renamed old version of my struct to load the existing data, and convert it across to the new model.&lt;/em&gt;&amp;rdquo;. Since I&amp;rsquo;ve been testing the app on my phone, and using plausible data, it was going to be painful enough to lose it that I thought I should go through those steps.&lt;/p&gt;
&lt;p&gt;First, I make a copy of the old struct, and renamed it with the app version number that used it. No need to bring all the computed properties into this struct, just the bits that get encoded into the JSON.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;V01HabitItem&lt;/span&gt;: Identifiable, Codable, Equatable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; name: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; started = Date()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; timesDone = &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lastDone: Date
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; daysBetweenCompletions = &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I could go ahead and change the official struct to add the new properties it needs. Now when the JSON is attempted to be decoded into the new struct, it will fail, so we need to detect that and try with the old version of the struct.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Habits&lt;/span&gt;: ObservableObject {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Published &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items = [HabitItem]() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;didSet&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; encoded = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? JSONEncoder().encode(items) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UserDefaults.standard.&lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;(encoded, forKey: &lt;span style="color:#e6db74"&gt;&amp;#34;Habits&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;JSON encoding fail&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; savedItems = UserDefaults.standard.data(forKey: &lt;span style="color:#e6db74"&gt;&amp;#34;Habits&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decodedItems = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? JSONDecoder().decode([HabitItem].&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, from: savedItems) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items = decodedItems
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;JSON decoding fail - trying v0.1&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; v01Items = [V01HabitItem]()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decodedItems = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? JSONDecoder().decode([V01HabitItem].&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, from: savedItems) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v01Items = decodedItems
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v01Items.forEach { oldHabit &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items.append(HabitItem(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id: oldHabit.id,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name: oldHabit.name,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; started: oldHabit.started,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timesDone: oldHabit.timesDone,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastDone: oldHabit.lastDone,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; daysBetweenCompletions: oldHabit.daysBetweenCompletions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;JSON decoding fail&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items = []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once it&amp;rsquo;s decoded, we need to ForEach through and create instances of the new struct from the old ones.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s made my init() a bit messy, normally I&amp;rsquo;d move that out - maybe have a separate file with the old struct and a function for decoding it and copying into the new struct, but this is not a production app, it&amp;rsquo;s only on my phone and on two simulations on my macBook, so this code won&amp;rsquo;t stay in once I&amp;rsquo;ve updated each instance by running it once.&lt;/p&gt;
&lt;p&gt;If it works correctly, I should get the debug message and the data will still be there on the first run, then on a second run the message should not appear (since the new struct version will have been written to the file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-22-at-7.41.44-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>JSON encode/decode</title><link>https://blog.iankulin.com/json-encode-decode/</link><pubDate>Mon, 24 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/json-encode-decode/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_3110.png" width="150" alt="Screenshop of Habits app"&gt;
&lt;p&gt;As usual, I&amp;rsquo;m spending way more time on the apps written from scratch in the &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/4/3/challenge"&gt;100Days series&lt;/a&gt;. The Habit tracking app I&amp;rsquo;m working on has been good practice, especially of the architecture of the simple &lt;a href="https://blog.iankulin.com/list-apps/"&gt;list based app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My version has a couple of refinements I quite like. I&amp;rsquo;m using a checkmark in a rectangle as the button to mark that activity as done, and I&amp;rsquo;ve added a nice fade to the checkmark as time goes on to represent the percentage of time from when it is done until it becomes due again.&lt;/p&gt;
&lt;p&gt;Until this app I&amp;rsquo;ve purely been using the preview and simulator for looking and and checking the operation of the app. But now I&amp;rsquo;ve started using my own iPhone for testing and actually trying to use the app from day to day. There&amp;rsquo;s a noticeable difference between thinking up use-cases in your head or &lt;a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food"&gt;dogfooding&lt;/a&gt; it. This has led to several improvements, but now I&amp;rsquo;m at a crossroads with one to do with the data persistence.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using the Swift &lt;a href="https://www.ralfebert.com/ios/json-handling-in-swift/"&gt;built in JSON&lt;/a&gt; encode/decode which has been quite painless. It uses the struct property names (so not really in line with the JSON snakecase convention) and just magics everthing up for you. Here&amp;rsquo;s the structs, and the JSON produced after a week of using the app.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;HabitItem&lt;/span&gt;: Identifiable, Codable, Equatable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; name: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; started = Date()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; timesDone = &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lastDone: Date
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; daysBetweenCompletions = &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Habits&lt;/span&gt;: ObservableObject {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Published &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items = [HabitItem]() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;didSet&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; encoded = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? JSONEncoder().encode(items) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UserDefaults.standard.&lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;(encoded, forKey: &lt;span style="color:#e6db74"&gt;&amp;#34;Habits&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; savedItems = UserDefaults.standard.data(forKey: &lt;span style="color:#e6db74"&gt;&amp;#34;Habits&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; decodedItems = &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;? JSONDecoder().decode([HabitItem].&lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;, from: savedItems) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items = decodedItems
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items = []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;5D52B636-030A-4571-8B6E-2117AFD69304&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Dishes&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687916240.08548605&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687741044.36479199&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;26D89BA0-D3FA-49CF-BB01-249A19E90871&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Bed&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;688060851.81663799&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702031.59694302&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;C73234ED-3FA9-4B91-AC24-42E7391E3BE1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Eating &amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;688108866.69676399&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702494.12051105&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;0940450A-2A5E-4B11-B049-9B33F9C01F7D&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Brush teeth&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;688110048.34211302&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702045.01338601&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;FA6556A8-17A8-4556-8690-9522A5B22856&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Write code&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;688108275.33836806&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702594.956689&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;DA2A1F85-1274-4C58-B411-D6FF9B2A2AAF&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Walk exercise&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702620.82404804&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702620.82411301&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;155D50A7-1403-47C6-AFCA-12DB13270A55&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Bins out&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687879744.60854602&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702107.33206701&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;86FE01B9-4716-4C2C-B768-1AC8E3B03DDB&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Roomba kitchen&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687916244.61723006&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702077.16412401&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;EEA3D2CD-3717-49DD-B5E2-924956F41189&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;timesDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;daysBetweenCompletions&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;&amp;#34;Washing&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;lastDone&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;688115581.30661595&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;started&amp;#34;&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;687702096.33233297&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The problem I&amp;rsquo;ve got now I&amp;rsquo;d like to change the struct to add some different functionality. As soon as I change the struct, on the next run the JSON decode will fail and my data model will be empty. This can be caught, and I could use a renamed old version of my struct to load the existing data, and convert it across to the new model. What I&amp;rsquo;d really like is an option in the JSON decoder that if a property is missing, and I&amp;rsquo;ve provided a default in the struct, that it just uses that.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not the first developer to have this wish, and there&amp;rsquo;s a &lt;a href="https://stackoverflow.com/questions/44575293/with-jsondecoder-in-swift-4-can-missing-keys-use-a-default-value-instead-of-hav"&gt;number of solid solutions&lt;/a&gt;, but they all seem to require a bit more work than my simple dream above.&lt;/p&gt;</description></item><item><title>Refreshing SwiftUI Views</title><link>https://blog.iankulin.com/refreshing-swiftui-views/</link><pubDate>Sun, 23 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/refreshing-swiftui-views/</guid><description>&lt;p&gt;SwiftUI does some property wrapper magic to (very efficiently) refresh your views, but what if you want to force a refresh for some reason? Here&amp;rsquo;s the techniques I&amp;rsquo;m currently using to do that.&lt;/p&gt;
&lt;p&gt;The tricks are below, but just so you can see them in context, here&amp;rsquo;s the sample app we&amp;rsquo;re working on. It&amp;rsquo;s a list of cars so you can keep track of how many of each kind you own. Here&amp;rsquo;s our data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Car&lt;/span&gt;: Identifiable, Codable, Equatable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; model: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; number = &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Cars&lt;/span&gt;: ObservableObject {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Published &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items = [Car]()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; car1 = Car(model: &lt;span style="color:#e6db74"&gt;&amp;#34;Station wagon&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; car2 = Car(model: &lt;span style="color:#e6db74"&gt;&amp;#34;Sedan&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; car3 = Car(model: &lt;span style="color:#e6db74"&gt;&amp;#34;Hatchback&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items = [car1, car2, car3]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;increment&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; car: Car) -&amp;gt; Bool {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; index = items.firstIndex(of: car)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; index = index {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items[index].number &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;deinit&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The app is a list inside a navigation stack. The view for each car is split out into a subview:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @StateObject &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars = Cars()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(cars.items) { car &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CarView(car: car, cars: cars)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Cars&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CarView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; car: Car
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars: Cars
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;Int.random&lt;span style="color:#e6db74"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10.&lt;/span&gt;..&lt;span style="color:#ae81ff"&gt;99&lt;/span&gt;&lt;span style="color:#e6db74"&gt;))&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack(alignment: .leading) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(car.model)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.headline)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;car.number&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;cars.increment(car) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Unexpected error - car not found:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;car.model&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;plus&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The random Int is just so we can get a visual indication that our view has refreshed.&lt;/p&gt;
&lt;h4 id="trick-1---change-a-value"&gt;Trick 1 - change a value&lt;/h4&gt;
&lt;p&gt;All the other tricks rely on this trick. SwiftUI reacts to any @State property changing, so to force a change, there just needs to be a @State property we change. I add a:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@State var refresh = false
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;to my ContentView. Whenever this changes, ie refresh.toggle() the view will be redrawn. Of course we don&amp;rsquo;t want just the ContentView redrawn, but the subview as well, so it needs to be passed into the subview. We don&amp;rsquo;t do anything with it there, just pass it in.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @StateObject &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars = Cars()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; refresh = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(cars.items) { intItem &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CarView(car: intItem, cars: cars, refresh: refresh)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Cars&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CarView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; car: Car
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars: Cars
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; refresh: Bool
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;Int.random&lt;span style="color:#e6db74"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10.&lt;/span&gt;..&lt;span style="color:#ae81ff"&gt;99&lt;/span&gt;&lt;span style="color:#e6db74"&gt;))&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack(alignment: .leading) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(car.model)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.headline)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;car.number&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;cars.increment(car) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Unexpected error - car not found:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;car.model&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;plus&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="trick-2---refreshable"&gt;Trick 2 - .refreshable()&lt;/h4&gt;
&lt;p&gt;That&amp;rsquo;s the infrastructure in place, so now we can use it. We could add a button that the user could click to refresh, but 2022 users have been trained to pull down on views to make them refresh, so let&amp;rsquo;s do that. Just add a &lt;code&gt;.refreshable()&lt;/code&gt; modifier to our list, then toggle refresh in the closure.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @StateObject &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars = Cars()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; refresh = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(cars.items) { intItem &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CarView(car: intItem, cars: cars, refresh: refresh)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Cars&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .refreshable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; refresh.toggle()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now when the user pulls the list down, the familiar wait wheel appears, the view, and the car subviews, all redraw.&lt;/p&gt;
&lt;h4 id="trick-3---onchangeof-scene"&gt;Trick 3 - OnChange(of: scene)&lt;/h4&gt;
&lt;p&gt;In my Habit app, the activities are going to appear with a temporal description of when they should be done, like &amp;ldquo;next week&amp;rdquo;, &amp;ldquo;in a few minutes&amp;rdquo;, or &amp;ldquo;tomorrow&amp;rdquo;. Those will be created from the date/time the activity was last done, how often the activity should be done, and the current date/time. So if our app has been in the background and we open it up, we want fresh calculations. We can do that by detecting a &lt;code&gt;scenePhase&lt;/code&gt; change to &lt;code&gt;.active&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We have to declare a variable for it, and then add the .onChange to the view:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @StateObject &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars = Cars()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; refresh = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Environment(&lt;span style="color:#960050;background-color:#1e0010"&gt;\&lt;/span&gt;.scenePhase) &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; scenePhase
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(cars.items) { intItem &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CarView(car: intItem, cars: cars, refresh: refresh)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Cars&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .onChange(of: scenePhase) { newPhase &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; newPhase == .active {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; refresh.toggle()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="trick-4---a-timer"&gt;Trick 4 - a timer&lt;/h4&gt;
&lt;p&gt;Even if our user leaves the app open and foregrounded, eventually the descriptions will be out of date, so another thing we could do is use a timer. We have to add a timer property, and add an .onReceive() to the view. The timer interval in seconds is set when the timer is created. The example below is going to trigger every second.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @StateObject &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars = Cars()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; refresh = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; timer = Timer.publish(every: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, on: .main, &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;: .common).autoconnect()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(cars.items) { intItem &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CarView(car: intItem, cars: cars, refresh: refresh)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Cars&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .onReceive(timer, perform: { &lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; refresh.toggle()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>You need to enjoy puzzles</title><link>https://blog.iankulin.com/you-need-to-enjoy-puzzles/</link><pubDate>Sat, 22 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/you-need-to-enjoy-puzzles/</guid><description>&lt;p&gt;I&amp;rsquo;m writing the Habits &lt;a href="https://blog.iankulin.com/list-apps/"&gt;list based app&lt;/a&gt; from #100Days and had a working MVP, then for some reason, decided to refactor by changing the subview I&amp;rsquo;d written as a function, into a struct. Some time later, I discovered that my list items were not updating correctly, so detective time.&lt;/p&gt;
&lt;p&gt;I talked a little bit about the architecture yesterday - the item is a struct, and there&amp;rsquo;s a class containing an array of the items. Something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Car&lt;/span&gt;: Identifiable, Codable, Equatable {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; id = UUID()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; model: String
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; number = &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Cars&lt;/span&gt;: ObservableObject {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Published &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; items = [Car]()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; car1 = Car(model: &lt;span style="color:#e6db74"&gt;&amp;#34;Station wagon&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; car2 = Car(model: &lt;span style="color:#e6db74"&gt;&amp;#34;Sedan&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; car3 = Car(model: &lt;span style="color:#e6db74"&gt;&amp;#34;Hatchback&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items = [car1, car2, car3]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;increment&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; car: Car) -&amp;gt; Bool {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; index = items.firstIndex(of: car)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; index = index {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items[index].number &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;deinit&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then the ContentView is a NavigationView with a List. To build the list, I ForEach on the array inside the object, calling a subview on each one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @StateObject &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars = Cars()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NavigationView {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(cars.items) { intItem &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CarView(car: intItem, cars: cars)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;Cars&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CarView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; car: Car
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cars: Cars
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack(alignment: .leading) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(car.model)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.headline)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;car.number&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;cars.increment(car) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Unexpected error - car not found:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;car.model&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;plus&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When the user clicks on the button in a list item, the underlying data is amended somehow - in this pared down example the number of that type of car is incremented. Since my &amp;ldquo;car&amp;rdquo; is a value type there&amp;rsquo;s no point incrementing its number, so instead I call the .increment method on the cars object that is holding my array.&lt;/p&gt;
&lt;p&gt;Then, in that method, I have to search for the car the user intends to change, then change it.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func increment(_ car: Car) -&amp;gt; Bool {
 let index = items.firstIndex(of: car)
 if let index = index {
 items[index].number += 1
 return true
 } else {
 return false
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The situation of not being able to find the car should never occur, but out of longstanding habit I check for it and log an error to the console. This then happens repeatedly&amp;hellip;&lt;/p&gt;
&lt;p&gt;After some print statement debugging, I got it into my head that I&amp;rsquo;d caused this by changing the subview from a function (which had worked) to a struct (which was not). I decided it was a value type / reference type problem cause between the difference between these two subview approaches. Since that seems like an interesting blog post topic, I spent a long time distilling the code down to a simple looking example of this phenomena.&lt;/p&gt;
&lt;p&gt;Then tried it, it worked correctly.&lt;/p&gt;
&lt;p&gt;Then changed the function to the struct, it worked correctly.&lt;/p&gt;
&lt;p&gt;Went back to the original app, changed the subview function to a struct, it worked correctly.&lt;/p&gt;
&lt;p&gt;So I don&amp;rsquo;t know what caused my original problem, or what changed to fix it. In theory I could go back through the commits (they have helpful names like &amp;ldquo;general progress&amp;rdquo; and &amp;ldquo;progress save&amp;rdquo;) and find it.&lt;/p&gt;
&lt;p&gt;But I&amp;rsquo;ve already spent way longer on this app than I should have, and all I have to show for it is this post about what a doofus I am.&lt;/p&gt;</description></item><item><title>Purple warning - "Publishing changes"</title><link>https://blog.iankulin.com/purple-warning-publishing-changes/</link><pubDate>Fri, 21 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/purple-warning-publishing-changes/</guid><description>&lt;p&gt;It&amp;rsquo;s a pretty safe bet that if Xcode is saying there&amp;rsquo;s an error in my code, that it&amp;rsquo;s correct, and I am in error - not Xcode. Today I came across a situation where that might not be true.&lt;/p&gt;
&lt;p&gt;I think the purple warnings are problems detected at runtime - I&amp;rsquo;ve heard of thread problems causing purple warnings. The error I was getting was &amp;ldquo;&lt;code&gt;Publishing changes from within view updates is not allowed, this will cause undefined behavior.&lt;/code&gt;&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The error was on two lines in my model where I&amp;rsquo;d called a method to update the model from a button press in a sub-view function.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-17-at-5.16.18-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;After poking around to try and work it out, I found &lt;a href="https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/"&gt;this clear blog post from Donny Wals&lt;/a&gt; and it turns out it&amp;rsquo;s possibly a bug in XCode 14. My situation fitted the description - calling the method from a button in a list, and the temporary workaround of adding a modifier to the button eliminated the warning.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m on Xcode 14.0, and I gather from Donny&amp;rsquo;s post that the problem might have been fixed in newer versions.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the calling code - a button in a list, with the &amp;ldquo;fix&amp;rdquo; on line 25.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;habitView&lt;/span&gt;(habitItem: HabitItem, habitsCollection: Habits) -&amp;gt; some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack(alignment: .leading) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(habitItem.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.headline)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(habitItem.lastDone.formatted(date: .abbreviated, time: .omitted))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34; (&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;habitItem.timesDone&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;)&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.largeTitle)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;habitsCollection.markAsDone(habit: habitItem) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Unexpected error - habit not found in collection:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;habitItem.name&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; habitItem.due {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;rectangle&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.system(size: &lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;checkmark.rectangle&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.system(size: &lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .buttonStyle(.bordered)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>List Apps</title><link>https://blog.iankulin.com/list-apps/</link><pubDate>Thu, 20 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/list-apps/</guid><description>&lt;p&gt;When I was first programming professionally, it wasn&amp;rsquo;t long before I noticed that there were patterns to the sort of bread-and-butter things I was writing most times - the majority of the small business applications I wrote tracked several entities; for each entity there needed to be add/edit/delete screens, there would be some business rules around those things, and some reports and search functionality.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/4/3/challenge"&gt;Day 47 Milestone app&lt;/a&gt; is for tracking habits - presented in a list; you need to be able to add and delete them, view details and do some business logic on them. Not only does this sound a lot like the earlier expense tracking app, but also not that different to the app idea I&amp;rsquo;ve got for tracking when I charge each of the rechargeable batteries in my house.&lt;/p&gt;
&lt;p&gt;Eventually these list type apps written in SwiftUI will need a better (perhaps MVVM) architecture, but at the simple end, it seems these apps are going to be some variation on this recipe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The item is going to be a struct with an id&lt;/li&gt;
&lt;li&gt;It will conform to Identifiable and Codable&lt;/li&gt;
&lt;li&gt;The collection of items will be a class that inherits from ObservableObject&lt;/li&gt;
&lt;li&gt;The collection property will be @Published&lt;/li&gt;
&lt;li&gt;The collection property will have an init() that reads the items from somewhere and a didset() that writes them&lt;/li&gt;
&lt;li&gt;The instance of that collection class will be an @StateObject&lt;/li&gt;
&lt;li&gt;The items will be shown as a List inside a NavigationView&lt;/li&gt;
&lt;li&gt;It will probably have a toolbar with a control to add items&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Patterns in development like this are convenient - they lead to high quality programs developed quickly, and being compliant with the &lt;a href="https://developer.apple.com/design/human-interface-guidelines/guidelines/overview/"&gt;Apple HIG&lt;/a&gt; means all the behaviours will be familiar to users as well as being accessible.&lt;/p&gt;
&lt;p&gt;The danger is that they look and feel like every other list type app, so developers need to think carefully about what all the common use cases are, as well as appealing design, to ensure the users&amp;rsquo; needs are being addressed and the application can justify it&amp;rsquo;s existence alongside all the other offerings.&lt;/p&gt;</description></item><item><title>Drawing Feedback</title><link>https://blog.iankulin.com/drawing-feedback/</link><pubDate>Wed, 19 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/drawing-feedback/</guid><description>&lt;p&gt;Here&amp;rsquo;s the summary of my learning from comparing &lt;a href="https://blog.iankulin.com/project-9-drawing/"&gt;my efforts&lt;/a&gt; with Paul&amp;rsquo;s solutions to the Project Nine challenges from &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/drawing-wrap-up"&gt;Day 46&lt;/a&gt; of his &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of SwiftUI course&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Create an &lt;code&gt;Arrow&lt;/code&gt; shape – having it point straight up is fine. This could be a rectangle/triangle-style arrow, or perhaps three lines, or maybe something else depending on what kind of arrow you want to draw.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Very similar solutions - Shape returning a Path, expect he finished off his with path.closeSubPath() rather than adding in the last line. That&amp;rsquo;s a bit neater, so that point to Paul.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Make the line thickness of your &lt;code&gt;Arrow&lt;/code&gt; shape animatable.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I did say in &lt;a href="https://blog.iankulin.com/project-9-drawing/"&gt;my solution&lt;/a&gt; that it seemed too simple, and I was expecting to have to use AnimatableData, and that was correct. Paul does not mean animating the &lt;em&gt;line&lt;/em&gt; &lt;em&gt;thickness&lt;/em&gt;, he means the &lt;em&gt;shaft width&lt;/em&gt; of the arrow - though I can see how they&amp;rsquo;re sort of the same thing. I wish he had his website code on GitHub and I would have fixed it for him, along with an error I&amp;rsquo;d spotted in one of the earlier lessons.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/e3GAMZAVqus?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;In any case, Paul&amp;rsquo;s fix was as per the earlier lesson.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Create a &lt;code&gt;ColorCyclingRectangle&lt;/code&gt; shape that is the rectangular cousin of &lt;code&gt;ColorCyclingCircle&lt;/code&gt;, allowing us to control the position of the gradient using one or more properties.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was just a matter of a couple of edits to the ColorCyclingCircle() struct Paul had created in one of the lessons to change the shape and to pass in values so the gradient could be manipulated. Although our solutions where similar, Paul&amp;rsquo;s was a bit more thorough - controlling the start and end points of the gradient.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m interested that Paul uses the US spelling for colour. I&amp;rsquo;ve wondered about that in my code - of course Apple spell it that way in all the Swift source, so it does look odd if you stick to the UK spelling, and I guess the majority of people who learn English as a second language probably learn American spellings.&lt;/p&gt;</description></item><item><title>Project 9 - Drawing</title><link>https://blog.iankulin.com/project-9-drawing/</link><pubDate>Tue, 18 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-9-drawing/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-16-at-12.17.46-pm.jpg" alt="Screenshot of Xcode and the preview showing some fancy graphics"&gt;&lt;/p&gt;
&lt;p&gt;These few days of &lt;a href="https://www.hackingwithswift.com/100/swiftui/43"&gt;#100DaysOfSwiftUI&lt;/a&gt; we made some pretty shapes by playing around with some of the SwiftUI systems for drawing on the screen, including paths, shapes, transformations, ImagePaint, drawingGroup() to use Metal rendering, blurs, blend modes and using animatableData for animating - which I think is the solution to an animation problem in my TimesTable app I hadn&amp;rsquo;t been able to solve yet.&lt;/p&gt;
&lt;p&gt;The challenges were:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Create an &lt;code&gt;Arrow&lt;/code&gt; shape – having it point straight up is fine. This could be a rectangle/triangle-style arrow, or perhaps three lines, or maybe something else depending on what kind of arrow you want to draw.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Make the line thickness of your &lt;code&gt;Arrow&lt;/code&gt; shape animatable.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Create a &lt;code&gt;ColorCyclingRectangle&lt;/code&gt; shape that is the rectangular cousin of &lt;code&gt;ColorCyclingCircle&lt;/code&gt;, allowing us to control the position of the gradient using one or more properties.&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h4 id="arrow-shape"&gt;Arrow shape&lt;/h4&gt;
&lt;p&gt;Nothing too tricky here - define the Arrow as a Shape, then build the part for it. The rect in path has yZero at the top.&lt;/p&gt;
&lt;h4 id="animate-line-thickness"&gt;Animate line thickness&lt;/h4&gt;
&lt;p&gt;I was expecting to have to use animatableData in the challenges, so perhaps I&amp;rsquo;ve misunderstood the brief here&lt;/p&gt;
&lt;h4 id="colorcycling-rectangle"&gt;ColorCycling Rectangle&lt;/h4&gt;
&lt;p&gt;I locked in the start point for the gradient, then let the user manipulate the end point with two sliders. I wanted a vertical slider, but it wasn&amp;rsquo;t as simple as rotating a slider so I abandoned that as not worth the time investment for the present.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContentView&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lineWidth = &lt;span style="color:#ae81ff"&gt;10.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; xAmount = &lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @State &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; yAmount = &lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; endPoint = UnitPoint(x: xAmount, y: yAmount)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ColorCyclingRectangle(startPoint: .top, endPoint: endPoint)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Slider(value: &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;xAmount)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding(.horizontal)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Slider(value: &lt;span style="color:#960050;background-color:#1e0010"&gt;$&lt;/span&gt;yAmount)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding(.horizontal)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Divider()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Arrow()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .stroke(.red, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .frame(width: &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;, height: &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .animation(.linear(duration: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;), value: lineWidth)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding(.vertical)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Line width: &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;5&amp;#34;&lt;/span&gt;) {lineWidth = &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;10&amp;#34;&lt;/span&gt;) {lineWidth = &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;25&amp;#34;&lt;/span&gt;) {lineWidth = &lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&lt;span style="color:#e6db74"&gt;&amp;#34;50&amp;#34;&lt;/span&gt;) {lineWidth = &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding(.vertical)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ColorCyclingRectangle&lt;/span&gt;: View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; startPoint: UnitPoint
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; endPoint: UnitPoint
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; body: some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ZStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(&lt;span style="color:#ae81ff"&gt;0.&lt;/span&gt;.&amp;lt;&lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;) { value &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Rectangle()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .inset(by: Double(value))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .strokeBorder(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LinearGradient(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gradient: Gradient(colors: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; color(&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;: value, brightness: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; color(&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;: value, brightness: &lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; startPoint: startPoint,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; endPoint: endPoint
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lineWidth: &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .drawingGroup()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;color&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; value: Int, brightness: Double) -&amp;gt; Color {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; targetHue = Double(value) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; Double(&lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;) &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; targetHue &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; targetHue &lt;span style="color:#f92672"&gt;-=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Color(hue: targetHue, saturation: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, brightness: brightness)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Arrow&lt;/span&gt;: Shape {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; rect: CGRect) -&amp;gt; Path {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; path = Path()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// follow the points from the tip around clockwise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.move(to: CGPoint(x: rect.midX, y: rect.minY))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.addLine(to: CGPoint(x: rect.maxX&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, y: rect.maxY&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.addLine(to: CGPoint(x: rect.maxX&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, y: rect.maxY))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.addLine(to: CGPoint(x: rect.maxX&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, y: rect.maxY))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.addLine(to: CGPoint(x: rect.maxX&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, y: rect.maxY&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; path
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As this wasn&amp;rsquo;t a real project - just scratch code of various techniques - I hadn&amp;rsquo;t worried about a git repo for it, but twice that came back to bite me when I wanted to go abandon an approach and pick up at an earlier point. That&amp;rsquo;s a lesson for me.&lt;/p&gt;</description></item><item><title>When it Works</title><link>https://blog.iankulin.com/when-it-works/</link><pubDate>Mon, 17 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/when-it-works/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-15-at-10.43.18-am.jpg" alt="Screenshot of swiftui code and the iphone simulator with a roughly drawn face"&gt;&lt;/p&gt;
&lt;p&gt;The little joy of something working. It&amp;rsquo;s one of the things that makes coding enjoyable. Like a good video game you have an overarching goal, but on the way you need to solve a large number of problems of variable complexity, and you get a little bit of dopamine for each one.&lt;/p&gt;
&lt;p&gt;I think in every language I&amp;rsquo;ve ever learned, as soon as I know how to draw something on the screen, I start to get the urge to create a simple drawing application. When I was starting on Visual C++ and the MFC (Microsoft Foundation Classes) the &lt;a href="https://www.amazon.com/Beginning-Visual-C-Ivor-Horton/dp/1861000081"&gt;book&lt;/a&gt; I used to get started built a drawing application as an example. It hooks into the benefit of being able to quickly see the evidence you&amp;rsquo;ve achieved something.&lt;/p&gt;</description></item><item><title>Musings on Data</title><link>https://blog.iankulin.com/musings-on-data/</link><pubDate>Sun, 16 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/musings-on-data/</guid><description>&lt;p&gt;I&amp;rsquo;ve been feeling my enthusiasm for the online courses I started waning a little, additionally, I have enough progress under my belt that I could actually start working on some of the projects in my &amp;ldquo;App Ideas&amp;rdquo; notebook. I&amp;rsquo;m not sure if starting on them is a smart idea, or just a way of procrastinating. One thing most of those apps have in common, and that I haven&amp;rsquo;t substantively learned yet is persisting data. Their data requirements vary from a sort of specialised todo list only required on that device, to relational data that needs to be live synced across devices including macOS and needs to support rolling back transactions to resolve conflicts.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://blog.iankulin.com/iexpense-challenges/"&gt;iExpense&lt;/a&gt; app from the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/archiving-swift-objects-with-codable"&gt;100 Days course&lt;/a&gt; included encoding data to JSON then saving it in UserDefaults, but that&amp;rsquo;s the extent of my data expertise so far. And, that would be fine for a prototype version of a couple of the app ideas for now.&lt;/p&gt;
&lt;p&gt;I know these things exist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Core Data (and, I imagine CloudKit for saving the SQLite in iCloud)&lt;/li&gt;
&lt;li&gt;FireBase - the Google database engine that seems popular for serious apps that need to share data&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s possible to just save files in the apps sandbox (which I don&amp;rsquo;t know how to do, but sounds a bit more legit that using UserDefaults)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In programming terms, I grew up on large relational databases - on disk. RAM used to be much more precious and capricious, and saving a file much slower - so I have some thinking adjustments to do. For my Todo type app that&amp;rsquo;s not likely to have more than 100 records, it&amp;rsquo;s probably legit to just encode the whole thing to JSON and write it to a file for every add/edit/delete.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure what I&amp;rsquo;ll do - the logical thing is to buckle down and work through the 100 days until I get to databases in the tutorials, but if working on apps is more engaging and leads to more programming that could be better. My jobby-job was particularly demanding this week, so perhaps the weekend will fix this problem for me.&lt;/p&gt;</description></item><item><title>Moonshot Feedback</title><link>https://blog.iankulin.com/moonshot-feedback/</link><pubDate>Sat, 15 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moonshot-feedback/</guid><description>&lt;p&gt;I&amp;rsquo;ve watched Paul&amp;rsquo;s solution to the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/moonshot-wrap-up"&gt;Moonshot challenges&lt;/a&gt; (the solutions are one of the perks of being a Hacking With Swift subscriber). When I&amp;rsquo;m solo learning like this its one of the few ways I can get any feedback on my coding, so I highly value it, and usually write one of these posts as a way to ensure I reflect on it.&lt;/p&gt;
&lt;p&gt;The second challenge was to pull out a couple of sub-views, Paul had used a struct as I had, but put them in their own files. I think that&amp;rsquo;s good practice if those sections are going to be used from other views, otherwise I like them in the the file with the view they&amp;rsquo;re a part of. I guess if you make it a habit pull them out into files, you&amp;rsquo;d look there for them so it would not be a drama, but XCode is pretty handy for finding what you want in a reasonable size file, so that&amp;rsquo;s not a big consideration.&lt;/p&gt;
&lt;p&gt;The challenge I was really interested to see was his use of a List for the &amp;ldquo;list&amp;rdquo; version of the main view. Although his list looked a bit nicer when he&amp;rsquo;d finished, that&amp;rsquo;s more of a comment on the effort I put into the design (minimal - I just made a list version of the grid style). The List does detect that it&amp;rsquo;s inside a NavigationView and has those &amp;gt; signs at the end of each row - so there&amp;rsquo;s a visual prompt to the user that these are tappable for more. In my version, I&amp;rsquo;d just kept the existing scroll view, deleted the grid and used HStacks to build out the view for each mission.&lt;/p&gt;
&lt;p&gt;Lists do have a heap of features - checkboxes, multiple selection, pull down for refresh, left slide for delete and all sorts of other goodies. However, none of those are needed for this app, so in that respect my solution is fine.&lt;/p&gt;
&lt;p&gt;There is however, a generally important reason for using the standard iOS controls in the way they are intended - accessibility. There&amp;rsquo;s a good example of that a few minutes later when Paul adds his toolbar to the NavigationView. Here&amp;rsquo;s mine - succinct, nice use of the ternary operator:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.toolbar {
 Image(systemName: showingList ? &amp;#34;square.grid.2x2&amp;#34; : &amp;#34;list.bullet&amp;#34;)
 .onTapGesture {
 showingList.toggle()
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&amp;rsquo;s Paul&amp;rsquo;s:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;. toolbar { 
 Button {
 showingGrid.toggle()
} label: {
 if showingGrid
 {
 Label(&amp;#34;Show as table&amp;#34;, systemImage: &amp;#34;list.dash&amp;#34;)
 } else {
 Label(&amp;#34;Show as grid&amp;#34;, systemImage: &amp;#34;square.grid.2x2&amp;#34;)
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Essentially the same, except he&amp;rsquo;s used a Button rather than an Image with an .onTapGesture. You may ask why that matters - the answer is that if you were blind and using the iOS screen reader it would matter a lot. With Paul&amp;rsquo;s version the reader would know it was a button - something for collecting user input, AND it would read out the purpose contained in the label.&lt;/p&gt;
&lt;p&gt;Apple deserve credit for the effort and thought put into their accessibility features, and as developers we get a lot of that functionality for free, or at least very cheaply - but only if we do things the Apple way with standard controls.&lt;/p&gt;</description></item><item><title>Moonshot Challenges</title><link>https://blog.iankulin.com/moonshot-challenges/</link><pubDate>Fri, 14 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moonshot-challenges/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-09-at-2.00.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-09-at-2.00.26-pm.png" width="269" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Another few coding challenges at the end of a tutorial app in the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/moonshot-wrap-up"&gt;100 Days of SwiftUI&lt;/a&gt; course. The app is a sort of information app - composed of navigation views going down into more detail about the Apollo space missions. The most exciting revelation for me was how straightforward it is to pull JSON into your apps data structures.&lt;/p&gt;
&lt;h4 id="challenge-1"&gt;Challenge 1&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Add the launch date to &lt;code&gt;MissionView&lt;/code&gt;, below the mission badge. You might choose to format this differently given that more space is available, but it’s down to you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Adding a text view, but also another computed property to the Mission type to retrieve a longer version of the date string.&lt;/p&gt;
&lt;h4 id="challenge-2"&gt;Challenge 2&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Extract one or two pieces of view code into their own new SwiftUI views – the horizontal scroll view in &lt;code&gt;MissionView&lt;/code&gt; is a great candidate, but if you followed my styling then you could also move the &lt;code&gt;Rectangle&lt;/code&gt; dividers out too.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One of the &lt;a href="https://blog.iankulin.com/tags/swiftlint/"&gt;SwiftLint&lt;/a&gt; rules is about the length of views, and I recall Paul Hegarty saying something in the first couple of CS193p lectures about views needing to be kept short. I never know the pros and cons of extracting parts of them as a function or as another struct. And if I do extract them as a struct, it always seems odd to me to just be able to &amp;ldquo;call&amp;rdquo; a struct exactly as a function - although since this is declarative programing and we&amp;rsquo;re just describing a view I guess it&amp;rsquo;s fine.&lt;/p&gt;
&lt;p&gt;For this change, I pulled it out the crew view and a custom divider as structs. I had to move a baby data struct out of the MissionView namespace to make it available to the new view. An alternative have been keeping the new struct also in the main view, but I don&amp;rsquo;t love doing that too much. The trade off here was the potential advantages of reusing the new sub-view somewhere else, ease of testing and shorter views, vs polluting the namespace and putting things in the global app scope that do not need to be there. Deciding factor for me in this case was just ease of understanding the code.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/MoonShot/commit/1e5c84ccc4692df06026425684831fab60300215"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="challenge-3"&gt;Challenge 3&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;For a tough challenge, add a toolbar item to &lt;code&gt;ContentView&lt;/code&gt; that toggles between showing missions as a grid and as a list.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s some combination of unsettling and pleasing if Paul thinks something should be hard and I knock it out quickly. I never know if I&amp;rsquo;ve missed some crucial point or I&amp;rsquo;m just getting the hang of things.&lt;/p&gt;
&lt;p&gt;For this one I pulled out all the code that showed the grid inside a ScrollView(), duplicated it and edited the new one into a &amp;ldquo;list&amp;rdquo; by deleting the enclosing GridView(), changing a couple of VStacks to HStacks and tweaking some sizes. I added a @State variable for showing the grid, and a SF Symbol to the toolbar to swap between the views.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve previously been advised to avoid an if statement to chose between two views as a state changes since it causes the new view to be created (rather than a ternary operator on a modifier or similar), but there&amp;rsquo;s no real way of avoiding it here - the user is literally asking us for a new view - so that goes into the main view where I extracted the other code from.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/MoonShot/commit/9faa8c5fe99628264fef184112685365330e60fd"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I read down into the hints after completing this, Paul talks about some gotchas with List() - which I hadn&amp;rsquo;t used, so I&amp;rsquo;ll be interested to see his wrap up to see if a List would have been better than my solution.&lt;/p&gt;</description></item><item><title>Using Swift's map</title><link>https://blog.iankulin.com/using-swifts-map/</link><pubDate>Thu, 13 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-swifts-map/</guid><description>&lt;p&gt;In &lt;a href="https://www.hackingwithswift.com/100/swiftui/39"&gt;Day 39&amp;rsquo;&lt;/a&gt;s Moonshot tutorial app, Paul uses &lt;a href="https://developer.apple.com/documentation/swift/array/map(_:)-87c4d"&gt;&lt;code&gt;.map&lt;/code&gt;&lt;/a&gt; on an array without much comment about what&amp;rsquo;s going on. I assume this might be a common concept in modern languages, but it was new to me.&lt;/p&gt;
&lt;p&gt;First, here&amp;rsquo;s Paul&amp;rsquo;s code&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;(mission: Mission, astronauts: [String: Astronaut]) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.mission = mission
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;self&lt;/span&gt;.crew = mission.crew.map { member &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; astronaut = astronauts[member.name] {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; CrewMember(role: member.role, astronaut: astronaut)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fatalError(&lt;span style="color:#e6db74"&gt;&amp;#34;Missing &lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;member.name&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Mission&lt;/code&gt; here contains an array of &lt;code&gt;crew&lt;/code&gt; which is a struct with two strings, one of them being &lt;code&gt;name&lt;/code&gt;, but &lt;code&gt;self.crew&lt;/code&gt; (which belongs to the view we&amp;rsquo;re in) is an array of &lt;code&gt;CrewMember&lt;/code&gt; which is a struct with a &lt;code&gt;role: String&lt;/code&gt; and another struct &lt;code&gt;astronaut&lt;/code&gt;. That sounds confusing, but essentially, an array of one type is being processed to return an array of another type where there&amp;rsquo;s a 1:1 relationship between the elements of each array.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what &lt;code&gt;map&lt;/code&gt; does - it works through a collection, and uses the code in a closure to act on each element to transform it, then adds it to the array to be returned.&lt;/p&gt;
&lt;p&gt;Let me try to make it clearer with a simpler example. Time to open a Playground. Let&amp;rsquo;s map an array of integers into an array of strings:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let intArray = [0, 55, 21, 13]
var stringArray = [String]()

stringArray = intArray.map{ &amp;#34;\($0)&amp;#34;}

print(stringArray)
// [&amp;#34;0&amp;#34;, &amp;#34;55&amp;#34;, &amp;#34;21&amp;#34;, &amp;#34;13&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;$0 is a stand in for each element. This is a common Swift feature but this variable can be named for clarity if desired:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let intArray = [0, 55, 21, 13]
var stringArray = [String]()

stringArray = intArray.map{ number in &amp;#34;\(number)&amp;#34;}

print(stringArray)
// [&amp;#34;0&amp;#34;, &amp;#34;55&amp;#34;, &amp;#34;21&amp;#34;, &amp;#34;13&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we didn&amp;rsquo;t have map, we could do it the long way, something like this.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let intArray = [0, 55, 21, 13]
var stringArray = [String]()

intArray.forEach {
 stringArray.append(&amp;#34;\($0)&amp;#34;)
}

print(stringArray)
// [&amp;#34;0&amp;#34;, &amp;#34;55&amp;#34;, &amp;#34;21&amp;#34;, &amp;#34;13&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or, going back even further in time:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let intArray = [0, 55, 21, 13]
var stringArray = [String]()

for number in intArray {
 stringArray.append(&amp;#34;\(number)&amp;#34;)
}

print(stringArray)
// [&amp;#34;0&amp;#34;, &amp;#34;55&amp;#34;, &amp;#34;21&amp;#34;, &amp;#34;13&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So the .map method on a sequence returns an array of values built by applying to the passed closure to each element of the sequence.&lt;/p&gt;
&lt;p&gt;Paul&amp;rsquo;s code at the top steps through the mission.crew array which contains the crews names as strings, then attempts to look them up in the astronauts dictionary. If it finds them, it adds them (as a struct) to the crew array.&lt;/p&gt;</description></item><item><title>iExpense Feedback</title><link>https://blog.iankulin.com/iexpense-feedback/</link><pubDate>Wed, 12 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/iexpense-feedback/</guid><description>&lt;p&gt;I finally got around to looking at Paul&amp;rsquo;s solutions for the &lt;a href="https://blog.iankulin.com/iexpense-challenges/"&gt;iExpense challenges&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Use the user’s preferred currency, rather than always using US dollars.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Same approach as me,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.currency(code: Locale.current.currency?.identifier ?? &amp;quot;USD&amp;quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;except that he does the work in a local variable which is a bit neater. Since it appears in two places - the display view and the view for adding an expense, this would mean duplicating it, making it global (not rare for user default type settings) or passing it down through the view hierarchy. I feel either of the first two options are fine for this project, but Paul is thorough and extends the FormatStyle protocol, only for currency, to have a new computed property .localcurrency - which is a great solution.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Modify the expense amounts in &lt;code&gt;ContentView&lt;/code&gt; to contain some styling depending on their value – expenses under $10 should have one style, expenses under $100 another, and expenses over $100 a third style. What those styles are depend on you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;d done this with ternary operator modifiers:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; Text(item.amount, format: .currency(code: Locale.current.currency?.identifier ?? &amp;#34;USD&amp;#34;))
 }.foregroundColor((item.amount &amp;lt; 10) ? .purple : (item.amount &amp;lt; 100) ? .green : .blue)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But Paul, again, goes one better with another extension, this time of View, to create a style(for: Item) - this is a better approach, especially if (as would be likely in a real app) it was going to need to be reused.&lt;/p&gt;
&lt;p&gt;I also note that Paul added new files for both these extensions. I think that&amp;rsquo;s wise for the currency one, but perhaps this one could probably have lived wherever the ExpenceItem was defined.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;For a bigger challenge, try splitting the expenses list into two sections: one for personal expenses, and one for business expenses. This is tricky for a few reasons, not least because it means being careful about how items are deleted!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is where things get weird. I split the types up by running to ForEach in their own groups. Paul had warned that deleting would be tricky because the offset into the array would not necessarily match the offset being passed to the remove method - so the wrong one might be deleted. I implemented my solution without worrying about that and it seemed to work, so I assumed I&amp;rsquo;d just come up with a better solution and moved on. But it does bear thinking about. Say (starting from no records) I create two business expenses names &lt;em&gt;Bus1&lt;/em&gt; &amp;amp; &lt;em&gt;Bus 2&lt;/em&gt;, and then two personal expenses names &lt;em&gt;Per 1&lt;/em&gt; and &lt;em&gt;Per 2&lt;/em&gt;. My array of expense objects is going to look a bit like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Bus 1 [0]
Bus 2 [1]
Per 1 [2]
Per 2 [3]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But my code is going to list the personal expenses first, then the business ones:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Section(header: Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Personal&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(expenses.items) { item &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Group {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; item.type == &lt;span style="color:#e6db74"&gt;&amp;#34;Personal&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; itemView(item: item)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .onDelete(perform: removeItems)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Section(header: Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Business&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(expenses.items) { item &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Group {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; item.type == &lt;span style="color:#e6db74"&gt;&amp;#34;Business&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; itemView(item: item)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .onDelete(perform: removeItems)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-08-at-3.06.30-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-08-at-3.06.30-pm.png" width="283" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The numbers in the brackets are what I think their array indexes should be. If what I understand Paul to be saying, if I now try to delete &lt;em&gt;Per 2,&lt;/em&gt; its offset in the list would be 1 (the second item in the list), but the remove call to the array is going to delete the expense at index 1, which is actually &lt;em&gt;Bus 2&lt;/em&gt;. But actually, it just deletes the correct one:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-08-at-3.14.51-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-08-at-3.14.51-pm.png" width="736" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-08-at-3.14.56-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-08-at-3.14.56-pm.png" width="734" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Perplexed, now that I&amp;rsquo;m thinking carefully about it. I did some print statement debugging:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;removeItems&lt;/span&gt;(at offsets: IndexSet) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; i &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; offsets {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Offset:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;\(&lt;/span&gt;i&lt;span style="color:#e6db74"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Before remove&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; item &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; expenses.items {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(item.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; expenses.items.remove(atOffsets: offsets)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;After remove&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; item &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; expenses.items {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(item.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which outputs this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Offset:3
Before remove
Bus 1 [0]
Bus 2 [1]
Per 1 [2]
Per 2 [3]
After remove
Bus 1 [0]
Bus 2 [1]
Per 1 [2]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So actually, the IndexSet that SwiftUI passes to .onDelete() has already correctly identified the correct array index (shown as &amp;ldquo;Offset: 3&amp;rdquo; above). I assume as it&amp;rsquo;s built the List for me. That&amp;rsquo;s lovely, but I was perplexed about what Paul is talking about then.&lt;/p&gt;
&lt;p&gt;In Paul&amp;rsquo;s solution, he adds computed properties to the Expenses class to return arrays of either personal or business expenses. he does this with the arrays filter() method. This is a cool method (which I didn&amp;rsquo;t know about) but it returns a new array with elements meeting the filter condition. So it makes sense that when you create the index set from this, it&amp;rsquo;s indexed into the new smaller (filtered) array, so of course you need to account for this.&lt;/p&gt;
&lt;p&gt;In that case, this might be the first occasion when I think my solution is better than Paul&amp;rsquo;s!&lt;/p&gt;</description></item><item><title>Using AI to Generate Icons</title><link>https://blog.iankulin.com/using-ai-to-generate-icons/</link><pubDate>Tue, 11 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-ai-to-generate-icons/</guid><description>&lt;p&gt;Since I have minimal design skills, I went back to &lt;a href="https://www.fiverr.com/"&gt;Fiverr&lt;/a&gt; (the digital gig economy platform) to get some icons done for CodeTrimmer - explaining that I wanted something like a &amp;ldquo;pair of scissors floating over some computer code&amp;rdquo;. At the same time I&amp;rsquo;ve been playing with &lt;a href="https://github.com/divamgupta/diffusionbee-stable-diffusion-ui"&gt;DiffusionBee&lt;/a&gt; - a free Apple silicon version of the &lt;a href="https://stability.ai/blog/stable-diffusion-public-release"&gt;Stable Diffusion&lt;/a&gt; artifical intellligence that generates images from text prompts. The image above was created on an M1 Macbook using DiffusionBee.&lt;/p&gt;
&lt;p&gt;Since generating an image from a text prompt is exactly what I&amp;rsquo;d just done with Fiverr, I thought I&amp;rsquo;d give the AI a chance. Even if the results are not great, they will do for the toy applications I&amp;rsquo;m creating as I&amp;rsquo;m learning.&lt;/p&gt;
&lt;p&gt;The model in Stable Diffusion is trained on images scraped from the internet, so it&amp;rsquo;s strong on pop culture and painting styles. For instance &amp;ldquo;Jack Black as Aquaman&amp;rdquo; is likely to give good results, also &amp;ldquo;Kermit dressed as Yoda in front of a sunset landscape&amp;rdquo;. The license for the outputs of the AI is quite permissive, including for commercial purposes as long as it&amp;rsquo;s not used for evil (that&amp;rsquo;s my paraphrasing, the actual &lt;em&gt;CreativeML Open RAIL-M&lt;/em&gt; license is &lt;a href="https://github.com/CompVis/stable-diffusion/blob/main/LICENSE"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a couple of outputs for the different versions of the &amp;ldquo;scissors floating over code, macOS, icon&amp;rdquo; prompt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-05-at-10.56.41-am.jpg" alt="screen cap of stable diffusion output"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-05-at-10.35.25-am.jpg" alt="screen cap of stable diffusion output"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/kermit-dressed-as-yoda-in-front-of-a-sunset-landscape.jpg" alt="Kermit dressed as Yoda - Stable Diffusion"&gt;&lt;/p&gt;
&lt;p&gt;Like any tool, there is a skill to using it well, and with image generation from text prompts, the skill is in creating the prompts, hence a sudden plethora of articles about this all over the internet, and even a &lt;a href="https://promptbase.com/"&gt;market&lt;/a&gt; for them. So perhaps with a bit more skill I&amp;rsquo;d get something closer to what I&amp;rsquo;d like, although if you are prepared to accept something a bit abstract a couple of these would be fine.&lt;/p&gt;
&lt;p&gt;If I had a small amount of design skills these images might serve as a great creative thinking starting point - for example I like the criss-crossy background of one to represent code. I like the drop shadow and flat green of the ipod shuffle one - so these ideas could be combined to make a pair of flat green handled scissors floating over the lined background.&lt;/p&gt;
&lt;p&gt;If you are not allowed to use AI&amp;rsquo;s for evil (I&amp;rsquo;m looking at you &lt;a href="https://en.wikipedia.org/wiki/Skynet_(Terminator)"&gt;Skynet&lt;/a&gt;), it seems like using them as a tool for creating okay images for apps and other digital content is likely to become a practical use for them.&lt;/p&gt;</description></item><item><title>SwiftLint</title><link>https://blog.iankulin.com/swiftlint/</link><pubDate>Mon, 10 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swiftlint/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2022-10-04-at-08-30-59-code-complete-mcconnell-steve-amazon.com_.au-books.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I was watching a &lt;a href="https://www.techwithtim.net/"&gt;Tim Ruscica&lt;/a&gt; &lt;a href="https://www.youtube.com/watch?v=wJNikDr-aNM"&gt;video&lt;/a&gt; about the things that highly effective developers do, and it called to mind a book I read years ago called &lt;a href="https://www.amazon.com.au/Code-Complete-Steve-McConnell/dp/0735619670"&gt;Code Complete&lt;/a&gt;. It is the only book I ever owned that I immediately purchased the new edition when it came out. It was about the meta stuff around programming that is the difference between coding and developing. In particular, it got me invested in source control and testing.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been reading along, you&amp;rsquo;ll know I am keen to leverage the great tools available to support quality software development, and one I haven&amp;rsquo;t tackled till this week is a linter.&lt;/p&gt;
&lt;p&gt;Linters are tools to enforce (or at least suggest) rules to improve your code that are not strictly necessary (the compiler hasn&amp;rsquo;t enforced them) but they make your code better, or at least prettier, in other ways. I gather the most popular linter for Swift is &lt;a href="https://github.com/realm/SwiftLint"&gt;SwiftLint&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I installed it by downloading and running the &lt;a href="https://github.com/realm/SwiftLint/releases/download/0.49.1/SwiftLint.pkg"&gt;.pkg&lt;/a&gt;. Then in any Xcode projects you want to use it in, you go into &lt;em&gt;Build Phases&lt;/em&gt; for the current &lt;em&gt;Scheme&lt;/em&gt; in your project and add a new script that runs SwiftLint on the files in the project&amp;rsquo;s folder. There&amp;rsquo;s a good step by step &lt;a href="https://medium.com/developerinsider/how-to-use-swiftlint-with-xcode-to-enforce-swift-style-and-conventions-368e49e910"&gt;here&lt;/a&gt; by Vineet Choudhary. You should end up with something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-04-at-8.39.34-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now when you Command-B to build your project, the linter will run and (especially the first time) add some extra warnings and errors.&lt;/p&gt;
&lt;p&gt;SwiftLint has many rules. Some are default rules (these are the most widely accepted ones) and some are optional. Turning rules off or on, or altering their parameters is done using a &lt;code&gt;swiftlint.yml&lt;/code&gt; config file in the top level folder of your project.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example from &lt;a href="https://github.com/IanKulin/dotfiles/blob/main/.swiftlint.yml"&gt;mine&lt;/a&gt;, where I want to alter a rule:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;vertical_whitespace:
 max_empty_lines: 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s a rule called vertical_whitespace. It throws a warning if there is more than one consecutive empty line in a file. I use two line gaps all over my code to signify a separate method or function, so that does not work for me. The config change above changes it to allow two empty lines. This same config file can also be used to disable any of the default rules, or enable any of the optional rules.&lt;/p&gt;
&lt;p&gt;Rules can also be disabled and enabled in your code with special comments. For example, in my tests, I have enormous multi-line strings which don&amp;rsquo;t follow proper indentation for a deliberate reason, so at the top of this file I have:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;// swiftlint:disable line_length
// swiftlint:disable indentation_width
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can turn rules back on as well. For example in this code snippet I have code that the linter wants me to write as a trailing closure, but I&amp;rsquo;m not sure how to do that in this situation yet - so I turn the rule off before it, then back on.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;stripSpaces&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;_&lt;/span&gt; codeLines: String ) -&amp;gt; String {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// break into lines&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; lines = codeLines.components(separatedBy: &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; minCount = Int.max
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// step though the lines and count how many spaces, save the minimum amount&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; line &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; lines {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// swiftlint:disable trailing_closure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; leadingSpaces = line.&lt;span style="color:#66d9ef"&gt;prefix&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;while&lt;/span&gt;: { $0 == &lt;span style="color:#e6db74"&gt;&amp;#34; &amp;#34;&lt;/span&gt; }).count
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// swiftlint:enable trailing_closure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; minCount = leadingSpaces &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; minCount ? leadingSpaces : minCount
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// step though the lines again, and trim the min amount from each line&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; index &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.&lt;/span&gt;.&amp;lt;lines.count {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lines[index] = String(lines[index].dropFirst(minCount))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// stitch it back up&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; lines.joined(separator: &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My strategy with the linter was to turn all the optional rules on, then run it and go through to consider each of the optional rules I&amp;rsquo;ve transgressed to decide if it makes more sense to me to change the code or eliminate the rule.&lt;/p&gt;</description></item><item><title>Testing, testing</title><link>https://blog.iankulin.com/testing-testing/</link><pubDate>Sun, 09 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/testing-testing/</guid><description>&lt;p&gt;I have unit testing in my goals, and if I&amp;rsquo;m going to throw this &lt;a href="https://blog.iankulin.com/codetrimmer-first-macos-app/"&gt;space trimming&lt;/a&gt; macOS utility up on the web, now might be a good time to figure out how to add unit tests to a project, how to write them, and how to run them. XCode is well set up for this, so it&amp;rsquo;s really no drama to do.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-9.09.32-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Although I haven&amp;rsquo;t worried with any unit testing up to this point in my iOS/Swift learning, in my previous programming work I did a lot of work with the large calculations involved in translating GPS coordinates and robotic positioning models where errors would be bad - so I&amp;rsquo;ve written a lot of tests over the years. I&amp;rsquo;ve also definitely felt the confidence you can dramatically refactor code with when you know the code has a robust test suite. I&amp;rsquo;m a big fan.&lt;/p&gt;
&lt;h4 id="adding-tests-to-existing-project"&gt;Adding Tests to Existing Project&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-9.28.39-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-9.28.39-pm.png" width="239" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In Xcode, with your project open, you need to add the &lt;em&gt;testing target&lt;/em&gt;. &lt;code&gt;File | New | Target...&lt;/code&gt; then scroll down to find the &lt;em&gt;Unit Testing Bundle&lt;/em&gt;. When you add that, you&amp;rsquo;ll see a new folder in the Project Navigator, and a new source file - both with the name &lt;code&gt;&amp;lt;app name&amp;gt;Tests&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you click into the &lt;code&gt;&amp;lt;app name&amp;gt;Test.swift&lt;/code&gt; file, you&amp;rsquo;ll see a couple of methods for setting up the testing environment and cleaning up after it. They are called before and after each test - you&amp;rsquo;d use them if you needed to set something up like some files in a directory or similar and wanted to ensure it was not affected by the tests run before the current test. Don&amp;rsquo;t worry about them for the time being.&lt;/p&gt;
&lt;h4 id="writing-tests"&gt;Writing Tests&lt;/h4&gt;
&lt;p&gt;Before we can write any tests, we need to import any modules with the code we want to test. In my case, I want to test the function stripSpaces() which is in the ContentView of my app called CodeTrimmer. So in the top of the CodeTrimmerTests.swift file, under the other import, I add the import:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import XCTest
@testable import CodeTrimmer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we can write our tests. The test functions must start with the magic prefix &lt;em&gt;test&lt;/em&gt; so that Xcode treats them as such. The usual approach in the test is to call a function with some specific input, then test the output is what is expected with a special test version of assert():&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a couple. My function stripSpaces() deletes a number of spaces from each line of a block of code such that the block becomes left aligned. if the XCTAsserts() pass, the test passes and I get a green tick. To run the tests I just click in the breakpoint gutter next to the function declaration.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;testStripSpaces01&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; testString =
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; single line string no spaces
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; resultString = stripSpaces(testString)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; XCTAssertTrue(resultString == testString, testString)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;testStripSpaces02&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;throws&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; testString =
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; single line 4 spaces
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; resultString = stripSpaces(testString)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; XCTAssertFalse(resultString == testString, testString)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; XCTAssertTrue(testString.dropFirst(&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;) == resultString, testString)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Where's My App?</title><link>https://blog.iankulin.com/wheres-my-app/</link><pubDate>Sat, 08 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/wheres-my-app/</guid><description>&lt;p&gt;The iOS apps I&amp;rsquo;ve been making, can only run in the simulator or on my tethered device (which I haven&amp;rsquo;t actually tried yet), but the MacOS app I made today, in theory could be zipped up and distributed to the world from my website. At the very least, I wanted to drop it into my Applications folder so I could use it, so I needed to find the .app &amp;ldquo;file&amp;rdquo;, and realised I had no idea where it was. If that&amp;rsquo;s your situation, then here&amp;rsquo;s the steps you need.&lt;/p&gt;
&lt;p&gt;First of all, you are currently building debug versions of your app. We need to create a new &lt;em&gt;Scheme&lt;/em&gt; for the release build. In Xcode use the menus to go to Product | Scheme | New Scheme&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-1.51.03-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Give it a sensible name - maybe &lt;code&gt;&amp;lt;app name&amp;gt; Release&lt;/code&gt; or similar. Then Product | Scheme | Edit Scheme and on the Info tab, change it to a &lt;em&gt;Release&lt;/em&gt; build.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-2.40.14-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Build the app (Product | Build ), then back in the Product menu, there&amp;rsquo;s an item &amp;ldquo;Show Build Folder in Finder&amp;rdquo;. Click on that, and there&amp;rsquo;s your different builds. In the Release folder will be the &lt;em&gt;&lt;app name&gt;.app&lt;/em&gt; file that can be copied into your applications folder.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not actually sure if this is a file, more likely a folder that MacOS is pretending is a file. If you right click on it and select &amp;ldquo;Show Package Contents&amp;rdquo; you can see the actual files inside it.&lt;/p&gt;</description></item><item><title>Customizing the default About dialog for MacOS apps</title><link>https://blog.iankulin.com/customizing-the-default-about-dialog-for-macos-apps/</link><pubDate>Fri, 07 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/customizing-the-default-about-dialog-for-macos-apps/</guid><description>&lt;p&gt;The default Xcode MacOS targeted app has a built in &amp;ldquo;About&amp;rdquo; dialog called up from the &amp;ldquo;About &lt;app name&gt;&amp;rdquo; menu item in the Mac menu bar. It wasn&amp;rsquo;t immediately clear to me how to customise this, but after digging through some MacOS apps on GitHub, here&amp;rsquo;s the answer.&lt;/p&gt;
&lt;p&gt;When you app is being built, it looks for the file &amp;ldquo;Credits.rtf&amp;rdquo; in the app bundle. If that is found (&lt;a href="https://developer.apple.com/documentation/appkit/nsapplication/aboutpaneloptionkey/2869609-credits"&gt;or &amp;ldquo;Credits.html&amp;rdquo; or &amp;ldquo;Credits.rtfd&amp;rdquo;&lt;/a&gt;) it&amp;rsquo;s used to build out the About dialog along with your app icon.&lt;/p&gt;
&lt;p&gt;After you&amp;rsquo;ve created the &amp;ldquo;Credits.rtf&amp;rdquo; file, you need to drop it into the folder for your project where the source files go. Then in Xcode, add it to your project in that inner folder:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-1.15.45-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then when you rebuild the app, it will show in your about dialog.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-1.16.33-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>CodeTrimmer - First MacOS App</title><link>https://blog.iankulin.com/codetrimmer-first-macos-app/</link><pubDate>Thu, 06 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/codetrimmer-first-macos-app/</guid><description>&lt;p&gt;I was listening to the StackTrace app this morning (&lt;a href="https://stacktracepodcast.fm/episodes/169/"&gt;episode 169 - &amp;ldquo;Choosing What Bugs to Ship&amp;rdquo;&lt;/a&gt;) and one of the ideas discussed was taking the time to automate some of your development processes, partially to save time, but also because if you make a process simple and quick, you&amp;rsquo;ll be more likely to do it multiple times to improve quality.&lt;/p&gt;
&lt;p&gt;Coincidentally, I&amp;rsquo;d been thinking about how often I paste some code from Xcode in order to display it in one of these blog posts. If it&amp;rsquo;s from the middle of a method, it will generally be indented a long way in, and there&amp;rsquo;s no point in displaying it like that (especially for a mobile reader) so I usually manually delete a heap of spaces from each line to left align it whilst keeping the needed indentation.&lt;/p&gt;
&lt;p&gt;Sounds like a job for a tiny MacOS app - my first! Here it is in action. You copy your code from Xcode, and paste it into the app:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-11.43.38-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then press the &amp;ldquo;Strip Spaces&amp;rdquo; button to get:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-11.43.43-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got a few little niceties to add/tidy up, but it was a breeze converting my SwiftUI iOS development skills to MacOS - literally ticking a box.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/CodeTrimmer/blob/f0fc616bc1d334f7d5268f5231630940cd14d57b/CodeTrimmer/ContentView.swift"&gt;Source&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Design Challenge</title><link>https://blog.iankulin.com/design-challenge/</link><pubDate>Wed, 05 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/design-challenge/</guid><description>&lt;p&gt;So, I&amp;rsquo;ve been working on translating the &lt;a href="https://blog.iankulin.com/design-help/"&gt;UI design&lt;/a&gt; created by the external designer into SwiftUI, and have done all of the easy bits:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-8.19.43-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The rounded rectangles for things like the question display/number input are just ZStacks of roundedrects filled, then stroked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ZStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RoundedRectangle(cornerRadius: &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .fill(.white)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding(.horizontal)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RoundedRectangle(cornerRadius: &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .stroke(.black, lineWidth: &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .padding(.horizontal)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(questionText)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.title)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .fontWeight(.heavy)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(calculatorDisplay)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.title)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .fontWeight(.heavy)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .foregroundColor(.blue)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.frame(maxWidth: &lt;span style="color:#ae81ff"&gt;350&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.offset(y: &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Something I have learned in the process is the .offset modifer. This is what&amp;rsquo;s used to move a view from where SwiftUI would have placed it, and is what I&amp;rsquo;ve done to create that overlapped style where the question display/number input is sitting halfway over the bottom of the blue rounded rectangle. This is in the last line of the code above: &lt;code&gt;.offset(y: 15)&lt;/code&gt; This is moving the whole ZStack down by 15. A trick to watch with this is that since you&amp;rsquo;ve messed with SwiftUI&amp;rsquo;s arrangement, it doesn&amp;rsquo;t then shuffle everything else around this - you need to manually deal with making some space below it.&lt;/p&gt;
&lt;p&gt;The jobs still to do on this view is the customisation of the picker, which I think is going to have to be writing a picker from scratch (or finding the source for the library picker and altering it), and the even more custom combined picker/progress indicator.&lt;/p&gt;</description></item><item><title>Color Picker (website)</title><link>https://blog.iankulin.com/color-picker-website/</link><pubDate>Tue, 04 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/color-picker-website/</guid><description>&lt;p&gt;I&amp;rsquo;ve started work on trying to recreate a &lt;a href="https://blog.iankulin.com/design-help/"&gt;UI provided by a designer&lt;/a&gt;, and in the process needed to identify some colours from a PNG image. I found this great website for this exact purpose.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-30-at-4.36.17-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The site is ImageColorPicker. To use it, you &amp;ldquo;upload&amp;rdquo; your image (actually the image is not going anywhere - it&amp;rsquo;s all done in-browser). Then click on any area you want to identify the colour of.&lt;/p&gt;
&lt;p&gt;It gives the RGB values out of 255, so I divide each one by 255 to get the CGFloat values that the SwiftUI Color() type will take - for example I used the colour above as &lt;code&gt;Color(red: 0.89, green: 0.96, blue: 1.0)&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Design Help</title><link>https://blog.iankulin.com/design-help/</link><pubDate>Mon, 03 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/design-help/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-29-at-5.50.43-pm-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-29-at-5.50.43-pm-1.png" width="253" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I think I mentioned when I&amp;rsquo;d completed the &lt;a href="https://blog.iankulin.com/times-tables-day-35-challenge/"&gt;TimesTable app&lt;/a&gt;, that I was not happy with the design. It&amp;rsquo;s ugly, I don&amp;rsquo;t like the way the feedback about a wrong answer is shown at the same time as the next question, the number of questions setting would be rarely changed and looks uncomfortable where it is, I&amp;rsquo;m not sure the purpose of the picker for which times table would be clear, and it&amp;rsquo;s not appealing to children.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve used &lt;a href="https://www.fiverr.com/search/gigs?query=ux%20design&amp;amp;source=sorting_by&amp;amp;ref_ctx_id=ea3c1ec986bfad92b64b621e1b254729&amp;amp;search_in=everywhere&amp;amp;search-autocomplete-original-term=ux%20design&amp;amp;filter=new"&gt;Fiverr&lt;/a&gt; before to have various bits of graphic design done, so I thought I&amp;rsquo;d have a look, and sure enough there is a heap of people offering UI &amp;amp; UX design. Prices vary widely - from about $10 to $2000AUD. The lower end ones are generally newer entrants. There was a suspicious number of incredibly beautiful looking Ukrainian women wanting UX design gigs - I assumed they were fake and picked a couple of others.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the results so far (one guy is still making changes).&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/main-screen-copy.jpg" width="473" alt=""&gt;
&lt;p&gt;This one combined the number input with the question text - that&amp;rsquo;s smart. To change the number of questions you click on the circle at the top, then it shows you which question you are up to, plus the circle edge is indicating your progress. I love all of that, but do not like having &amp;ldquo;Question&amp;rdquo; written there. There&amp;rsquo;s no solution offered for showing you&amp;rsquo;ve got the question correct or wrong, but maybe I could do that in the sum area (with an animated tick, or a crossing out and re-writing). The picker for which table to practice is pretty similar in function. I didn&amp;rsquo;t like the backspace button - but the designer pushed back saying it met the brief of appealing to kids. I love the layered rounded rectangles, and the inset for the question progress. I&amp;rsquo;m not so keen on having &amp;ldquo;Times Tables&amp;rdquo; written at the top, ot the line underneath it. In my version, the title is animated a little to indicated right/wrong answers so perhaps that&amp;rsquo;s why this designer kept it.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/frame-36-copy.png" width="444" alt=""&gt;
&lt;p&gt;This designer&amp;rsquo;s first draft was black and white, so apart from moving the backspace (which I like) it was just a less ugly version of my UI. When I asked about how this was appealing to kids, he hit on these colours, which I think are great. The non-centred number of questions is driving me mental, but the designer is currently working on shrinking that somehow and moving it up to the light bulb in the top right. His idea for that was that it would give the user a hint when they clicked on it. That seemed like a change in functionality rather than a UX improvement to me. It turned out he was also a budding mobile developer, so he&amp;rsquo;d actually built the entire app - I&amp;rsquo;m not sure if in Flutter or Swift - either way, it looks like I could actually build it with my current skills (although perhaps not that picker). Unlike the first design with the inset and overlapping shapes that&amp;rsquo;s going to require some googling if it&amp;rsquo;s possible at all.&lt;/p&gt;
&lt;p&gt;So far, this has been a good experiment. Both designers quickly came up with something much prettier than I had, and although neither of them look perfect to me, now that I&amp;rsquo;ve seen them I have ideas to improve them.&lt;/p&gt;
&lt;p&gt;Now, the interesting challenge is to see how close I can get to implementing them!&lt;/p&gt;</description></item><item><title>iExpense Challenges</title><link>https://blog.iankulin.com/iexpense-challenges/</link><pubDate>Sun, 02 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/iexpense-challenges/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-29-at-6.41.29-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-29-at-6.41.29-am.png" width="308" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/100/swiftui/38"&gt;Day 38&lt;/a&gt; is three challenges on the iExpense app - a simple expense tracking app that uses UseDefaults for storing it&amp;rsquo;s data.&lt;/p&gt;
&lt;h3 id="locale"&gt;Locale&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Use the user’s preferred currency, rather than always using US dollars.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One of the joys of modern programming (as opposed to mid-1990&amp;rsquo;s programming) is the ability of the internet to give you answers. I knew the answer to this would be lurking in the locale environment variable, but instead of &lt;a href="https://developer.apple.com/documentation/swiftui/environmentvalues/locale"&gt;looking it up&lt;/a&gt;, just googled, and found a viable looking solution on &lt;a href="https://www.reddit.com/r/SwiftUI/comments/t7g7ds/localising_currency/"&gt;Reddit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Text(amount, format: .currency(code: Locale.current.currencyCode ?? &amp;quot;USD&amp;quot;))&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;When I paste it into Xcode, another modern miracle occurs - Xcode warns me know &lt;em&gt;currencyCode&lt;/em&gt; is out of date:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;'currencyCode' was deprecated in iOS 16: renamed to 'currency.identifier'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and offers to fix it for me:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Use 'currency.identifier' instead&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m like &amp;ldquo;sure, fix that for me&amp;rdquo;. Then there&amp;rsquo;s an error because the chaining is needs touched up now:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Value of optional type 'Locale.Currency?' must be unwrapped to refer to member 'identifier' of wrapped base type 'Locale.Currency'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and again offers to fix the chaining for me, so okay it, and end up with this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fileprivate &lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;itemView&lt;/span&gt;(item: ExpenseItem) -&amp;gt; some View {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack(alignment: .leading) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(item.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.headline)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(item.type)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(item.amount, format: .currency(code: Locale.current.currency?.identifier ?? &lt;span style="color:#e6db74"&gt;&amp;#34;USD&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }.foregroundColor((item.amount &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;) ? .purple : (item.amount &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;) ? .green : .blue)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="conditional-formatting"&gt;Conditional formatting&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Modify the expense amounts in &lt;code&gt;ContentView&lt;/code&gt; to contain some styling depending on their value – expenses under $10 should have one style, expenses under $100 another, and expenses over $100 a third style. What those styles are depend on you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Easy - ternary operator on HStack - see above&lt;/p&gt;
&lt;h3 id="conditional-list-building"&gt;Conditional list building&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;For a bigger challenge, try splitting the expenses list into two sections: one for personal expenses, and one for business expenses. This is tricky for a few reasons, not least because it means being careful about how items are deleted!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was tricky, but I didn&amp;rsquo;t run into deletion problems. I thought I&amp;rsquo;d just run the ForEach twice and use an if inside it to build two different list sections. The compiler was very upset with this, saying something about not being able to determine the type in the view, but not being able to identify the view.&lt;/p&gt;
&lt;p&gt;The solution for this turned out to be to put them inside a Group{}. I guess this means it&amp;rsquo;s something related to the ways Views are built and the magic inside the @ViewBuilder property wrapper.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Section(header: Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Personal&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(expenses.items) { item &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Group {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; item.type == &lt;span style="color:#e6db74"&gt;&amp;#34;Personal&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; itemView(item: item)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .onDelete(perform: removeItems)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Section(header: Text(&lt;span style="color:#e6db74"&gt;&amp;#34;Business&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(expenses.items) { item &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Group {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; item.type == &lt;span style="color:#e6db74"&gt;&amp;#34;Business&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; itemView(item: item)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .onDelete(perform: removeItems)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .navigationTitle(&lt;span style="color:#e6db74"&gt;&amp;#34;iExpense&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .toolbar{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showingAddExpense = &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image(systemName: &lt;span style="color:#e6db74"&gt;&amp;#34;plus&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Times Tables -Day 35 Challenge</title><link>https://blog.iankulin.com/times-tables-day-35-challenge/</link><pubDate>Sat, 01 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/times-tables-day-35-challenge/</guid><description>&lt;p&gt;The challenge for &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/3/3/challenge"&gt;Day 35&lt;/a&gt; of &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of Swift&lt;/a&gt; UI was to create a simple times tables drilling app. I&amp;rsquo;ve met all the requirements, so I&amp;rsquo;ll move on, but I am struck by how ugly it is. Making better looking apps needs to be added to my goals. Especially since this app is intended to appeal to children, and is at the end of a few lessons on animation, this is definitely a weakness of mine at the moment.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/ui-copy.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I enjoyed this challenge, it had a few interesting aspects. One was the number keypad for the user to enter, and the other one was creating the dialogue shown to the user at the end of a round with the statistics. I really don&amp;rsquo;t love the iOS modal dialogue boxes, so I ZStacked a rounded rectangle with some views on top of it, and controlled the visibility with .opacity. I wasn&amp;rsquo;t sure what would happen to the user OnTap events - would they go through? The answer is that if you make it very see through they go through to the elements underneath, but if it&amp;rsquo;s reasonably solid, they stay there.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/lQFDcBNAr-s?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/IanKulin/TimesTables/blob/719229d3caf80b12ddfff65032e0bee29036e1c9/TimesTables/ContentView.swift"&gt;Source&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Gitting Xcode to Push</title><link>https://blog.iankulin.com/gitting-xcode-to-push/</link><pubDate>Fri, 30 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gitting-xcode-to-push/</guid><description>&lt;p&gt;I&amp;rsquo;m very comfortable with doing all the routine git stuff from the command line, but it was bugging me that I hadn&amp;rsquo;t for the Xcode integration working. I was able to commit locally with no problem from Xcode, but could not push up to Github. It works fine from the command line, so the error about the change to a stronger SSH authentication didn&amp;rsquo;t really make sense to me.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-26-at-6.57.35-am.png" alt=""&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;ERROR: You&amp;rsquo;re using an RSA key with SHA-1, which is no longer allowed. Please use a newer client or a different key type&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://developer.apple.com/forums/thread/702389"&gt;This post&lt;/a&gt; from &lt;a href="https://developer.apple.com/forums/profile/pasllani"&gt;pasllani&lt;/a&gt; on the Apple Developer forums was super helpful, the only thing they missed was that you need to restart Xcode before it will work. The steps were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate a new ECDSA SSH key with &lt;code&gt;ssh-keygen -t ecdsa -C &amp;quot;IanKulin@kulin.com.au&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy the key to the keyboard with &lt;code&gt;pbcopy &amp;lt; ~/.ssh/id_ecdsa.pub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;On Github, add the new SSH key, and while you&amp;rsquo;re there, generate a new Token in Developer Tools&lt;/li&gt;
&lt;li&gt;In XCode, delete your github account, then recreate it specifying SSH and choose the ECDSA key. It will need the access token at this stage.&lt;/li&gt;
&lt;li&gt;Restart XCode&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="other-gitgithub-posts"&gt;Other git/Github posts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Intro to git - &lt;a href="https://blog.iankulin.com/gitting-started/"&gt;Gitting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Common git commands - &lt;a href="https://blog.iankulin.com/gitting-the-hang-of-it/"&gt;Gitting the Hang of it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/gitting-up-to-date/"&gt;Merge vs rebase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/create-an-empty-folder-on-github/"&gt;Create an Empty Folder on Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/download-a-directory-from-a-github-repo/"&gt;Download a Directory from a Github repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/how-to-download-a-file-from-github/"&gt;Download a File from Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Calculator</title><link>https://blog.iankulin.com/calculator/</link><pubDate>Thu, 29 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/calculator/</guid><description>&lt;p&gt;The app I&amp;rsquo;m working on currently (for multiplication tables practice) has a number type keypad and display a bit like a calculator - but for entering the answers. It&amp;rsquo;s been quite fun to think through all the little problems to make it work how you&amp;rsquo;d expect, so I was quite interested to watch an iOS Academy video where Afraz Siddiqui builds a partially finished SwiftUI version of the iOS Calculator app.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=cMde7jhQlZI"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-25-at-2.42.34-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It is a great example for beginners to see the power of building views quickly in SwiftUI, but the most fun part for me was thinking of all the edge cases that would hadn&amp;rsquo;t been dealt with in his quick build out of the logic (which wasn&amp;rsquo;t the point of the video).&lt;/p&gt;
&lt;p&gt;A couple of times I&amp;rsquo;ve thought about building copies of existing Apple iOS apps. I think part of the appeal is that the design work is done for me, so I&amp;rsquo;m just thinking about under the hood. Long term, I need to know more about design and to be better at it - I bet there&amp;rsquo;s a &lt;a href="https://designcode.io/ui-design"&gt;Meng To&lt;/a&gt; course that would be a good starting point.&lt;/p&gt;</description></item><item><title>User Defaults &amp; Horizontal Pickers</title><link>https://blog.iankulin.com/user-defaults-horizontal-pickers/</link><pubDate>Wed, 28 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/user-defaults-horizontal-pickers/</guid><description>&lt;p&gt;I&amp;rsquo;m on the challenges for &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/3/3/challenge"&gt;Day 35&lt;/a&gt; of &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of SwiftUI&lt;/a&gt;, and despite Paul&amp;rsquo;s very clear warning:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Important:&lt;/strong&gt; It’s really easy to get sucked into these challenges and spend hours&lt;/em&gt;&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I have spent ages fiddling around, but of course still learning. My issue is not so much getting stuck on bugs, rather I keep wanting to do things I don&amp;rsquo;t know how to do.&lt;/p&gt;
&lt;p&gt;One issue was solved for my by the wonderful &lt;a href="https://firesideswift.fireside.fm/"&gt;Fireside Swift&lt;/a&gt; podcast. I&amp;rsquo;m working through the old (Steve &amp;amp; Zac) episodes, and they did one on the UserDefaults just when I wanted to be able to persist the multiplication table selection the user had made (this challenge app is a multiplication tables drill app for kids).&lt;/p&gt;
&lt;p&gt;First, because I hate hard coded strings, and they seem to be a thing in SwiftUI, and there&amp;rsquo;s no #const system, I&amp;rsquo;ve got an enum:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;enum HCS: String {
 case operand = &amp;#34;Operand&amp;#34;
 case markerFeltWide = &amp;#34;Marker Felt Wide&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in the OnChange for the picker where I want to save/set the value into the default store:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.onChange(of: tablesSelection) { _ in 
 UserDefaults.standard.set(self.tablesSelection, 
 forKey: HCS.operand.rawValue)
 generateTable()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So basically, there&amp;rsquo;s a UserDefaults class that we can call the set method of, passing it the value we want to store, and a string value for the key.&lt;/p&gt;
&lt;p&gt;Getting it back is no harder:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@State private var tablesSelection =
 UserDefaults.standard.integer(forKey: HCS.operand.rawValue)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Super simple!&lt;/p&gt;
&lt;p&gt;If the key doesn&amp;rsquo;t exist, &lt;a href="https://developer.apple.com/documentation/foundation/userdefaults/1407405-integer"&gt;it returns a zero&lt;/a&gt;, so the slightly more complicated production version is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@State private var tablesSelection =
 (UserDefaults.standard.integer(forKey: HCS.operand.rawValue) != 0) ? 
 UserDefaults.standard.integer(forKey: HCS.operand.rawValue) : 5
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now to my horizontal wheel picker that the user manipulates to chose which times-table they want to practice:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-25-at-12.39.45-pm-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I love this little hack stolen from &lt;a href="https://stackoverflow.com/users/8279887/james-castrejon"&gt;James&lt;/a&gt; on &lt;a href="https://stackoverflow.com/questions/61965315/how-to-get-a-horizontal-picker-for-swift-ui"&gt;Stack Overflow&lt;/a&gt;. The first trick is that a .rotationEffect is applied to the picker, and the opposite .rotationEffect is applied to the selections. The second is to chop it off using the frame to make it a bit more compact.&lt;/p&gt;</description></item><item><title>Gitting up to date</title><link>https://blog.iankulin.com/gitting-up-to-date/</link><pubDate>Tue, 27 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gitting-up-to-date/</guid><description>&lt;p&gt;I&amp;rsquo;ve started the habit of branching my code for each feature or batches of features. This is not really needed, I&amp;rsquo;ve developing solo, and the code on &lt;code&gt;main&lt;/code&gt; is not in production. I could just go on committing, but part of my process is about becoming competent with git.&lt;/p&gt;
&lt;p&gt;There are a couple of git commands (&lt;code&gt;merge&lt;/code&gt; and &lt;code&gt;rebase&lt;/code&gt;) that mush code between branches together in different ways. The video below (from &lt;a href="https://www.udemy.com/user/manuel-lorenz/"&gt;Manuel Lorenz&lt;/a&gt; at &lt;a href="https://academind.com/"&gt;Academind&lt;/a&gt;) is a particularly clear look at these two commands.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/CRlGDDprdOQ?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;The discussion that follows is essentially just a re-hash of the video above, so if you think you&amp;rsquo;ve got it, you can leave now!&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the situation we start with. A &amp;ldquo;feature&amp;rdquo; branch was created at the point in time that the most recent commit on the master branch was &amp;ldquo;m2&amp;rdquo;. A couple of commits have been made to the feature branch, but meanwhile a further commit has been made on the master branch:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-24-at-7.00.08-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So, what are the options now? (this is just a rehash of the video above in my words, so if you already watched that, you may leave :- ) I&amp;rsquo;m going to use &amp;ldquo;master&amp;rdquo; in my discussions here since that&amp;rsquo;s what Manuel uses, but if you&amp;rsquo;re new to git and you&amp;rsquo;ve only ever seen &amp;ldquo;main&amp;rdquo; - they are just different names for the default branch, &amp;ldquo;main&amp;rdquo; is the current (and slightly better) preference for that name.&lt;/p&gt;
&lt;h3 id="merge"&gt;Merge&lt;/h3&gt;
&lt;p&gt;We could checkout the master branch, and merge feature into it with&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git merge feature&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That works, and our commit history for the master branch will look like this:&lt;/p&gt;
&lt;p&gt;M1 - M2 - F1 - F2 - M3 - Merge&lt;/p&gt;
&lt;h3 id="squash"&gt;Squash&lt;/h3&gt;
&lt;p&gt;To bundle up the feature branch and bring it across to master, we can (from the master branch) do:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git merge --squash feature&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That add all the feature changes to the current branch, but they&amp;rsquo;re not committed yet, so we&amp;rsquo;ll also:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git commit -m &amp;quot;add feature&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The history of the master branch now will be:&lt;/p&gt;
&lt;p&gt;M1 - M2 - M3 - add feature&lt;/p&gt;
&lt;p&gt;So this is sort of neater, instead of a chronological history in the commits, we&amp;rsquo;re conceptually saying the feature work was all done in a single commit after the M3 change. But&amp;hellip; what if we wanted to keep the feature commit history?&lt;/p&gt;
&lt;h3 id="rebase"&gt;Rebase&lt;/h3&gt;
&lt;p&gt;Just to recap, although our master branch is up to m3, the feature branch was &lt;em&gt;based&lt;/em&gt; off m2. So if we had a look at the history of the feature branch (using git log) it looks like this:&lt;/p&gt;
&lt;p&gt;M2 - F1 - F2&lt;/p&gt;
&lt;p&gt;Rebasing it will look at the changes in feature then apply them to the current m3 commit in master. If we change to the feature branch and enter:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git rebase master&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now the feature history will be&lt;/p&gt;
&lt;p&gt;M1 - M2 - M3 - F1 - F2&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s sort of cool and all, basically the code in the feature branch is in the state that master would be after we&amp;rsquo;ve joined them back up, so we can go ahead and test and so on. But we haven&amp;rsquo;t actually joined up feature and master yet. To do that, we could to checkout the master branch, and rebase it from feature with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git rebase feature&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then master will have the same history as feature: M1 - M2 - M3 - F1 - F2 That can just be committed and we&amp;rsquo;re good to go. Since the whole commit history is now in the master branch its fine to go ahead and delete the feature branch.&lt;/p&gt;
&lt;h3 id="gotcha"&gt;Gotcha&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re working in a team around a shared repository, it makes a lot of sense to rebase your local project from the current main/master in the shared repo. That way you can test for any problems your code might have with the current version, but, it&amp;rsquo;s bad form to rebase any commits that get pushed up. A good explanation for why this is can be found in the &lt;a href="https://git-scm.com/book/en/v2/Git-Branching-Rebasing"&gt;git docs&lt;/a&gt; - scroll down to &amp;ldquo;the perils of rebasing&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Instead what we should have done in the example above if we were planning on pushing the master would have been to rebase our feature branch as we did, but then change to the master branch and merge the feature branch in with&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git merge feature&lt;/code&gt;&lt;/p&gt;</description></item><item><title>iOS Academy</title><link>https://blog.iankulin.com/ios-academy-2/</link><pubDate>Mon, 26 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-academy-2/</guid><description>&lt;p&gt;A YouTube channel worth subscribing to is &lt;a href="https://www.linkedin.com/in/afrazsiddiqui"&gt;Afraz Siddiqui&amp;rsquo;s&lt;/a&gt; &lt;a href="https://www.youtube.com/c/iOSAcademy/videos"&gt;iOS Academy&lt;/a&gt;. He does a great videos on iOS development. My favouriets might be the shorter focussed ones, like this one on the new SwiftUI chart views.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/KVz_I10R-wA?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Ranges</title><link>https://blog.iankulin.com/ranges/</link><pubDate>Sun, 25 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ranges/</guid><description>&lt;p&gt;I wondered aloud, in a &lt;a href="https://blog.iankulin.com/project-4-challenges/"&gt;previous post&lt;/a&gt;, about the differences in writing a range as&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; ForEach(1..&amp;lt;21) {
 Text(String($0))
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;versus&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; ForEach(1...20) {
 Text(String($0))
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that&amp;rsquo;s been answered in in one of the Day 34 articles. It sounds like older versions of Swift might not have allowed the second version.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/3/2/key-points"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-6.51.32-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Animating Guess The Flag</title><link>https://blog.iankulin.com/animating-guess-the-flag/</link><pubDate>Sat, 24 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/animating-guess-the-flag/</guid><description>&lt;p&gt;The challenges for &lt;a href="https://www.hackingwithswift.com/100/swiftui/34"&gt;Project 6&lt;/a&gt; of 100 Days of SwiftUI was to add some animations to the &lt;a href="https://blog.iankulin.com/project-2-guess-the-flag/"&gt;Guess the Flag&lt;/a&gt; app from a little while ago. The animations themselves were not particularly tricky, my main issue was that I was creating the views for the three flags in a ForEach, so the animations were applied to all three flags, but we wanted different animations for the flag the user had clicked versus those they had not.&lt;/p&gt;
&lt;p&gt;I got around this by having the magnitude of the changes stored separately for each of the flag views - so they were all being animated, but some by a zero amount.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://videopress.com/v/QjnVovjB?resizeToParent=true&amp;amp;cover=true&amp;amp;playsinline=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true"&gt;https://videopress.com/v/QjnVovjB?resizeToParent=true&amp;amp;cover=true&amp;amp;playsinline=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s three animations going on here, so three @State properties in the view:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-3.06.16-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then attached to the code to create the buttons are all the modifiers for the animation.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-3.07.18-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In the FlagTapped() method you can see attached to the button there, we just change the ones we need:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-3.10.43-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then, to be a little bit fancy, I re-animated the flagScales after the user closes the alert. I really do not love that alert - it was part of the tutorial project, but it is not a good UI - and it covers up some of the lovely animation.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/GuessTheFlag/compare/b373e1d..cfd2fd4"&gt;Source&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Animation Feedback</title><link>https://blog.iankulin.com/animation-feedback/</link><pubDate>Sat, 24 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/animation-feedback/</guid><description>&lt;p&gt;I had a look at Paul&amp;rsquo;s version of the challenge to animate the Guess The Flags app, and like me he&amp;rsquo;d hit on altering the &lt;em&gt;amount&lt;/em&gt; of each animation depending on if it was the clicked on flag, but he&amp;rsquo;d done it with a lot less code - using the ternary operator in each of the modifiers - versus my approach of filling in an Int array for each of the effects and altering them depending on the user selection.&lt;/p&gt;
&lt;p&gt;Another round to Paul!&lt;/p&gt;</description></item><item><title>Animations in Views</title><link>https://blog.iankulin.com/animations-in-views/</link><pubDate>Fri, 23 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/animations-in-views/</guid><description>&lt;p&gt;It&amp;rsquo;s a very Apple-thinking thing to be learning about making beautiful and intuitive user experiences this early in a programing tutorial as I am with the &lt;a href="https://www.hackingwithswift.com/100/swiftui/32"&gt;100 Days of Swift UI&lt;/a&gt; series. Here&amp;rsquo;s a quick look at three different ways of doing animation in SwiftUI Views.&lt;/p&gt;
&lt;h4 id="implicit-animation"&gt;Implicit animation&lt;/h4&gt;
&lt;p&gt;An &lt;em&gt;implicit&lt;/em&gt; animation in SwiftUI is when you add a .&lt;em&gt;animation&lt;/em&gt;() modifier to a view. It needs to be bound to the value that&amp;rsquo;s changing so the framework knows to animate when that value changes, and the nature of the change.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-10.21.30-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-10.21.30-am.png" width="956" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://videopress.com/v/ItGiKMzz?resizeToParent=true&amp;amp;cover=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true"&gt;https://videopress.com/v/ItGiKMzz?resizeToParent=true&amp;amp;cover=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In the example above, the value that&amp;rsquo;s changing is &lt;code&gt;rounded&lt;/code&gt;. We declare this as an @State variable, then in the .animation() modifier, we tell the framework to watch it. When it does change (because the user presses the bottom) SwiftUI considers the difference between the view rendered in rounded state, and in the non-rounded state, then generates and outputs the frames between them.&lt;/p&gt;
&lt;h4 id="binding-animation"&gt;Binding animation&lt;/h4&gt;
&lt;p&gt;The .animation() modifier can be attached when a variable is bound to a control.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-11.13.05-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve slightly complicated things here by adding the .timingCurve() inside the .animation() - otherwise the animation wasn&amp;rsquo;t obvious visually - the effect of this is just to slow down the animation so it keeps happening for a bit after you&amp;rsquo;ve adjusted the slider.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://videopress.com/v/3caKJJkZ?resizeToParent=true&amp;amp;cover=true&amp;amp;autoPlay=true&amp;amp;controls=false&amp;amp;loop=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true"&gt;https://videopress.com/v/3caKJJkZ?resizeToParent=true&amp;amp;cover=true&amp;amp;autoPlay=true&amp;amp;controls=false&amp;amp;loop=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="explicit-animation"&gt;Explicit animation&lt;/h4&gt;
&lt;p&gt;Explicit animation is used when we want to control when the animation occurs, a common reason for this would be when we want to combine a couple of changes to take place simultaneously) .&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-11.38.02-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://videopress.com/v/6dE3XCSP?resizeToParent=true&amp;amp;cover=true&amp;amp;autoPlay=true&amp;amp;controls=false&amp;amp;loop=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true"&gt;https://videopress.com/v/6dE3XCSP?resizeToParent=true&amp;amp;cover=true&amp;amp;autoPlay=true&amp;amp;controls=false&amp;amp;loop=true&amp;amp;preloadContent=metadata&amp;amp;useAverageColor=true&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Gists for embedding code</title><link>https://blog.iankulin.com/gists-for-embedding-code/</link><pubDate>Fri, 23 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gists-for-embedding-code/</guid><description>&lt;p&gt;So, I might have found a slightly better method for sharing code in posts that I complained about the &lt;a href="https://blog.iankulin.com/wordpress-code-blocks/"&gt;other day&lt;/a&gt;. GitHub has a thing called &lt;a href="https://gist.github.com/"&gt;Gists&lt;/a&gt;. It&amp;rsquo;s like a tiny repository you can paste a code snippet into (or upload a source file). Once that&amp;rsquo;s done, you can just paste the URL of the Gist into &lt;a href="https://wordpress.com/support/gist/"&gt;Wordpress&lt;/a&gt; - it recognises it and does this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ForEach(&lt;span style="color:#ae81ff"&gt;0.&lt;/span&gt;.&amp;lt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;) { number &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// flag was tapped&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; flagTapped(number)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } label: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FlagView(flagOf: countries[number])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .rotation3DEffect(.degrees(flagSpinAmount[number]),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; axis: (x: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, y: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, z: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .opacity(flagOpacity[number])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .scaleEffect(flagScale[number])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .animation(.&lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;, value: flagSpinAmount)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I came across this being used on a &lt;a href="https://blog.rosay.io/create-a-camera-app-with-swiftui-60876fcb9118"&gt;blog post&lt;/a&gt; (about using the camera in apps) from &lt;a href="https://rosay.io/"&gt;Gaspard Rosay&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Don't Use Stupid Project Names</title><link>https://blog.iankulin.com/dont-use-stupid-project-names/</link><pubDate>Thu, 22 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/dont-use-stupid-project-names/</guid><description>&lt;p&gt;I&amp;rsquo;m up to &lt;a href="https://www.hackingwithswift.com/100/swiftui/32"&gt;Day 32&lt;/a&gt; of 100 Days of Swift UI, and although the tutorial is named &amp;ldquo;Project 6&amp;rdquo; it&amp;rsquo;s not really a project that becomes a simple app, it&amp;rsquo;s really just a series of tutorials on animation that I assume the techniques, but none of the code, will be used later in apps.&lt;/p&gt;
&lt;p&gt;I do find there&amp;rsquo;s some value in typing in the code (rather than cutting and pasting, or just passively watching) so I opened up Xcode to follow along. There not being an app name offered, I used &amp;ldquo;Project 6 - Animation&amp;rdquo;. XCode seemed happy enough with that, and created the directory and placeholder, but then refused to build it saying there was seven errors involving the __PACKAGE_NAME macro and a missing }&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-8.45.57-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I did a do-over with a project name of &amp;ldquo;Animation&amp;rdquo; and it works just fine. When I updated to XCode 14 I didn&amp;rsquo;t keep the previous version, so I&amp;rsquo;ve no idea if letting the user do this is an introduced bug or not. The culprit is the &amp;lsquo;-&amp;rsquo;. Projects with just a space in the name work fine.&lt;/p&gt;
&lt;p&gt;Since a google of the first error does not pull up any hits, I&amp;rsquo;ll paste it in here for future travelers giving their projects names with dashes.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;'___PACKAGENAME' is annotated with @main and must provide a main static function of type () -&amp;gt; Void, () throws -&amp;gt; Void, () async -&amp;gt; Void, or () async throws -&amp;gt; Void&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;As far as I can make out, that is part of the boilerplate creation process and should have been replaced. Likely the dash and the spaces are all replaced with underscores to make a valid app name and the triple underscore has a magic meaning of some kind.&lt;/p&gt;</description></item><item><title>Recording the Simulator</title><link>https://blog.iankulin.com/recording-the-simulator/</link><pubDate>Wed, 21 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/recording-the-simulator/</guid><description>&lt;p&gt;I&amp;rsquo;ve been using Quicktime to screen record the preview or simulator to document my work here and in Github ReadMe&amp;rsquo;s, but thanks to this &lt;a href="https://sarunw.com/posts/record-ios-simulator-video-and-gif-with-xcode/"&gt;excellent short post&lt;/a&gt; by &lt;a href="https://twitter.com/sarunw"&gt;Sarun W&lt;/a&gt;, I now know it&amp;rsquo;s possible to do that directly from the simulator, including capturing gestures, and exporting to animated gifs!&lt;/p&gt;</description></item><item><title>Wordpress Code Blocks</title><link>https://blog.iankulin.com/wordpress-code-blocks/</link><pubDate>Wed, 21 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/wordpress-code-blocks/</guid><description>&lt;p&gt;Non-iOS post warning :- )&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not really happy with the way I&amp;rsquo;m sharing code in these posts. I started off with the regular Wordpress code blocks:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; func isPossible(word: String) -&amp;gt; Bool {
 var tempWord = rootWord
 for letter in word {
 if let pos = tempWord.firstIndex(of: letter) {
 tempWord.remove(at: pos)
 } else {
 return false
 }
 }
 return true
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These seem a bit large to me, but it comes with a font size choice, which I like setting to &amp;ldquo;Tiny&amp;rdquo;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; func isPossible(word: String) -&amp;gt; Bool {
 var tempWord = rootWord
 for letter in word {
 if let pos = tempWord.firstIndex(of: letter) {
 tempWord.remove(at: pos)
 } else {
 return false
 }
 }
 return true
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s a reason why coloured syntax highlighting exists in IDE&amp;rsquo;s, so obviously I&amp;rsquo;d want that for my posts. If you move to a paid tier on Wordpress, as well as eliminating the advertisements from your posts, you get a new coloured code block called &amp;ldquo;SyntaxHighlighter Code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func isPossible(word: String) -&amp;gt; Bool {
 var tempWord = rootWord
 for letter in word {
 if let pos = tempWord.firstIndex(of: letter) {
 tempWord.remove(at: pos)
 } else {
 return false
 }
 }
 return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It has a few options, line numbers, making links clickable, and highlighting lines (as I&amp;rsquo;ve done above), but no size, and no control of the font or colours, which are so dreadful I&amp;rsquo;ve mostly given up on using them. It does have a number of languages to choose from which is impressive, but the highlighting is not as good as Xcode, here&amp;rsquo;s how that snippet looks with the theme I&amp;rsquo;m using.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-7.34.14-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So the Wordpress block is picking out keywords and types, but not properties. In the ideal world my code examples here would look exactly like this. I could just use screenshots like this, but there&amp;rsquo;s a couple of minor issues with that. The first is the problems with scaling on different devices, and the second is that non-Apple viewers don&amp;rsquo;t have a simple way of selecting text from an image.&lt;/p&gt;
&lt;p&gt;The idea solution would be that the SyntaxHighlighter code block had a few more options. Wordpress is known for the large number of plugins available, so there&amp;rsquo;s possibly a plugin that solves this problem, so a possible solution is for me to learn more about Wordpress which is not a big priority for me at the moment. Related to that is the possibility of using &amp;ldquo;Additional CSS classes&amp;rdquo; which is one of the options for the code block.&lt;/p&gt;
&lt;p&gt;I do note that when code is copied out from Xcode it includes the font and colour information (I guess as rich text?). If I copy the code above and pasted it into word and change the background colour, it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-7.43.31-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So that raises the prospect of pasting it into a different Wordpress block that displays rich text, but if there is such a thing, I can&amp;rsquo;t see how to access it.&lt;/p&gt;
&lt;p&gt;HTML has evolved in part to solve this sort of problem, and there is an HTML block for Wordpress. If I save the Word doc above into HTML and paste it into the HTML block I get this, which is about 75% of the way towards what I&amp;rsquo;m after.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;func&lt;/strong&gt; isPossible(word: String) -&amp;gt; Bool {&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;var&lt;/strong&gt; tempWord = rootWord&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;for&lt;/strong&gt; letter &lt;strong&gt;in&lt;/strong&gt; word {&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;if&lt;/strong&gt; &lt;strong&gt;let&lt;/strong&gt; pos = tempWord.firstIndex(of: letter) {&lt;/p&gt;
&lt;p&gt;tempWord.remove(at: pos)&lt;/p&gt;
&lt;p&gt;} &lt;strong&gt;else&lt;/strong&gt; {&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;return&lt;/strong&gt; &lt;strong&gt;false&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;return&lt;/strong&gt; &lt;strong&gt;true&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;The HTML source is not pretty, but I can&amp;rsquo;t see why this couldn&amp;rsquo;t work if I wrote something to convert the pasted rich text into nicer HTML that looks closer to the Xcode IDE version.&lt;/p&gt;
&lt;p&gt;Other people have solved this problem. I notice Paul Hudson has exactly the presentation I&amp;rsquo;d like on his pages:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-8.11.37-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;His HTML for this (correctly) leaves the work for the CSS. I had a quick look, and other than knowing it was written by &lt;a href="https://getbootstrap.com/"&gt;BootStrap&lt;/a&gt;, it was mostly incomprehensible to me. Better HTML and CSS is on my list of coding goals, but my current level of knowledge is stuck on 1996 HTML. I&amp;rsquo;d be happy to chuck up a page with some blinking text, a visitor counter and an under construction gif for any clients looking for that.&lt;/p&gt;</description></item><item><title>Changing Xcode Font Size</title><link>https://blog.iankulin.com/changing-xcode-font-size/</link><pubDate>Tue, 20 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/changing-xcode-font-size/</guid><description>&lt;p&gt;I&amp;rsquo;ve been changing the Xcode font size by going into the &lt;em&gt;preferences&lt;/em&gt;, &lt;em&gt;themes&lt;/em&gt;, then selecting the different content types and changing the font size.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-7.04.52-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I guess I could speed that process up by saving some themes with different text sizes, but I&amp;rsquo;ve noticed a few times in tutorial videos that there must have been an easier way. It turns out that Ctrl + &amp;ldquo;+&amp;rdquo; and Ctrl + &amp;ldquo;-&amp;rdquo; increases and decreases the code font size.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure why I need to change the font size so often, but it was bugging me, so I&amp;rsquo;m glad to find there&amp;rsquo;s sensible shortcuts for it.&lt;/p&gt;</description></item><item><title>Project 5 - Word Scramble</title><link>https://blog.iankulin.com/project-5-word-scramble/</link><pubDate>Mon, 19 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-5-word-scramble/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-4.30.14-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Another &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of Swift UI&lt;/a&gt; project wrapped up - this time a scrabble like word game. New techniques included saving a large text file in the app bundle and loading it (via a string) into an array on launch of the view. Also a short adventure into UIKit to use a UITextChecker.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/WordScramble/compare/c64c21d..ada15e2"&gt;Source&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Word Scramble Feedback</title><link>https://blog.iankulin.com/word-scramble-feedback/</link><pubDate>Mon, 19 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/word-scramble-feedback/</guid><description>&lt;p&gt;As is my practice now, after completing the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/word-scramble-wrap-up"&gt;challenges for Project 5&lt;/a&gt;, I reviewed Paul&amp;rsquo;s solution (which is only available to subscribers) to see what he&amp;rsquo;d done better so I could learn from it.&lt;/p&gt;
&lt;p&gt;Most of the differences where not of much significance, but there was a couple of things I picked up:&lt;/p&gt;
&lt;p&gt;When the user had pressed the reset button, to empty the array of word guesses from the previous turn, I had&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;usedWords = [String]()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Whereas Paul had:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-6.19.35-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I have no idea if that&amp;rsquo;s better performing or safer, but to me, it&amp;rsquo;s a lot clearer, so I prefer Paul&amp;rsquo;s solution.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-4.30.14-pm-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-4.30.14-pm-1.png" width="266" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The second difference is a user experience one. I had chosen to put the score and reset button both in a bottom toolbar. It&amp;rsquo;s a good solution in the sense that it keeps the score and the reset buttons visible regardless of the size of the (scrollable) list. My code for this was appended to the bottom of the list:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.toolbar {
 ToolbarItem(placement: .bottomBar) {
 HStack {
 Text(&amp;#34;Score: \(score)&amp;#34;)
 Spacer()
 Button(&amp;#34;New Word&amp;#34;) { startGame() }
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Paul used a &lt;em&gt;SafeAreaInset&lt;/em&gt; appended to the bottom of the list, and as he was entering it I was thinking it was overly complex:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;•safeAreaInset(edge:.bottom) {
 Text(&amp;#34;Score: \(score)&amp;#34;)
 .frame (maxWidth: .infinitv)
 .padding()
 .background( .blue)
 .foregroundColor(.white)
 .font(.title)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;until I saw the result and loved it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-6.22.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-6.22.34-pm.png" width="490" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>How to upgrade XCode and Swift</title><link>https://blog.iankulin.com/how-to-upgrade-xcode-and-swift/</link><pubDate>Sun, 18 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-upgrade-xcode-and-swift/</guid><description>&lt;p&gt;With the September release of XCode 14 and Swift 5.7 it was time for my first update - I looked in &amp;ldquo;About&amp;rdquo; for an update link but there wasn&amp;rsquo;t one - so if you&amp;rsquo;re as dense as me, the tip is to head to the &lt;a href="https://www.google.com/url?sa=t&amp;amp;rct=j&amp;amp;q=&amp;amp;esrc=s&amp;amp;source=web&amp;amp;cd=&amp;amp;cad=rja&amp;amp;uact=8&amp;amp;ved=2ahUKEwjN8OGn85r6AhXlWHwKHf85DzAQFnoECBUQAQ&amp;amp;url=https%3A%2F%2Fapps.apple.com%2Fus%2Fapp%2Fxcode%2Fid497799835%3Fmt%3D12&amp;amp;usg=AOvVaw2fEvMbfRtGhB4SPHYB54NX"&gt;Mac App Store&lt;/a&gt; and have a look at the Updates page.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-8.53.34-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Your current XCode version can, of course be found in the &lt;em&gt;XCode | About&lt;/em&gt; dialogue. Mine was on 13.4.1. There&amp;rsquo;s a couple of ways of finding the Swift version - If you&amp;rsquo;ve got an XCode project open, click on the .xcodeproj in the explorer,and have a look in &lt;em&gt;Build Settings&lt;/em&gt; for &lt;em&gt;Swift Compiler - Language&lt;/em&gt; for the major version.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-8.56.44-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Or for a bit more accuracy, try &lt;code&gt;xcrun swift -version&lt;/code&gt; at the command line. I got a few errors, then the version 5.6.1&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-8.55.13-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I won&amp;rsquo;t bang on about the &lt;a href="https://www.youtube.com/watch?v=tYBZ8AVH0Q0"&gt;changes for XCode&lt;/a&gt; or &lt;a href="https://www.swift.org/blog/swift-5.7-released/"&gt;Swift&lt;/a&gt; since that&amp;rsquo;s already been done well elsewhere, and the Swift changes are mostly beyond my expertise level.&lt;/p&gt;</description></item><item><title>How to download a file from GitHub</title><link>https://blog.iankulin.com/how-to-download-a-file-from-github/</link><pubDate>Sat, 17 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-download-a-file-from-github/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-13-at-9.12.31-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A quick tip - since it was not immediately obvious to me. If you need to download a file from GitHub (as opposed to cloning it etc), look for the &amp;ldquo;Raw&amp;rdquo; button - that&amp;rsquo;s a link to the file, then right click and do whatever your browser needs to download a web resource.&lt;/p&gt;</description></item><item><title>Project 4 Challenges</title><link>https://blog.iankulin.com/project-4-challenges/</link><pubDate>Fri, 16 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-4-challenges/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-13-at-7.22.43-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-13-at-7.22.43-pm.png" width="197" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve completed the Project 4 challenges (source) of the 100 Days of SwiftUI, no biggie - the increase in difficulty between each step of Paul&amp;rsquo;s bootcamp is small enough that it&amp;rsquo;s never too stressful, but large enough you feel like you&amp;rsquo;re progressing all the time.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;ve paid to be a member of Hacking with Swift, one of the perks is to see Paul&amp;rsquo;s video solutions. I&amp;rsquo;ve not worries about it before, but I should - looking at them and comparing to my efforts is probably good feedback. So here&amp;rsquo;s the differences in our answers to the challenges.&lt;/p&gt;
&lt;h4 id="sections"&gt;Sections&lt;/h4&gt;
&lt;p&gt;When I changed the VStacks to Sections, I put the section title text at the top:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Section(header: Text(&amp;#34;When do you want to wake up?&amp;#34;)) {
 HStack {
 DatePicker(&amp;#34;Please enter a time&amp;#34;, selection: $wakeUp, 
 displayedComponents: .hourAndMinute).labelsHidden()
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Paul had his in a trailing closure at the bottom:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Section {
 DatePicker(&amp;#34;Please enter a time&amp;#34;, selection: $wakeUp, 
 displayedComponents: .hourAndMinute).labelsHidden()
}
header: {
 Text(&amp;#34;When do you want to wake up?&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I thought mine was nicer, but then the very next thing he shows in the video is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Section(&amp;#34;When do you want to wake up?&amp;#34;) {
 DatePicker(&amp;#34;Please enter a time&amp;#34;, selection: $wakeUp, 
 displayedComponents: .hourAndMinute).labelsHidden()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So there - I have learned something for an expert&amp;hellip;&lt;/p&gt;
&lt;p&gt;The next job was to change the stepper to a picker for the number of cups of coffee. I had something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Section(header: Text(&amp;#34;Daily coffee intake?&amp;#34;) {
 Picker(coffeeAmount == 1 ? &amp;#34;1 cup&amp;#34; : &amp;#34;\(coffeeAmount) cups&amp;#34;, 
 selection: $coffeeAmount) {
 ForEach(1...20, id: \.self) { i in
 if i == 1 {
 Text(&amp;#34;1 cup&amp;#34;)
 } else {
 Text(&amp;#34;\(i) cups&amp;#34;)
 }
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And Paul:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Section(&amp;#34;Daily coffee intake?&amp;#34;) {
 Picker(&amp;#34;Number of cups&amp;#34;, selection: $coffeeAmount) {
 ForEach(1..&amp;lt;21) {
 Text(String($0))
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;so yeah, then when I looked at my output properly (see simulator image above) I noticed mine didn&amp;rsquo;t make sense anyway. My bad - that&amp;rsquo;s a horrid careless error.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure about the difference in the ranges 1&amp;hellip;20 and 1..&amp;lt;21 the ..&amp;lt; is a great habit for avoiding off by one errors with collections, perhaps that&amp;rsquo;s the reason for Paul&amp;rsquo;s choice there.&lt;/p&gt;
&lt;p&gt;The third thing to do was to get rid of the alert, and show the result live as changes were made. I did mine by by adding a text field whose contents were a call to a slightly modified calculateBedtime() function. Paul moved that code up to a new computed property. I&amp;rsquo;m not sure I see any difference there except style.&lt;/p&gt;
&lt;p&gt;So, that&amp;rsquo;s a worthwhile thing to do - to look at Paul&amp;rsquo;s solution and compare it to mine, so I&amp;rsquo;ll go on and do that in future.&lt;/p&gt;</description></item><item><title>Machine Learning</title><link>https://blog.iankulin.com/machine-learning/</link><pubDate>Thu, 15 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/machine-learning/</guid><description>&lt;p&gt;A few years ago when I still used a Tom-Tom for car navigation, I was a little freaked out when it started offering suggestions on where to go to when I started the car - guessing, usually correctly, where I wanted to go. Like - how did it know I was leaving school for band practice two towns over?&lt;/p&gt;
&lt;p&gt;Clearly, is must have been collecting data on times/days and departure locations to learn some of my habits. It felt quite invasive, but I thought it must have been on-device since I had the wifi turned off in the unit.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://www.hackingwithswift.com/100/swiftui/27"&gt;Day 27&lt;/a&gt; of 100 Days of Swift UI we CoreML and use a dataset to train a model, then incorporate it in an App. The most shocking thing about it was how straightforward it was. In the example, the model was trained in CreateML from XCode and only used on the device, rather than trained on it, but current iPhones have the power to do that.&lt;/p&gt;
&lt;p&gt;Obviously there&amp;rsquo;s some amazing things that can be done with machine learning, but I&amp;rsquo;m actually more excited about the small things - perhaps just offering better defaults for user inputs.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a great intro to machine learning for Apple developers &lt;a href="https://developer.apple.com/machine-learning/"&gt;here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Before SwiftUI</title><link>https://blog.iankulin.com/before-swiftui/</link><pubDate>Wed, 14 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/before-swiftui/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-10-at-9.28.24-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m on Day 26 of 100 Days, and didn&amp;rsquo;t grok the dates on my first read through, so I&amp;rsquo;ve read a couple of other explanations and sat down with a coffee and thought I&amp;rsquo;d see what YouTube had for me on the subject. I seen a few great &lt;a href="https://www.youtube.com/c/iOSAcademy/videos"&gt;iOS Academy&lt;/a&gt; videos, so &lt;a href="https://www.youtube.com/watch?v=HSFTzcYzuEQ"&gt;this one&lt;/a&gt; seemed like a good choice.&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t seen enough to say if it is a good or great explanation of dates, calendars and date components in Swift yet, but man, getting to the stage of writing useful code when using storyboards and UIKit takes a while! It&amp;rsquo;s literally 3:42 in to the video before there&amp;rsquo;s enough infrastructure done for &amp;ldquo;hello world&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Takeaways: (1) Watching the launch of SwiftUI must have been mind-blowing. (2) I super appreciate the existence of Playgrounds for learning Swift.&lt;/p&gt;</description></item><item><title>Rock, Paper Scissors (2)</title><link>https://blog.iankulin.com/rock-paper-scissors-2/</link><pubDate>Tue, 13 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rock-paper-scissors-2/</guid><description>&lt;p&gt;When I was forced by a deadline into delivering this project, I noted in its &lt;a href="https://blog.iankulin.com/rock-paper-scissors-1/"&gt;post&lt;/a&gt; that there was a number of improvements to make:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;The rock paper scissors could be some better data structure than an array and some ints.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I don’t love the try to win, try to lose aspect, but the client specified it&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Having the didUserWin and didComputerWin funcs is a cop out – that should probably be a single function returning a win/lose/draw type&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I also am unhappy with nesting them in the view namespace to use my #consts&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;duplicating the last part of the view but making the elements .hidden() to keep the same spacing seems like a kludge&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;when I added the link to the Hacking With SwiftUI page with the app brief just now, I noticed I haven’t done the scoring the way it was asked for&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&amp;rsquo;s the progress&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The rock paper scissors could be some better data structure than an array and some ints&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Done. Made a sweet swifty enum. Read about it here. That also solved #4&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I don’t love the try to win, try to lose aspect, but the client specified it&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We can&amp;rsquo;t always help what a client wants. Deal with it.&lt;/p&gt;
&lt;p&gt;_Having the didUserWin and didComputerWin funcs is a cop out – that should probably be a single function returning a win/lose/dra_w&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;enum GameResult {
 case win
 case loss
 case draw
}

func gameResult(user: FingerShape, computer: FingerShape) -&amp;gt; GameResult {
 switch user {
 case .rock:
 switch computer {
 case .rock: return .draw
 case .paper: return .loss
 case .scissors: return .win
 }
 case .paper:
 switch computer {
 case .rock: return .win
 case .paper: return .draw
 case .scissors: return .loss
 }
 case .scissors:
 switch computer {
 case .rock: return .loss
 case .paper: return .win
 case .scissors: return .draw
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;duplicating the last part of the view but making the elements .hidden() to keep the same spacing seems like a kludge&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This was an issue - not just because of the mess of code, but because (according to Paul) it&amp;rsquo;s a workload issue - the view managing part of SwiftUI has to keep creating and destroying parts of our view instead of recycling them. In an earlier lesson, he encouraged the use of the ternary operator in modifiers to avoid this - but .hidden() doesn&amp;rsquo;t have any arguments. Here&amp;rsquo;s the sort of code I&amp;rsquo;m talking about - I&amp;rsquo;ve got this sort of thing several places.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;if revealResult {
 Text(computerSelection.rawValue)
 .font(.system(size: 200))
 Text(winText).font(.title)
 Spacer()
 Button(&amp;#34;Play again&amp;#34;) {
 goalIsWinThisTurn = Bool.random()
 revealResult = false
 if showScores {
 score = 0
 numberOfPlays = 0
 showScores.toggle()
 }
 }
 .buttonStyle(CustomButtonStyle())
}
else {
 Text(computerSelection.rawValue)
 .font(.system(size: 200))
 .hidden()
 Text(winText)
 .font(.title)
 .hidden()
 Spacer()
 Button(&amp;#34;Play again&amp;#34;) {}
 .buttonStyle(CustomButtonStyle())
 .hidden()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I was surprised when googling something like &amp;ldquo;swiftui view visibility not hidden()&amp;rdquo; I found, instead of a stack overflow question, a &lt;a href="https://developer.apple.com/tutorials/swiftui-concepts/choosing-the-right-way-to-hide-a-view?changes=_3"&gt;nice Apple tutorial&lt;/a&gt; at the top of the links.&lt;/p&gt;
&lt;p&gt;Within was my answer - most views have an .opacity() modifier. With that, the code above becomes:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Group {
 Text(computerSelection.rawValue)
 .font(.system(size: 200))
 Text(winText).font(.title)
 Spacer()
 Button(&amp;#34;Play again&amp;#34;) {
 goalIsWinThisTurn = Bool.random()
 revealResult = false
 if showScores {
 score = 0
 numberOfPlays = 0
 showScores.toggle()
 }
 }
 .buttonStyle(CustomButtonStyle())
}
.opacity(revealResult ? 1 : 0)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Noice. I then when on a ternary insertion spree that dropped my view body down from 74 lines of code to 52 and improved readability substantially.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/RockPaper/blob/fe8e2eea247e9c1ab13daa3e39929100991f69c5/RockPaper/ContentView.swift"&gt;Current source&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Swift enums</title><link>https://blog.iankulin.com/swift-enums/</link><pubDate>Mon, 12 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swift-enums/</guid><description>&lt;p&gt;I&amp;rsquo;ve started on the refactoring for &lt;a href="https://blog.iankulin.com/rock-paper-scissors-1/"&gt;Rock, paper, scissors&lt;/a&gt;. One of the things I didn&amp;rsquo;t like was using Ints to signal which shape (I&amp;rsquo;m calling the rock, or paper, or scissors hand shape a &lt;em&gt;shape&lt;/em&gt;) was being handed around. The Int I was using was also the index into an array of the emoji&amp;rsquo;s - so if I did an off-by-one I was risking an out of bounds on the array.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m pleased with this solution:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-08-at-6.39.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-08-at-6.39.53-pm.png" width="921" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re a refugee from C, there&amp;rsquo;s a lot happening here:&lt;/p&gt;
&lt;p&gt;1 - In C enums are actually int&amp;rsquo;s inside, and you can mess with which int goes with which value. In Swift, they can be any type, but the type is specified when you define the enum. They are extracted with &lt;em&gt;.rawvalue&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;2 - The Swift String type is a beast. Yes it does emojis, and a lot of other very cool unicode stuff that has a cost.&lt;/p&gt;
&lt;p&gt;3 - enums can have methods. I know right?&lt;/p&gt;
&lt;p&gt;When I read the &lt;a href="https://docs.swift.org/swift-book/"&gt;Swift book&lt;/a&gt;, I felt it was a travesty against nature for enums being allowed to have methods, but the code above felt like the most natural thing ever. I must be acclimatising.&lt;/p&gt;
&lt;p&gt;Apart from eliminating the array out of bounds possibility, this also made the code more readable, and meant that I could remove the default case from all my switch statements.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/RockPaper/commit/ff3d81599b92ba39cfa2a41c5c8c213f1b442735?diff=split"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I might not be happy with the name of this type - &lt;em&gt;Shape&lt;/em&gt;. I have spend a bit of time thinking about it, and even looked up Rock Paper Scissors on Wikipedia to see what terminology they used. It&amp;rsquo;s meant to represent the shape of the hands made by the players in each round of the game. It felt unclear enough that I added a comment to make it clearer - a sure sign it&amp;rsquo;s not. It may change to &lt;em&gt;FingerShape&lt;/em&gt; when I do the next lot of refactoring.&lt;/p&gt;</description></item><item><title>.git stuffed</title><link>https://blog.iankulin.com/git-stuffed/</link><pubDate>Sun, 11 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-stuffed/</guid><description>&lt;p&gt;I&amp;rsquo;m in a bit of a swing with my git process. I usually develop locally committing as needed, then when I reach some sort of first milestone, create an empty repo on GitHub the push up to it by:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git remote add origin git@github.com:IanKulin/RockPaper.git
git branch -M main
git push -u origin main
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or, I start on GitHub, create a new repo with a readme.md in it, and then use the -f (force) flag when I push to it and override the contents. I think forgetting about this might have been the source of tonight&amp;rsquo;s problems with &amp;ldquo;unrelated histories&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-07-at-8.26.16-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll try and reproduce it on the weekend - I thought I could just pull down the readme from github and the push up all my local changes, but I for the unrelated histories error. I started googling and entering git commands I knew nothing about for a while before just deleting the .git folder, and the github repo, and starting again.&lt;/p&gt;</description></item><item><title>Rock, Paper, Scissors (1)</title><link>https://blog.iankulin.com/rock-paper-scissors-1/</link><pubDate>Sat, 10 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rock-paper-scissors-1/</guid><description>&lt;p&gt;As I mentioned yesterday, I needed to make some progress to blog about, and I had a half working version of a Rock, Paper, Scissors for &lt;a href="https://www.hackingwithswift.com/100/swiftui/25"&gt;Day 25&lt;/a&gt; so I pushed myself to get that working.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s lots in the code below I don&amp;rsquo;t love.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The rock paper scissors could be some better data structure than an array and some ints.&lt;/li&gt;
&lt;li&gt;I don&amp;rsquo;t love the try to win, try to lose aspect, but the client specified it&lt;/li&gt;
&lt;li&gt;Having the didUserWin and didComputerWin funcs is a cop out - that should probably be a single function returning a win/lose/draw type&lt;/li&gt;
&lt;li&gt;I also am unhappy with nesting them in the view namespace to use my #consts&lt;/li&gt;
&lt;li&gt;duplicating the last part of the view but making the elements .hidden() to keep the same spacing seems like a kludge&lt;/li&gt;
&lt;li&gt;when I added the link to the Hacking With SwiftUI page with the app brief just now, I noticed I haven&amp;rsquo;t done the scoring the way it was asked for&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/RockPaper/commit/b1497cccf2dc8af953b946458af797dc5ad12dc9?diff=unified#diff-223dd39ecc4f631b084c99b065a71ea40dc2deba8e36e7f5f939802e60c80186"&gt;source on github&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State var score = 0
@State var goalIsWinThisTurn = Bool.random()
@State var userSelection = 0
@State var computerSelection = 0
@State var winText = &amp;quot;&amp;quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// game has two modes - revealResult - buttons don't work, and we can see the computer result
// or not readyToPlay - user can chose their play
@State var revealResult = false

let rock = 0
let paper = 1
let scissors = 2
let rpsEmoji = \[&amp;quot;🪨&amp;quot;, &amp;quot;📃&amp;quot;, &amp;quot;✂️&amp;quot;\]

 
var body: some View {
 ZStack{
 LinearGradient(colors: \[.white, .gray\], startPoint: .top, endPoint: .bottom)
 VStack{
 Button(&amp;quot;Score: \\(score)&amp;quot;){
 score = 0
 }
 .font(.title)
 .foregroundColor(.primary)
 
 Spacer()
 if goalIsWinThisTurn {
 Text(&amp;quot;Try to win&amp;quot;)
 .font(.title)
 }
 else {
 Text(&amp;quot;Try to lose&amp;quot;)
 .font(.title)
 }
 
 Spacer()
 HStack{
 Spacer()
 Button(rpsEmoji\[rock\]) {
 processButton(selection: rock)
 }
 Spacer()
 Button(rpsEmoji\[paper\]) {
 processButton(selection: paper)
 }
 Spacer()
 Button(rpsEmoji\[scissors\]){
 processButton(selection: scissors)
 }
 Spacer()
 }.font(.system(size: 60))
 
 Spacer()
 if revealResult {
 Text(rpsEmoji\[computerSelection\])
 .font(.system(size: 200))
 Text(winText).font(.title)
 Spacer()
 Button(&amp;quot;Play again&amp;quot;) {
 goalIsWinThisTurn = Bool.random()
 revealResult = false
 }
 .buttonStyle(CustomButtonStyle())
 }
 else {
 Text(rpsEmoji\[computerSelection\])
 .font(.system(size: 200))
 .hidden()
 Text(winText)
 .font(.title)
 .hidden()
 Spacer()
 Button(&amp;quot;Play again&amp;quot;) {
 goalIsWinThisTurn = Bool.random()
 revealResult = false
 }
 .buttonStyle(CustomButtonStyle())
 .hidden()
 }
 Spacer()
 }
 }
}

func didUserWin(user: Int, computer: Int) -&amp;gt; Bool {
 switch(user) {
 case rock:
 switch(computer) {
 case scissors: return true
 default: return false
 }
 case paper:
 switch(computer) {
 case rock: return true
 default: return false
 }
 case scissors:
 switch(computer) {
 case paper: return true
 default: return false
 }
 default:
 assert(false)
 return false
 }
}

func didComputerWin(user: Int, computer: Int) -&amp;gt; Bool {
 switch(computer) {
 case rock:
 switch(user) {
 case scissors: return true
 default: return false
 }
 case paper:
 switch(user) {
 case rock: return true
 default: return false
 }
 case scissors:
 switch(user) {
 case paper: return true
 default: return false
 }
 default:
 assert(false)
 return false
 }
}

func processButton(selection: Int) {
 if !revealResult {
 computerSelection = Int.random(in: 0...2)
 userSelection = selection
 if didUserWin(user: userSelection, computer: computerSelection) {
 winText = &amp;quot;You win&amp;quot;
 if goalIsWinThisTurn {
 score += 1
 winText = &amp;quot;You win&amp;quot;
 } else {
 score -= 1
 winText = &amp;quot;You win, sorry&amp;quot;
 }
 } else if didComputerWin(user: userSelection, computer: computerSelection) {
 if goalIsWinThisTurn {
 score -= 1
 winText = &amp;quot;You lose, sorry&amp;quot;
 } else {
 score += 1
 winText = &amp;quot;You lose!&amp;quot;
 }
 } else {
 winText = &amp;quot;Draw&amp;quot;
 }
 revealResult = true
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;struct CustomButtonStyle : ButtonStyle {
func makeBody(configuration: Configuration) -&amp;gt; some View {
configuration.label
.font(.title)
.padding(10)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(5)
}
}&lt;/p&gt;</description></item><item><title>A deadline is a good thing</title><link>https://blog.iankulin.com/a-deadline-is-a-good-thing/</link><pubDate>Fri, 09 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/a-deadline-is-a-good-thing/</guid><description>&lt;p&gt;I usually have a few days of blog posts written in advance so I can schedule one to come out each day, and not sweat if I&amp;rsquo;m caught up in real life. There&amp;rsquo;s no real reason why I should have that strict publishing schedule, but it is part of my internal discipline to ensure that, at least on average I&amp;rsquo;m making some sort of report-able progress or effort each day.&lt;/p&gt;
&lt;p&gt;And of course, there&amp;rsquo;s the psychological weight of my streak!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-07-at-7.45.40-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m bushed from work tonight, and probably would not have done any programming work beyond watching a related YouTube, but I&amp;rsquo;d run out of scheduled posts, and had a half started project as the next thing to do.&lt;/p&gt;
&lt;p&gt;The pressure of having to come up with a post here made me drag it out and do a MVP of it to met the specifications. Yay deadlines.&lt;/p&gt;</description></item><item><title>Learn to Code</title><link>https://blog.iankulin.com/learn-to-code/</link><pubDate>Thu, 08 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/learn-to-code/</guid><description>&lt;p&gt;This blog exists for a couple of reasons - firstly Paul Hudson insisted on posting progress in the 100 days of SwiftUI on social media, and secondly, when I try to explain something, I&amp;rsquo;m forced to understand it clearly - so I know this is a good learning technique.&lt;/p&gt;
&lt;p&gt;This video from &lt;a href="https://fireship.io/"&gt;Fireship&lt;/a&gt; says this idea is called the &lt;a href="https://www.lifehack.org/862931/feynman-technique"&gt;Feynman Technique&lt;/a&gt;&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/NtfbWkxJTHw?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>.self in ForEach</title><link>https://blog.iankulin.com/self-in-foreach/</link><pubDate>Wed, 07 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/self-in-foreach/</guid><description>&lt;p&gt;I&amp;rsquo;m on Day 25 of Hacking With SwiftUI, and &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/2/2/key-points"&gt;Paul is making a point&lt;/a&gt; about how SwiftUI can loop over an array to build a view. He starts with this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let agents = [&amp;#34;Cyril&amp;#34;, &amp;#34;Lana&amp;#34;, &amp;#34;Pam&amp;#34;, &amp;#34;Sterling&amp;#34;]
VStack {
 ForEach(0..&amp;lt;agents.count) {
 Text(agents[$0])
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But then proposes an alternative:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let agents = [&amp;#34;Cyril&amp;#34;, &amp;#34;Lana&amp;#34;, &amp;#34;Pam&amp;#34;, &amp;#34;Sterling&amp;#34;]
VStack {
 ForEach(agents, id: \.self) {
 Text($0)
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;He explains the use of \.self here by saying&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So, we come back to how Swift can identify values in our array. When we were using a range such as &lt;code&gt;0..&amp;lt;5&lt;/code&gt; or &lt;code&gt;0..&amp;lt;agents.count&lt;/code&gt;, Swift knew for sure that each item was unique because it would use the numbers from the range – each number was used only once in the loop, so it would definitely be unique.&lt;/p&gt;
&lt;p&gt;In our array of strings that’s no longer possible, but we can clearly see that each value is unique: the values in &lt;code&gt;[&amp;quot;Cyril&amp;quot;, &amp;quot;Lana&amp;quot;, &amp;quot;Pam&amp;quot;, &amp;quot;Sterling&amp;quot;]&lt;/code&gt; don’t repeat. So, what we can do is tell SwiftUI that the strings themselves – “Cyril”, “Lana”, etc – are what can be used to identify each view in the loop uniquely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m having a couple of problems with this.&lt;/p&gt;
&lt;h4 id="grumble-one"&gt;Grumble One&lt;/h4&gt;
&lt;p&gt;The first is that Swift can&amp;rsquo;t really use the &amp;ldquo;strings themselves&amp;rdquo;. So this doesn&amp;rsquo;t work:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ForEach(agents, String($0)) {
 Text($0)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It complains that there&amp;rsquo;s no initialiser to take those arguments. So \.self must be something fancier. This is something I can presumably investigate by looking at the initialisers for ForEach.&lt;/p&gt;
&lt;h4 id="grumble-two"&gt;Grumble Two&lt;/h4&gt;
&lt;p&gt;And the second grumble is that the first formulation &lt;code&gt;ForEach(0..&amp;lt;agents.count)&lt;/code&gt; throws a compiler warning &amp;ldquo;&lt;code&gt;Non-constant range: argument must be an integer literal&lt;/code&gt;&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-03-at-11.44.26-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Which is true, if I swap it for the number &amp;lsquo;4&amp;rsquo; it stops complaining. I guess in the initialiser for ForEach there&amp;rsquo;s something specifying this although it&amp;rsquo;s not clear to me why. Again it&amp;rsquo;s an initialiser mystery because I can make the warning go away by adding the id reference to self.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-03-at-2.06.48-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Googling for the warning finds a &lt;a href="https://www.hackingwithswift.com/forums/swiftui/compiler-warning-non-constant-range-argument-must-be-an-integer-literal/14878"&gt;page on the Hacking with Swift forums&lt;/a&gt; that throws a little bit of light on the issue by looking at the inits.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-03-at-2.13.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The difference between these two that we&amp;rsquo;re interested in is that in the first, the type is &lt;code&gt;Range&lt;/code&gt;, and the second &lt;code&gt;Data&lt;/code&gt;. &lt;a href="https://www.hackingwithswift.com/users/roosterboy"&gt;@RoosterBoy&lt;/a&gt;&amp;rsquo;s explanation for why we should use literal ints for the range is that it&amp;rsquo;s to be safe from the array changing size during the loop - which is slightly unsatisfying because that could still happen with the literal int range.&lt;/p&gt;
&lt;h4 id="grumble-one-again"&gt;Grumble One again&lt;/h4&gt;
&lt;p&gt;These initialisers also shed some light on my first problem, the references used by SwiftUI should be KeyPaths. &lt;a href="https://sarunw.com/posts/what-is-keypath-in-swift/"&gt;Sarun&amp;rsquo;s explanation&lt;/a&gt; of them makes them sound like actual references - ie memory addresses for the properties. That would make sense since SwiftUi wants them to keep track of things.&lt;/p&gt;
&lt;p&gt;So I tried this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ForEach(agents, id: \String.$0) {
 Text($0)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;but the compiler is still unhappy - so clearly I am not knowledgeable enough yet to solve this one.&lt;/p&gt;</description></item><item><title>I git it*</title><link>https://blog.iankulin.com/i-git-it/</link><pubDate>Tue, 06 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/i-git-it/</guid><description>&lt;img src="https://blog.iankulin.com/images/6y8jpj5f4el91.jpg" width="500" alt=""&gt;
&lt;p&gt;This meme&amp;rsquo;s been trending in the &lt;a href="https://old.reddit.com/r/ProgrammerHumor/comments/x3udcz/is_it_really_that_much_easy_because_in_the/"&gt;r/ProgrammerHumor subreddit&lt;/a&gt;, and although &amp;ldquo;to do literally anything&amp;rdquo; is a stretch, my git / github workflow is pretty routine now using the &lt;a href="https://xkcd.com/1597/"&gt;relevant xkcd&lt;/a&gt; method, but actually with quite a bit of understanding from the first half of the excellent &lt;a href="https://git-scm.com/book/en/v2"&gt;Pro Git book&lt;/a&gt;. I highly recommend it.&lt;/p&gt;
&lt;p&gt;I had in my goals to set up XCode for push (I think I probably just need to generate a token on GitHub and save it in xcode), so I will do that for completion, but I&amp;rsquo;m also enjoying my &lt;a href="https://blog.iankulin.com/oh-my-zsh/"&gt;pimped out terminal&lt;/a&gt; so I&amp;rsquo;m pretty much a git cli guy now.&lt;/p&gt;
&lt;p&gt;I may have gone overboard creating repos for every tiny app in my learning process - sorry Microsoft you have to host all of that for free. It is probably possible to have one repo, with a heap of separate projects inside it. That would require the ability to clone just a directory and it&amp;rsquo;s sub directories, which is something I don&amp;rsquo;t know how to do yet :-/&lt;/p&gt;
&lt;p&gt;&lt;em&gt;* Yes, I&amp;rsquo;m always going to do bad git puns in post titles.&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Project 3</title><link>https://blog.iankulin.com/project-3/</link><pubDate>Mon, 05 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-3/</guid><description>&lt;p&gt;This one&amp;rsquo;s not really a project, just a couple of little updates to earlier work, and a code snippet.&lt;/p&gt;
&lt;h4 id="challenge-1"&gt;Challenge 1&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Go back to project 1 and use a conditional modifier to change the total amount text view to red if the user selects a 0% tip.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first one is pretty simple - a ternary condition to make the total red if the tip is set to zero.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-02-at-5.10.56-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Text(grandTotal, format: currencyCode)
.foregroundColor(tipPercentage == 0 ? .red : .primary)&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;ternary operator&lt;/em&gt; is like a little inline if then else statement. It has the format:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;(w ? t : f)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;where w is the condition, t is the code if the condition is true and f is the code if the condition is false. So the code above checks if the tipPercentage is zero, if it is, the grandTotal text is red, otherwise it&amp;rsquo;s coloured &lt;em&gt;.primary&lt;/em&gt; - one of the semantic colors. The semantic colours are colours set by the system and referred to by their purpose. In this case it will be black (the &amp;ldquo;primary&amp;rdquo; text colour), unless I change the theme to dark mode, in which case it will be white.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/WeSplit/compare/v1.0...v1.1"&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="challenge-2"&gt;Challenge 2&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Go back to project 2 and replace the&lt;/em&gt; &lt;code&gt;Image&lt;/code&gt; &lt;em&gt;view used for flags with a new&lt;/em&gt; &lt;code&gt;FlagImage&lt;/code&gt;&lt;em&gt;&lt;code&gt;()&lt;/code&gt; view that renders one flag image using the specific set of modifiers we had.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Again, pretty straightforward. I just made a new view struct in the main view.&lt;/p&gt;
&lt;p&gt;struct FlagView: View {
var flagOf: String&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; var body: some View {
 Image(flagOf)
 .renderingMode(.original)
 .shadow(radius: 5)
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Then called it with our flag name.&lt;/p&gt;
&lt;p&gt;ForEach(0..&amp;lt;3) { number in
Button {
// flag was tapped
flagTapped(number)
} label: {
FlagView(flagOf: countries[number])
}
}&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/GuessTheFlag/compare/v1.0...v1.1"&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="challenge-3"&gt;Challenge 3&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Create a custom&lt;/em&gt; &lt;code&gt;ViewModifier&lt;/code&gt; &lt;em&gt;(and accompanying&lt;/em&gt; &lt;code&gt;View&lt;/code&gt; &lt;em&gt;extension) that makes a view have a large, blue font suitable for prominent titles in a view.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
VStack{
Text(&amp;ldquo;Hello World&amp;rdquo;)
.titleStyle()
Spacer()
}
}
}&lt;/p&gt;
&lt;p&gt;struct ProminentTitle: ViewModifier {
func body(content: Content) -&amp;gt; some View {
content
.font(.largeTitle)
.foregroundColor(.blue)
.padding()
}
}&lt;/p&gt;
&lt;p&gt;extension View {
func titleStyle() -&amp;gt; some View {
modifier(ProminentTitle())
}
}&lt;/p&gt;</description></item><item><title>Sean != Erica</title><link>https://blog.iankulin.com/sean-erica/</link><pubDate>Sun, 04 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sean-erica/</guid><description>&lt;p&gt;When Swift was newer, there was a bunch of podcasts about it - in early episodes of &lt;a href="https://podcasts.apple.com/au/podcast/fireside-swift/id1269435221"&gt;Fireside Swift&lt;/a&gt; the existence of a Swift Podcast Network is often mentioned, but now it&amp;rsquo;s more of an established language there&amp;rsquo;s a bit less current content to listen to, and what there is, is less focused on learning Swift and more about what&amp;rsquo;s happening in the community.&lt;/p&gt;
&lt;p&gt;Being firmly in the camp of needing to learn more about the language, I&amp;rsquo;ve listen to a number of older podcasts, or even current ones (such as Fireside) but their older episodes. It is sort of an odd experience traveling on several slightly out of sync timelines, but quite a joy to see what happens to predictions - like the occasion when &lt;a href="https://twitter.com/twostraws"&gt;Paul Hudson&lt;/a&gt; predicts that an &amp;ldquo;Xcode lite&amp;rdquo; on iPad is unlikely to be able to write apps until a more swift like framework for developing interfaces exists.&lt;/p&gt;
&lt;p&gt;One of the podcasts I&amp;rsquo;m working through by every episode is Paul&amp;rsquo;s &lt;a href="https://podcasts.apple.com/au/podcast/swift-over-coffee/id1435076502"&gt;Swift Over Coffee&lt;/a&gt;. The first season he was paired up with &lt;a href="https://seanallen.co/"&gt;Sean Allen&lt;/a&gt;, but I&amp;rsquo;ve just started the second season with &lt;a href="https://ericasadun.com"&gt;Erica Sadun&lt;/a&gt;. When I first came across Sean it took me a while to warm to his enthusiastic voice, but what I loved about him was he was never reluctant to ask Paul to explain something - usually something I needed explained as well. The dynamic of an expert (and expert teacher) co-hosting with a relative newbie was a great combination for me.&lt;/p&gt;
&lt;p&gt;Erica is a giant in the Swift community, and she has a deep understanding and an wide knowledge of Swift topics, so she&amp;rsquo;s going to be great. In the first episode she and Paul riffed on a heap of interesting topics with great enthusiasm and clear enjoyment, but I did miss Sean asking for explanations!&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 4</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-4/</link><pubDate>Sat, 03 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-4/</guid><description>&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-3.jpg" width="417" alt="This image has an empty alt attribute; its file name is psm\_v10\_d562\_the\_hindoo\_earth-3.jpg"&gt;
&lt;p&gt;Then the last trick for for decomposing the views, is to remember we can pass values when we init a struct. So something like this:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 VStack{
 GreenPaddedText(text: &amp;quot;Hello&amp;quot;)
 GreenPaddedText(text: &amp;quot;world&amp;quot;)
 }
}


struct GreenPaddedText: View {
 var text: String

 var body: some View {
 Text(text)
 .foregroundColor(.green)
 .padding()
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;This is probably my favourite - because although in this example I&amp;rsquo;ve created the mini-view struct in the body, if it&amp;rsquo;s a building block I can use elsewhere in a different view, it&amp;rsquo;s super portable.&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 3</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-3/</link><pubDate>Fri, 02 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-3/</guid><description>&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-3.jpg" width="472" alt=""&gt;
&lt;p&gt;The next part of day 23 started to make my brain hurt a bit. It&amp;rsquo;s easy to imagine that when presenting a complex screen - perhaps some data from a source as a mixture of images and text loaded from a database into a scroll-able view, that the view may start to get complex. Then it becomes good practice to decompose the views to make the code clearer, less error prone, and to avoid any unnecessary repetition.&lt;/p&gt;
&lt;p&gt;Paul&amp;rsquo;s first suggestion is to pull some parts of the view as &lt;em&gt;properties&lt;/em&gt; of the same view.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let greenText = Text(&amp;quot;Hello&amp;quot;).foregroundColor(.green)

var body: some View {
 VStack{
 greenText
 greenText
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;This works fine, and exactly how you expect, except that if you don&amp;rsquo;t enclose it in the VStack, you just get one Text, but two ContentPreviews. I do not understand why yet, but its probably something to do with the @ViewBuilder property wrapper.&lt;/p&gt;
&lt;p&gt;But&amp;hellip; a property can&amp;rsquo;t refer to another property, so this isn&amp;rsquo;t compilable Swift:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-8.24.46-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;To get around this, we can use a computed property. So this works:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@State private var greeting = &amp;quot;Hello&amp;quot;

var greenText: some View {
 Text(greeting).foregroundColor(.green)
}

var body: some View {
 VStack{
 greenText
 greenText
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;In fact, this is what I&amp;rsquo;ve been doing so far to decompose views, although I&amp;rsquo;ve been dropping the segments underneath the main body to make things subjectively neater.&lt;/p&gt;
&lt;p&gt;Paul cautions about returning multiple views in this computed property manner. So this does not work:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-8.33.20-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That makes sense - we were relying on the implied return. Putting them in a VStack would work - because we&amp;rsquo;re just returning a single view (the VStack) which happens to contain multiple views.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@State private var greeting = &amp;quot;Hello&amp;quot;

var body: some View {
 VStack{
 greenText
 greenText
 }
}

var greenText: some View {
 VStack{
 Text(greeting).foregroundColor(.green)
 Text(greeting).foregroundColor(.green)
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s another view container type called Group which is like a stack, but just contains, rather than arranging, a collection of views, that can be used in the same way.&lt;/p&gt;
&lt;p&gt;Alternatively, and I assume this is related to the problem I had above, we can just wrap the property with the @ViewBuilder attribute.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@State private var greeting = &amp;quot;Hello&amp;quot;

var body: some View {
 VStack{
 greenText
 greenText
 }
}

@ViewBuilder var greenText: some View {
 Text(greeting).foregroundColor(.green)
 Text(greeting).foregroundColor(.green)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 2</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-2/</link><pubDate>Thu, 01 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-2/</guid><description>&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-2.jpg" width="484" alt=""&gt;
&lt;p&gt;Although &amp;ldquo;immutable&amp;rdquo; the view structs can contain some control statements such as if/then and for loops. So this is quite legal, and useful.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State private var likesGreen = true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 if likesGreen {
 Text(&amp;quot;Hello World&amp;quot;)
 .background(.green)
 }
 else
 {
 Text(&amp;quot;Hello World&amp;quot;)
 .background(.blue)
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;But Paul cautions against this, saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/conditional-modifiers"&gt;You can often use regular if conditions to return different views based on some state, but this actually creates more work for SwiftUI – rather than seeing one Button being used with different colors, it now sees two different Button views, and when we flip the Boolean condition it will destroy one to create the other rather than just recolor what it has.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead, he encourages the use of the ternary operator in modifiers for these situations. The ternary operator is like an if/then/else statement packaged up neatly. So the code above becomes:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State private var likesGreen = true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 Text(&amp;quot;Hello World&amp;quot;)
 .background(likesGreen ? .green : .blue)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;He&amp;rsquo;s not arguing that it&amp;rsquo;s neater, but also that it&amp;rsquo;s more efficient - that the first version has to destroy and create a Text when the value of the flag changes, whereas the second one just changes the colour. I assume he&amp;rsquo;s correct, but it&amp;rsquo;s not obvious why that would be so. It must be in the magic of how SwiftUI optimises how and when it renders each part of a view.&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 1</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-1/</link><pubDate>Wed, 31 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-1/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I found this one of the trickier days, so I&amp;rsquo;ll write it out to clear up my thinking.&lt;/p&gt;
&lt;p&gt;To draw to to screen in SwiftUI, we don&amp;rsquo;t call a command to draw on a canvas or window. Rather, a &lt;em&gt;view&lt;/em&gt; is defined as an immutable struct of type some View. Here&amp;rsquo;s the simple one from the default Xcode project.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
Text(&amp;ldquo;Hello, world!&amp;rdquo;)
.padding()
}
}&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;body&lt;/em&gt; var must be present, and can contain up to ten views. This simple example only contains one view - a text box. There are container view types that can, well, contain other views - for example a &lt;em&gt;HStack&lt;/em&gt; which arranges its content horizontally.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
HStack {
Text(&amp;ldquo;Hello, world!&amp;rdquo;)
.padding()
Rectangle()
.frame(width: 100, height: 100, alignment: .center)
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;produces:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.20.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.20.54-pm.png" width="164" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Having defined our view struct, we don&amp;rsquo;t call for it to be rendered on the screen, SwiftUI is just going to do that for us whenever it thinks it needs to. This seems bizarre at first, but you get used to it. It will happen when it needs to - usually because the information that makes up the view has changed. We help that to happen by binding the views to their data in various ways. More on that another day.&lt;/p&gt;
&lt;p&gt;If you look back at the code above, you&amp;rsquo;ll see that views often have &lt;em&gt;modifiers&lt;/em&gt; attached to them. In our example the &lt;em&gt;padding()&lt;/em&gt; on the text and the &lt;em&gt;.frame()&lt;/em&gt; on the rectangle. There&amp;rsquo;s many different modifiers for all of the different view primitives. Many of them are common to different primitives, some are different. It&amp;rsquo;s possible to attach them to container views - in which case they are applied to all the views in the container. For example, if we move the .frame() to the HStack, like this:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
HStack {
Text(&amp;ldquo;Hello, world!&amp;rdquo;)
.padding()
Rectangle()
}
.frame(width: 100, height: 100, alignment: .center)
}
}&lt;/p&gt;
&lt;p&gt;We get&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.33.07-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.33.07-pm.png" width="188" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The order of the modifiers is important. You can think of each modifier that&amp;rsquo;s added as wrapping around the view and any previous modifiers. In this example, we&amp;rsquo;ve got the same text field with the &lt;em&gt;.padding()&lt;/em&gt; and &lt;em&gt;.background(.blue)&lt;/em&gt; modifiers. The left one has the padding first, then is wrapped in the blue background. The right one has the blue background applied first, then the padding.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.39.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.39.53-pm.png" width="192" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Project 2 - Guess the Flag</title><link>https://blog.iankulin.com/project-2-guess-the-flag/</link><pubDate>Wed, 31 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-2-guess-the-flag/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-23-at-8.26.59-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Another 100 Days project - the second tutorial one. This was once again a &amp;ldquo;V&amp;rdquo; design pattern (put everything in the view) and as I kept growing it, especially in the challenges, I had a growing sense of unease.&lt;/p&gt;
&lt;p&gt;New things for me was how image assets work - identifying them with strings is convenient, but I&amp;rsquo;m hoping there&amp;rsquo;s safer system later using enums or something to avoid runtime surprises. Also the alert dialog box system. I was wondering how this was going to work in a declarative framework. I do not really approve of modal dialogs in mobile UI&amp;rsquo;s but I guess they have their place. I appreciated the gradients and frosted glass effects -super simple to implement, and if done thoughtfully, pretty.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/GuessTheFlag"&gt;Source/Github&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Download a Directory from a GitHub Repo</title><link>https://blog.iankulin.com/download-a-directory-from-a-github-repo/</link><pubDate>Tue, 30 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/download-a-directory-from-a-github-repo/</guid><description>&lt;p&gt;For &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/guess-the-flag-introduction"&gt;Challenge 2&lt;/a&gt; in the 100 days, I needed to download a directory of flag images from Paul&amp;rsquo;s GitHub. He has all the projects as sub-directories of a single &amp;ldquo;Hacking With Swift&amp;rdquo; repo. I didn&amp;rsquo;t need to whole thing, just the directory with the images.&lt;/p&gt;
&lt;p&gt;Strangely, git does not have any simple way of doing this. Neither does GitHub - I assumed the web interface would have a &amp;ldquo;download as zip&amp;rdquo; option as it does for tags.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/7106012/download-a-single-folder-or-directory-from-a-github-repo"&gt;One&lt;/a&gt; of the popular solutions on StackOverflow was to use SVN, which GitHub supports and which does have this functionality. I much preferred &lt;a href="https://stackoverflow.com/users/11218031/avinash-thakur"&gt;Avinash Takur&amp;rsquo;s&lt;/a&gt; &lt;a href="https://stackoverflow.com/questions/7106012/download-a-single-folder-or-directory-from-a-github-repo/70729494#70729494"&gt;suggestion&lt;/a&gt; to use GitHub&amp;rsquo;s web based VSCode.&lt;/p&gt;
&lt;p&gt;To access their VSCode, change the .com in the repo url to .dev. For example, instead of https://github.&lt;strong&gt;com&lt;/strong&gt;/twostraws/HackingWithSwift/tree/main/SwiftUI/project2, go to https://github.&lt;strong&gt;dev&lt;/strong&gt;/twostraws/HackingWithSwift/tree/main/SwiftUI/project&lt;code&gt;2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-22-at-6.50.16-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done, right click on the directory to download it.&lt;/p&gt;</description></item><item><title>Challenge 1</title><link>https://blog.iankulin.com/challenge-1/</link><pubDate>Mon, 29 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/challenge-1/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-22-at-8.54.26-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m up to Challenge 1 of 100 Days of SwiftUI (&lt;a href="https://www.hackingwithswift.com/100/swiftui/19"&gt;Day 19&lt;/a&gt;) which is to make your own simple (no MVVM) version of the app built in the previous three days. It&amp;rsquo;s about as simple as can be whilst still feeling like a real app. Something I hadn&amp;rsquo;t done before was limiting the keyboard to numbers or adding a toolbar to close it, so that was nice.&lt;/p&gt;
&lt;p&gt;Something that&amp;rsquo;s not nice, is that when you touch into the text field to change the number, it&amp;rsquo;s not selected ready to type over (the way they always are in browser url fields) so you need to backspace over the previous entry. That&amp;rsquo;s the sort of anoying behaviour I don&amp;rsquo;t like. It seems (after some googling) there&amp;rsquo;s no straightforward way of addressing this in SwiftUI, with the best solution involving importing a package. I will come back to that because it is bugging me.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State private var fromDistance = 0.0
@State private var fromUnits = &amp;ldquo;meters&amp;rdquo;
@State private var toUnits = &amp;ldquo;kilometers&amp;rdquo;
@FocusState private var distanceIsFocused: Bool&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let distanceUnits = \[&amp;quot;meters&amp;quot;, &amp;quot;kilometers&amp;quot;, &amp;quot;yards&amp;quot;, &amp;quot;miles&amp;quot;\]

var toDistance: Double {
 var meters = 0.0
 // convert the from distance to meters
 switch fromUnits {
 case &amp;quot;meters&amp;quot;: meters = fromDistance
 case &amp;quot;kilometers&amp;quot;: meters = fromDistance\*1000
 case &amp;quot;yards&amp;quot;: meters = fromDistance\*0.9144
 case &amp;quot;miles&amp;quot;: meters = fromDistance\*1609.34
 default: assert(false)
 }
 // convert the meters to the from distance
 switch toUnits {
 case &amp;quot;meters&amp;quot;: return meters
 case &amp;quot;kilometers&amp;quot;: return meters/1000
 case &amp;quot;yards&amp;quot;: return meters/0.9144
 case &amp;quot;miles&amp;quot;: return meters/1609.34
 default : assert(false)
 }
 return 0.0
}


var body: some View {
 NavigationView {
 Form {
 
 Section {
 TextField(&amp;quot;Distance&amp;quot;, value: $fromDistance, format: .number)
 .keyboardType(.decimalPad)
 .focused($distanceIsFocused)
 Picker(&amp;quot;Units&amp;quot;, selection: $fromUnits) {
 ForEach(distanceUnits, id: \\.self) {
 Text($0)
 }
 }
 .pickerStyle(.segmented)
 }
 header: {
 Text(&amp;quot;Distance&amp;quot;)
 }
 
 Section {
 if toDistance &amp;lt; 9.9 {
 Text(&amp;quot;\\(toDistance, specifier: &amp;quot;%.4f&amp;quot;)&amp;quot;)
 } else if toDistance &amp;gt; 999 {
 Text(&amp;quot;\\(toDistance, specifier: &amp;quot;%.0f&amp;quot;)&amp;quot;)
 } else {
 Text(&amp;quot;\\(toDistance, specifier: &amp;quot;%.2f&amp;quot;)&amp;quot;)
 }

 Picker(&amp;quot;Units&amp;quot;, selection: $toUnits) {
 ForEach(distanceUnits, id: \\.self) {
 Text($0)
 }
 }
 .pickerStyle(.segmented)
 }
 header: {
 Text(&amp;quot;Converted Distance&amp;quot;)
 }
 
 }
 .navigationTitle(&amp;quot;Distance Conversion&amp;quot;)
 .toolbar {
 ToolbarItemGroup(placement: .keyboard) {
 Spacer()
 Button(&amp;quot;Done&amp;quot;) {
 distanceIsFocused = false
 }
 }
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/HSUnitConvert"&gt;Source on Github&lt;/a&gt;&lt;/p&gt;</description></item><item><title>$==Commitment</title><link>https://blog.iankulin.com/commitment/</link><pubDate>Sun, 28 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/commitment/</guid><description>&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-21-at-9.47.54-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Paul Hudson&amp;rsquo;s 100 Days of Swift UI course is free - the videos are on YouTube, all the reading and tasks are available on his website for free. Although I assumed he made his living from the prodigious number of Swift books he&amp;rsquo;s written, he doesn&amp;rsquo;t push them in the course (except for his free book &lt;a href="https://www.hackingwithswift.com/quick-start/swiftui"&gt;Swift UI by Example&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;He&amp;rsquo;s so unpushy, I didn&amp;rsquo;t realise till a few days ago that you could pay to become a &lt;a href="https://www.hackingwithswift.com/plus"&gt;&amp;ldquo;subscriber&amp;rdquo; to Hacking with Swift&lt;/a&gt;. Really, he&amp;rsquo;s too nice.&lt;/p&gt;
&lt;p&gt;There are some goodies that come with the + subscription (one I&amp;rsquo;m likely to use later is the book discount) , but really I was very happy to sign up for a year for a couple of reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paul is a major contributor to the Swift community, and especially to beginners like me. The world is a better place if he can afford to keep doing that.&lt;/li&gt;
&lt;li&gt;This extra commitment, which is a tiny fraction of what I could spend on a bootcamp, makes it more likely I&amp;rsquo;ll complete the course, and puts a time limit on it - if I don&amp;rsquo;t complete it in the year, I&amp;rsquo;ll feel compelled to pay again.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Int.times()</title><link>https://blog.iankulin.com/int-times/</link><pubDate>Sat, 27 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/int-times/</guid><description>&lt;p&gt;When writing &lt;a href="https://blog.iankulin.com/the-_-underscore/"&gt;yesterday&amp;rsquo;s post&lt;/a&gt; about iterating through a range or collection and using the underscore to throw away the item, I had in the back of my mind that there should be a more straightforward way of doing something a number of times.&lt;/p&gt;
&lt;p&gt;Just to re-iterate (lol), here&amp;rsquo;s the issue. If we want to print &amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo; three times, in Swift the simplest we can do is:&lt;/p&gt;
&lt;p&gt;for _ in 1&amp;hellip;3 {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;I had the idea, that this should really be a method of the Int type. And in fact I could write it as an extension that took a closure. Then we could just do this:&lt;/p&gt;
&lt;p&gt;3.times {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;That feels much more like the Swift way of doing things (although I probably picked it up during a brief flirtation with Ruby). Of course, I&amp;rsquo;d implement it with a while loop and a counter, so there&amp;rsquo;d still be the counter memory allocated, but only for an Int rather than the Array element type.&lt;/p&gt;
&lt;p&gt;With this system, the problem I was talking about yesterday:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for _ in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;would become:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;thingStrings.count.times {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;Which is, I admit, not amazingly better, but better, especially if the compiler is allocating the memory and filling it with each array value in the first example (which I don&amp;rsquo;t know if it is, but am increasingly interested in finding out).&lt;/p&gt;
&lt;p&gt;Feeling pretty pleased with myself for inventing this new Int method, I had a extra thought that in fact, the Swift community may already of invented this and incorporated it in the language, so I should google it first. It turns out it&amp;rsquo;s not aprt of the official language, but neither (unsurprisingly) am I the first to think of it.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a &lt;a href="https://stackoverflow.com/questions/30554013/what-is-the-shortest-way-to-run-same-code-n-times-in-swift"&gt;Stack Overflow answer&lt;/a&gt; to a question &amp;ldquo;What is the shortest way to run same code n times in Swift?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/30554013/what-is-the-shortest-way-to-run-same-code-n-times-in-swift"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-20-at-3.12.32-pm.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So shout out to &lt;a href="https://stackoverflow.com/users/4358829/matteo-piombo"&gt;Matteo Piombo&lt;/a&gt; for doing the work for my idea seven years before I had it! It&amp;rsquo;s still just for code clarity, but great use of extensions and closures.&lt;/p&gt;
&lt;p&gt;I still maintain that &lt;code&gt;for _ in&lt;/code&gt; is not great, and that &lt;code&gt;for each in&lt;/code&gt; where each was a synonym for the underscore would be the prettiest solution. A likely con of this proposal is that is would be a code breaking change for any code that has already uses &lt;code&gt;for each&lt;/code&gt; which could be quite common.&lt;/p&gt;</description></item><item><title>The _ Underscore</title><link>https://blog.iankulin.com/the-_-underscore/</link><pubDate>Fri, 26 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/the-_-underscore/</guid><description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/The_Undertaker"&gt;&lt;img src="https://blog.iankulin.com/images/undertaker_with_fire.jpg" width="84" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve learned (so far) an underscore can be used for a couple of things in Swift, both of them loosely translating to &amp;ldquo;doesn&amp;rsquo;t really matter&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The first is in a parameter name for a function. Swift has a very cool feature I haven&amp;rsquo;t seen before where an argument can have a different internal and external name. As usual, this will make more sense in code. Imagine this:&lt;/p&gt;
&lt;p&gt;func sumNumbers(firstNumber: Int, secondNumber: Int) -&amp;gt; Int {
return firstNumber + secondNumber
}&lt;/p&gt;
&lt;p&gt;let sum = sumNumbers(firstNumber: 5, secondNumber: 3 )&lt;/p&gt;
&lt;p&gt;Using the magic of internal and external parameter names, we can make the function call read a bit better. The external parameter name goes first and is separated from the internal name by a space.&lt;/p&gt;
&lt;p&gt;func sumNumbers(numbers firstNumber: Int, and secondNumber: Int) -&amp;gt; Int {
return firstNumber + secondNumber
}&lt;/p&gt;
&lt;p&gt;let sum = sumNumbers(numbers: 5, and: 3 )&lt;/p&gt;
&lt;p&gt;That makes the function call a little clearer, but we can go one better by making the external name of the first parameter optional using the underscore:&lt;/p&gt;
&lt;p&gt;func sumNumbers(_ firstNumber: Int, and secondNumber: Int) -&amp;gt; Int {
return firstNumber + secondNumber
}&lt;/p&gt;
&lt;p&gt;let sum = sumNumbers(5, and: 3 )&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s the first use - to make a parameter of a function unnamed in the call.&lt;/p&gt;
&lt;p&gt;The second is in loops where we don&amp;rsquo;t care about the item we&amp;rsquo;re extracting. Normally we want to do something with each item in a collection we&amp;rsquo;re iterating through. For example, we might want to print each string in an array:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for thing in thingStrings {
print(thing)
}&lt;/p&gt;
&lt;p&gt;But what if we didn&amp;rsquo;t? Perhaps this is what we&amp;rsquo;re doing:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for thing in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve sort of created a variable for no purpose - it&amp;rsquo;s not referenced. If that&amp;rsquo;s our intention, it would be better to have the compiler know that so if we inadvertently use it, it would warn us. The underscore comes to help with this, we can just:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for _ in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;I have no idea if this results in better binary code - ie if LVM was reserving memory for &lt;code&gt;thing&lt;/code&gt; in the previous example. Knowing Chris Lattner, it probably is smarter than that.&lt;/p&gt;
&lt;p&gt;Although one quickly gets used to reading code like the above example, I don&amp;rsquo;t really like this use of the underscore. The code does not read well, it&amp;rsquo;s not Swifty enough. I&amp;rsquo;d probably say:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for eachThing in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;Thinking about this today whilst driving back from shopping, I hit on the idea there should be a reserved word &amp;ldquo;each&amp;rdquo; to achieve the same purpose as the underscore, but perhaps the code above is as good as we can get. It will do until I find out if LVM is optimising it away. (&lt;a href="https://www.hackingwithswift.com/sixty/4/1/for-loops"&gt;Paul says, obliquely, it is not&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not forgetting about the .forEach method on arrays. But that has the same issue - it forces me to name (or underscore) the variable to hold the value we&amp;rsquo;re iterating past.&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;thingStrings.forEach { _ in
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;</description></item><item><title>Uwrap App</title><link>https://blog.iankulin.com/uwrap-app/</link><pubDate>Thu, 25 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/uwrap-app/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_2549.png" width="269" alt=""&gt;
&lt;p&gt;Part of the &lt;a href="https://twitter.com/twostraws"&gt;@twostraws&lt;/a&gt; programmatic universe is his Swift learning app, &lt;a href="https://apps.apple.com/us/app/unwrap/id1440611372"&gt;Unwrap&lt;/a&gt; that I&amp;rsquo;ve included in my learning goals. It presents little snippets of learning with a 60 second video, and in a written version, then tests the user to check their understanding. It is slightly gamified - you get points for answers, but it&amp;rsquo;s not clear to me how that works beyond the satisfying haptics when your score runs up at the end of a section.&lt;/p&gt;
&lt;p&gt;The tests so far (I&amp;rsquo;m up to Functions) have been code examples along with a &amp;ldquo;true or false&amp;rdquo; question for the set - often &amp;ldquo;This code is valid Swift&amp;rdquo;, but sometimes things like &amp;ldquo;This code prints four messages&amp;rdquo;. At first, I didn&amp;rsquo;t love the tests as they often didn&amp;rsquo;t test the thing I&amp;rsquo;d just learned. For example in the unit about for loops, the hidden error in some code might have been an undeclared variable being used. But the effect of this has been to make me look at each example carefully to look for errors - in the process I&amp;rsquo;ve learned the Swift syntax well (which I would otherwise have relied on Xcode to help me with), and sharpened my ability to spot errors (that I would otherwise have relied on the compiler to help me with).&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_2553.png" width="142" alt=""&gt;
&lt;p&gt;The screen shots here are from my SE2 iPhone - so on a sensible sized device the code may be a little easier to read (no it does not to landscape on the phone). I do value having it on the phone though - it&amp;rsquo;s perfect for making good use of tiny bites of time through the day which would be Swift learning free otherwise.&lt;/p&gt;
&lt;p&gt;Even if you&amp;rsquo;re not following one of Paul&amp;rsquo;s Hacking with Swift courses, the Unwrap app is a great way to polish your Swift knowledge.&lt;/p&gt;</description></item><item><title>Codewars / reduce</title><link>https://blog.iankulin.com/codewars-reduce/</link><pubDate>Wed, 24 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/codewars-reduce/</guid><description>&lt;p&gt;&lt;a href="https://www.codewars.com/"&gt;&lt;img src="https://blog.iankulin.com/images/1_0plbhkaulwnsx4u2mqyn2w.png" width="242" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.codewars.com/"&gt;codewars.com&lt;/a&gt; is a &amp;ldquo;coding practice&amp;rdquo; website. You chose a language and a skill level, then it offers up a task (or &lt;em&gt;kata&lt;/em&gt;) for you to write a suitable function. The first one it gave me was seemed too hard, so I changed my level to beginner and skipped to the next one. This was my task:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; Given an array of integers, find the one that appears an odd number of times.
 
 There will always be only one integer that appears an odd number of times.
 
 Examples
 [7] should return 7, because it occurs 1 time (which is odd).
 [0] should return 0, because it occurs 1 time (which is odd).
 [1,1,2] should return 2, because it occurs 1 time (which is odd).
 [0,1,0,1,0] should return 0, because it occurs 3 times (which is odd).
 [1,2,2,3,3,3,4,3,3,3,2,2,1] should return 4, because it appears 1 time (which is odd).
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I know there&amp;rsquo;s a cool &amp;ldquo;Set&amp;rdquo; container type in Swift, so my plan was to iterate through the array, then for each number if there&amp;rsquo;s no entry in the set, then add one, but if there is, remove it. That way whatever is left in the set at the end must be in the original array an odd number of times. I was pretty pleased with myself. Here&amp;rsquo;s the code I Playground&amp;rsquo;d up:&lt;/p&gt;
&lt;p&gt;func oddTimesInt(intArray: [Int]) -&amp;gt; Int {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var intSet: Set&amp;lt;Int&amp;gt; = \[\]

for number in intArray {
 if intSet.contains(number) {
 intSet.remove(number)
 } else {
 intSet.insert(number)
 }
}

assert(intSet.count == 1)
if let firstNumber = intSet.first {
 return firstNumber
} else {
 // this is guaranteed not to happen in the specification
 assert(false)
 return 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;While you are writing your code in their webpage, you can run it against a test suite. Mine passed the first time; even more pleased with myself.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re ready, you can submit your code. Now it runs against a much bigger test suite. It passed again; now my head is swelling a little at just how canny I am.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a chance to clean up, comment, or to refactor your code before it&amp;rsquo;s finally locked in. Then once you commit that, it shows you other people&amp;rsquo;s solutions. This was the top one:&lt;/p&gt;
&lt;p&gt;func findIt(_ seq: [Int]) -&amp;gt; Int {
seq.reduce(0, ^)
}&lt;/p&gt;
&lt;p&gt;lol. One. Line. That&amp;rsquo;ll learn me.&lt;/p&gt;
&lt;p&gt;So, anyways&amp;hellip;. I guess I learned there&amp;rsquo;s an array method called &lt;em&gt;reduce&lt;/em&gt;, and it reduces an array, but I want to argue my code is easier to understand.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/swift/array/reduce(_:_:)"&gt;According to the docs&lt;/a&gt;, &lt;em&gt;reduce&lt;/em&gt; &amp;ldquo;Returns the result of combining the elements of the sequence using the given closure.&amp;rdquo; Basically, the 0 in the example above is the inital value of the accumulator, then the closure repeatedly operates on the accumulaotr and each value of the array, then the final result in the accumulator is returned. An example will make a lot more sense. Here&amp;rsquo;s the one from the Apple documentation:&lt;/p&gt;
&lt;p&gt;let numbers = [1, 2, 3, 4]
let numberSum = numbers.reduce(0, { x, y in
x + y
})
// numberSum == 10&lt;/p&gt;
&lt;p&gt;The closure in the winning entry used all the closure redaction tricks I think I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/closures/"&gt;complained&lt;/a&gt; about before. We could make it a bit more readable by putting syntax back.&lt;/p&gt;
&lt;p&gt;func findIt(_ seq: [Int]) -&amp;gt; Int {
seq.reduce(0, {accumulator, element in accumulator ^ element} )
}&lt;/p&gt;
&lt;p&gt;or to go back another step:&lt;/p&gt;
&lt;p&gt;func superXOR(accumulator: Int, element:Int) -&amp;gt; Int {
return accumulator ^ element
}&lt;/p&gt;
&lt;p&gt;func findIt(_ seq: [Int]) -&amp;gt; Int {
seq.reduce(0, superXOR )
}&lt;/p&gt;
&lt;p&gt;The ^ operator is XOR. I know what that does, and could even manually do it on two binary numbers.&lt;/p&gt;
&lt;p&gt;This compact solution is based on knowing that XORing all the integers will leave the odd value, since XORing two identical numbers gives zero, and that carrying forward the XORed value of two different numbers will shake out to leave the number that only appears once. I was not going to think of that solution, even if I&amp;rsquo;d known &lt;em&gt;reduce&lt;/em&gt;() existed.&lt;/p&gt;</description></item><item><title>Sometimes the Gold is in the Comments</title><link>https://blog.iankulin.com/sometimes-the-gold-is-in-the-comments/</link><pubDate>Tue, 23 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sometimes-the-gold-is-in-the-comments/</guid><description>&lt;p&gt;I&amp;rsquo;m still not 100% clear on @ObservedObject v @StateObject. So when YouTube offered up this video, in which Paul promises during the intro that I&amp;rsquo;ll understand the data bindings by the end, I thought it would be the video for me.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/stSB04C4iS4?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;I guess I should really have twigged that I&amp;rsquo;d never heard of @ObjectBinding, but I pushed on to the 12 minute mark when he imports the &lt;em&gt;Combine&lt;/em&gt; framework. Hang on, what&amp;rsquo;s that?&lt;/p&gt;
&lt;p&gt;Checking out the comments, it all becomes clear.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-14-at-8.26.59-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Sadly, it&amp;rsquo;s a page down in the comments, when it&amp;rsquo;d be great to be pinned to the top.&lt;/p&gt;
&lt;p&gt;When we&amp;rsquo;re publishing content in a tech field that&amp;rsquo;s changing, there probably is probably some benefit to tagging it with versions - for example, my post on git is sort of useful now, but in three years? Who knows. Like this video of Paul&amp;rsquo;s it may be unhelpful and confusing.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll put some thought into how I might do that. Going back and updating things is probably not realistic (this is certainly the case for twostraws&amp;rsquo; videos - he&amp;rsquo;s prolific), but branding content with the versions of things might be. Wordpress dates content at the bottom of the post (on my current theme) so that would give some future traveler some chance, but the more novice they are (and currently my understanding of these topics is only helpful for novices) the less likely they are to be equipped to translate the date into the likelihood of the content being outdated. Perhaps I can use tags somehow. I&amp;rsquo;ll think about it.&lt;/p&gt;</description></item><item><title>Playgrounds are good</title><link>https://blog.iankulin.com/playgrounds-are-good/</link><pubDate>Mon, 22 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/playgrounds-are-good/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2778.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A couple of times (&lt;a href="https://blog.iankulin.com/protocols/"&gt;Protocols&lt;/a&gt; &amp;amp; &lt;a href="https://blog.iankulin.com/named-loops/"&gt;Named Loops&lt;/a&gt;) in the past few days I&amp;rsquo;ve needed to write and run a couple of tiny C or C++ snippets, and I&amp;rsquo;ve acutely felt the lack of Swift Playgrounds for it. It occurred to me that Playgrounds has been instrumental in my enjoyment of learning Swift - it&amp;rsquo;s just a bit magic to grab the closest device and noodle out an idea or to make sure I&amp;rsquo;ve understood a new concept.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/15152540.jpg" width="89" alt=""&gt;
&lt;p&gt;In a &lt;a href="https://podcasts.apple.com/us/podcast/episode-11-chris-lattner/id1505697997?i=1000478871841"&gt;conversation between Chris Lattner and Paul Hudson&lt;/a&gt; I&amp;rsquo;ve listened to recently, they discuss the value of &lt;a href="https://www.apple.com/swift/playgrounds/"&gt;Playgrounds&lt;/a&gt; and the excellent &lt;a href="https://docs.swift.org/swift-book/"&gt;Swift book&lt;/a&gt; in bringing the community along from Objective C. I could not agree more about the value of these two.&lt;/p&gt;
&lt;p&gt;As an alternative, I downloaded &lt;a href="https://apps.apple.com/in/app/c-programming-language/id499545918"&gt;C Language app&lt;/a&gt; for the iPad. This seems like a reasonable editor that uses an online compiler - it worked for my purpose although it wasn&amp;rsquo;t real snappy.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2779.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A couple of alternatives in the app store mentioned the ability to compile offline - which didn&amp;rsquo;t make sense to me until I pressed build on this one and realised it was using a server somewhere to do that work. So this may well not be the best one for AUD5.&lt;/p&gt;
&lt;p&gt;I was amazed to find heaps of online compilers for things which would have done the job just as well. I&amp;rsquo;ve bookmarked &lt;a href="https://itsourcecode.com/compile-code-run-using-online-compiler-ide-for-free/"&gt;this one&lt;/a&gt; which does all sorts of languages including C and Swift.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://itsourcecode.com/compile-code-run-using-online-compiler-ide-for-free/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-13-at-12.19.58-pm.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Named Loops</title><link>https://blog.iankulin.com/named-loops/</link><pubDate>Sun, 21 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/named-loops/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_2768.png" width="133" alt=""&gt;
&lt;p&gt;Here’s a neat thing I haven’t seen before. Other languages I’ve worked in haven’t had a neat way to break out of a set of nested loops to a particular loop. It’s not an issue that comes up a lot, but when it has I’ve solved it by creating a continue flag and having that as the first condition of each loop.&lt;/p&gt;
&lt;p&gt;To explain, say if we had these two loops (in C):&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string);&lt;/p&gt;
&lt;p&gt;for (i=0; i&amp;lt;=3; i++) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (j=0; j&amp;lt;length; j++) {
 printf(&amp;quot;char:%c num:%d\\n&amp;quot;, string\[j\], i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;and for some unexplained reason, we need to break out of both loops when we encounter a lowercase ‘t’. There is a C command to break out of a loop - &lt;em&gt;break&lt;/em&gt;. But it only breaks out of the current loop:&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string);&lt;/p&gt;
&lt;p&gt;for (i=0; i&amp;lt;=3; i++) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (j=0; j&amp;lt;length; j++) {
 printf(&amp;quot;char:%c num:%d\\n&amp;quot;, string\[j\], i);
 if (string\[j\] == 't') {
 break;
 } 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Since the outside loop that is iterating ‘i’ is not broken out of, we still end up looping through to the letter ‘t’ four times. Like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;char:T num:0
char:h num:0
char:i num:0
char:s num:0
char: num:0
char:s num:0
char:t num:0
char:T num:1
char:h num:1
char:i num:1
char:s num:1
char: num:1
char:s num:1
char:t num:1
char:T num:2
char:h num:2
char:i num:2
char:s num:2
char: num:2
char:s num:2
char:t num:2
char:T num:3
char:h num:3
char:i num:3
char:s num:3
char: num:3
char:s num:3
char:t num:3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So in C/C++ I would convert the loops to &lt;em&gt;while&lt;/em&gt;, and set a &lt;em&gt;continue&lt;/em&gt; flag. First the whiles:&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string);&lt;/p&gt;
&lt;p&gt;i = 0;
while (i &amp;lt;=3) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;j=0;
while (j&amp;lt;length) {
 printf(&amp;quot;char:%c num:%d\\n&amp;quot;, string\[j\], i);
 if (string\[j\] == 't') {
 break;
 } 
 j++;
}

i++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Then the flag, which I&amp;rsquo;ve called &lt;em&gt;keepGoing&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string) ;&lt;/p&gt;
&lt;p&gt;int keepGoing = true;
i=0;
while (keepGoing==true &amp;amp;&amp;amp; i &amp;lt;=3) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;j=0;
while (keepGoing==true &amp;amp;&amp;amp; j&amp;lt;length) {
 printf(&amp;quot;char: %c num: %d\\n&amp;quot;, string\[j\], i);
 if (string\[j\]=='t') {
 keepGoing = false;
 }
 j++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;This gives us the output we want and we can close the ticket. Note that I have&lt;br&gt;
typedef&amp;rsquo;d &lt;em&gt;true&lt;/em&gt; and &lt;em&gt;false&lt;/em&gt; off-screen in the code above.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;char:T num: 0
char:h num: 0
char:i num: 0
char:s num: 0
char: num: 0
char:s num: 0
char:t num: 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With Swift, we can just name the loops, then break out to a named loop level:&lt;/p&gt;
&lt;p&gt;littleLoop: for i in 0&amp;hellip;3 {
bigLoop: for char in &amp;ldquo;This string&amp;rdquo; {
print(&amp;ldquo;char:\(char) num:\(i)&amp;rdquo;)
if char==&amp;ldquo;t&amp;rdquo;{
break littleLoop
}
}
}&lt;/p&gt;
&lt;p&gt;Note that I didn&amp;rsquo;t need to name the internal &lt;em&gt;littleLoop&lt;/em&gt;, that was just showing off.&lt;/p&gt;</description></item><item><title>Checkpoint 9</title><link>https://blog.iankulin.com/checkpoint-9/</link><pubDate>Sat, 20 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-9/</guid><description>&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/-JmAbcISEmY?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;/*
Your challenge is this: write a function that accepts an
optional array of integers, and returns one randomly.
If the array is missing or empty, return a random number
in the range 1 through 100.&lt;/p&gt;
&lt;p&gt;If that sounds easy, it’s because I haven’t explained
the catch yet: I want you to write your function in a
single line of code. No, that doesn’t mean you should
just write lots of code then remove all the line breaks
– you should be able to write this whole thing in one
line of code.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-9"&gt;https://www.hackingwithswift.com/quick-start/beginners/checkpoint-9&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;*/&lt;/p&gt;
&lt;p&gt;func randomInt(_ numbers:[Int]?) -&amp;gt; Int { numbers?.randomElement() ?? Int.random(in:1&amp;hellip;100) }&lt;/p&gt;
&lt;p&gt;print(randomInt(nil))
print(randomInt([2,5]))&lt;/p&gt;</description></item><item><title>Visual Studio Code</title><link>https://blog.iankulin.com/visual-studio-code/</link><pubDate>Fri, 19 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/visual-studio-code/</guid><description>&lt;img src="https://blog.iankulin.com/images/visual_studio_code_1.35_icon.svg_.png" width="149" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve noticed &lt;a href="https://code.visualstudio.com/"&gt;Visual Studio Code&lt;/a&gt; in a few videos, and admired what a clean interface it had, and was impressed how opening a terminal window was automatically in the directory you were working in.&lt;/p&gt;
&lt;p&gt;I had a need to write some html/css, and some C++ in the last couple of days, so that seemed like a great excuse to give it a try. I&amp;rsquo;d have to say my opinion of it has only gone up. Clearly, it is right at home with HTML and CSS - code completion and syntax colouring all working nicely. I followed TechWithTim&amp;rsquo;s suggestion to install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer"&gt;Live Server extension&lt;/a&gt; - which was a completely painless experience.&lt;/p&gt;
&lt;p&gt;Same positive experience when I created a .cpp file, and VSC wanted me to take Microsoft&amp;rsquo;s advice about a bunch of extensions, and correctly suggested I build with Mac&amp;rsquo;s clang compiler.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-11-at-3.40.15-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So many things just work how you expect them to. Within seconds I was trying out breakpoints, compiling and managing the git commits. I even tried the Swift extension.&lt;/p&gt;
&lt;p&gt;I have no intention of leaving Xcode, but with a great tool like this, I will never be a &lt;a href="https://blog.iankulin.com/vim/"&gt;vim&lt;/a&gt; guy.&lt;/p&gt;</description></item><item><title>CSS Intro</title><link>https://blog.iankulin.com/css-intro/</link><pubDate>Thu, 18 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/css-intro/</guid><description>&lt;p&gt;When I wrote my last commercial HTML (in 1996 lol) I&amp;rsquo;m pretty sure there was no CSS. It was the land of textured backgrounds, blinking scrolling text, &amp;ldquo;under construction&amp;rdquo; gifs, and links to &lt;a href="https://en.wikipedia.org/wiki/Gopher_(protocol)"&gt;gopher&lt;/a&gt; URLs were not uncommon. So this is an area I need to update my skills a little just to carry on a coherent conversation in the developer world.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve bumped into a couple of &lt;a href="https://www.techwithtim.net/"&gt;Tech With Tim&lt;/a&gt; &lt;a href="https://www.youtube.com/c/TechWithTim/videos"&gt;videos&lt;/a&gt; recently, and I really liked his CSS intro for &amp;ldquo;Non-web developers&amp;rdquo;.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/ZzoAu4VPyho?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Checkpoint 8</title><link>https://blog.iankulin.com/checkpoint-8/</link><pubDate>Wed, 17 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-8/</guid><description>&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/Ga800-Qgft4?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;/*
Your challenge is this: make a protocol that describes a
building, adding various properties and methods, then
create two structs, House and Office, that conform to it.&lt;/p&gt;
&lt;p&gt;Your protocol should require the following:
A property storing how many rooms it has.
A property storing the cost as an integer
(e.g. 500,000 for a building costing $500,000.)
A property storing the name of the estate agent
responsible for selling the building.
A method for printing the sales summary of the building,
describing what it is along with its other properties.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-8"&gt;https://www.hackingwithswift.com/quick-start/beginners/checkpoint-8&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;*/&lt;/p&gt;
&lt;p&gt;protocol Building {
var rooms: Int {get set}
var cost: Int {get set}
var realEstateAgent: String {get set}
}&lt;/p&gt;
&lt;p&gt;extension Building {
func printSalesSummary(){
print(&amp;ldquo;Rooms: \(rooms) Cost: £\(cost) Agent: \(realEstateAgent)&amp;rdquo;)
}
}&lt;/p&gt;
&lt;p&gt;struct House: Building {
var rooms: Int
var cost: Int
var realEstateAgent: String
var occupants: Int
}&lt;/p&gt;
&lt;p&gt;struct Office: Building {
var rooms: Int
var cost: Int
var realEstateAgent: String
var floorArea: Int
}&lt;/p&gt;
&lt;p&gt;var myOffice = Office(rooms: 5, cost:500_000, realEstateAgent: &amp;ldquo;Bloggs&amp;rdquo;, floorArea: 500)
var myHouse = House(rooms: 2, cost:200_000, realEstateAgent: &amp;ldquo;Fletchers&amp;rdquo;, occupants: 1)&lt;/p&gt;
&lt;p&gt;myOffice.printSalesSummary()
myHouse.printSalesSummary()&lt;/p&gt;</description></item><item><title>Protocols</title><link>https://blog.iankulin.com/protocols/</link><pubDate>Tue, 16 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/protocols/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/protocoldroid-swe.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The evolution of structs into class-like things that can hold properties &lt;em&gt;and&lt;/em&gt; methods in Swift raised in my mind &amp;ldquo;what about inheritance?&amp;rdquo; - but no: structs in Swift can not use inheritance.&lt;/p&gt;
&lt;p&gt;Swift classes implement inheritance, but only from one class; there&amp;rsquo;s no multiple inheritance. Protocols neatly address both these concerns to a large extent, but perhaps before we look at how they work, we should have a brief diversion into inheritance in C++.&lt;/p&gt;
&lt;p&gt;#include &lt;iostream&gt;&lt;/p&gt;
&lt;p&gt;class Shape {
public:
int sides;
};&lt;/p&gt;
&lt;p&gt;class Drawable {
public:
virtual void draw() {}
};&lt;/p&gt;
&lt;p&gt;class Square : public Shape, public Drawable {
public:
Square(){
sides = 4;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; void draw() {
 std::cout &amp;lt;&amp;lt; &amp;quot;■&amp;quot; &amp;lt;&amp;lt; std::endl;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;};&lt;/p&gt;
&lt;p&gt;int main() {
Square square;
square.draw();
return 0;
}&lt;/p&gt;
&lt;p&gt;In the code above, there&amp;rsquo;s two classes &lt;em&gt;Shape&lt;/em&gt; and &lt;em&gt;Drawable&lt;/em&gt;. You could regard &lt;em&gt;Drawable&lt;/em&gt; as an &lt;em&gt;interface&lt;/em&gt; if you&amp;rsquo;re coming from Java world (because the method draw() is marked &lt;em&gt;virtual&lt;/em&gt; - there&amp;rsquo;s no implementation). The class &lt;em&gt;Square&lt;/em&gt; inherits from both those classes - it&amp;rsquo;s a new class that is a &lt;em&gt;Shape&lt;/em&gt;, but is also &lt;em&gt;Drawable&lt;/em&gt;. (Line 15 above)&lt;/p&gt;
&lt;p&gt;Perhaps in this program there&amp;rsquo;s other items such as images or text - ie not shapes which might also inherit from &lt;em&gt;Drawable&lt;/em&gt;. Elsewhere, we could have a method that had to draw a collection of things - it doesn&amp;rsquo;t care what the items are, as long as they are &lt;em&gt;Drawable&lt;/em&gt; - they have to implement the &lt;em&gt;Draw&lt;/em&gt;() method.&lt;/p&gt;
&lt;p&gt;Swift addresses most of these needs with Protocols. A protocol defines a set of properties and methods. Then a struct, class, or even enum can &lt;em&gt;conform&lt;/em&gt; with this protocol - they contain the same properties, and are required by the compiler to implement the methods from the protocol.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the Swift equivalent of the C++ above:&lt;/p&gt;
&lt;p&gt;protocol Shape {
var sides: Int { get set }
}&lt;/p&gt;
&lt;p&gt;protocol Drawable {
func draw()
}&lt;/p&gt;
&lt;p&gt;struct Square: Shape, Drawable {
var sides: Int = 4
func draw() {
print(&amp;quot;■&amp;quot;)
}
}&lt;/p&gt;
&lt;p&gt;let square = Square()
square.draw()&lt;/p&gt;
&lt;p&gt;So, that&amp;rsquo;s cool, but not amazing. The power is really that now we can write a function that takes anything that&amp;rsquo;s Drawable as if it was a real type. To wit:&lt;/p&gt;
&lt;p&gt;class IanClass: Drawable {
func draw() {
print(&amp;ldquo;Ian&amp;rdquo;)
}
}&lt;/p&gt;
&lt;p&gt;func drawAThing(_ thingToDraw: Drawable){
thingToDraw.draw()
}&lt;/p&gt;
&lt;p&gt;let squareStruct = Square()
let ianClass = IanClass()&lt;/p&gt;
&lt;p&gt;drawAThing(squareStruct)
drawAThing(ianClass)&lt;/p&gt;
&lt;p&gt;So the function &lt;em&gt;drawAThing()&lt;/em&gt; is happy to draw anything, as long as it conforms with the Drawable protocol by implementing the draw() method. It doesn&amp;rsquo;t even matter what it is - as in this example where we&amp;rsquo;ve passed it a struct on one occasion, and a class on another.&lt;/p&gt;
&lt;p&gt;Protocols are a great example of Swift being flexible while being type-safe.&lt;/p&gt;</description></item><item><title>Checkpoint 7</title><link>https://blog.iankulin.com/checkpoint-7/</link><pubDate>Mon, 15 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-7/</guid><description>&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-7"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-08-at-8.43.44-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;/*
Your challenge is this: make a class hierarchy
for animals, starting with Animal at the top,
then Dog and Cat as subclasses, then Corgi and
Poodle as subclasses of Dog, and Persian and Lion
as subclasses of Cat.&lt;/p&gt;
&lt;p&gt;But there’s more:
The Animal class should have a legs integer
property that tracks how many legs the animal has.
The Dog class should have a speak() method that
prints a generic dog barking string, but each of
the subclasses should print something slightly
different.
The Cat class should have a matching speak() method,
again with each subclass printing something
different.
The Cat class should have an isTame Boolean property,
provided using an initializer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-7"&gt;https://www.hackingwithswift.com/quick-start/beginners/checkpoint-7&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;*/&lt;/p&gt;
&lt;p&gt;class Animal {
var legs = 4
init(legs: Int) {
self.legs = legs
}
}&lt;/p&gt;
&lt;p&gt;class Dog: Animal {
func speak() { print(&amp;ldquo;woof&amp;rdquo;) }
}&lt;/p&gt;
&lt;p&gt;class Corgi: Dog {
override func speak() { print(&amp;ldquo;Your Majesty?&amp;rdquo;) }
}&lt;/p&gt;
&lt;p&gt;class Poodle: Dog {
override func speak() { print(&amp;ldquo;yip&amp;rdquo;) }
}&lt;/p&gt;
&lt;p&gt;class Cat: Animal {
var isTame: Bool&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init (isTame: Bool, legs: Int) {
 self.isTame = isTame
 super.init(legs: legs)
}

func speak() { print(&amp;quot;meow&amp;quot;) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;class Persian: Cat {
override func speak() { print(&amp;ldquo;hiss&amp;rdquo;) }&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init() { super.init (isTame: true, legs: 4 ) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;class Lion: Cat {
override func speak() { print(&amp;ldquo;rawr&amp;rdquo;) }&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init() { super.init (isTame: false, legs: 4 ) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;let lion = Lion()
print(lion.legs)&lt;/p&gt;</description></item><item><title>Scope Creep</title><link>https://blog.iankulin.com/scope-creep/</link><pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/scope-creep/</guid><description>&lt;p&gt;&lt;a href="https://www.inc.com/lolly-daskal/7-reasons-why-you-need-to-embrace-procrastination.html"&gt;&lt;img src="https://blog.iankulin.com/images/getty_492816326_104863.jpg" width="441" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In project management, and especially in programming &amp;ldquo;scope creep&amp;rdquo; refers to the common situation where what&amp;rsquo;s required to regard a project as finished keeps growing. Most commonly, in the form of extra features required to be added to an application. I&amp;rsquo;ve especially seen this when clients see early versions of applications and it prompts them to request things that were not in the original specification.&lt;/p&gt;
&lt;p&gt;My iOS development learning journey has been experiencing some of this as well. I started out with only four clear goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complete the Standford CS193p course&lt;/li&gt;
&lt;li&gt;Complete Hacking with SwiftUI&lt;/li&gt;
&lt;li&gt;Get an app in the app store&lt;/li&gt;
&lt;li&gt;Document my progress by blogging my learning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of the things I have been adding are easily justified - getting my head around git &amp;amp; GitHub make a lot of sense to add. Others (I&amp;rsquo;m looking at you the two hours I spent learning about customising my zsh shell), not so much.&lt;/p&gt;
&lt;p&gt;The overarching goal is to be a competent iOS developer (at something around the junior dev level) in a year of part time study. Which I think I&amp;rsquo;m probably on track for, but I do keep thinking of things to add. For example, my HTML knowledge pre-dates CSS, so that probably needs a weekend spent on it so I&amp;rsquo;m at least aware of what I don&amp;rsquo;t know. Similarly, websites are no longer based on PHP - I&amp;rsquo;m sure any CS grads would have some JavaScript skills in their toolkit. Also, as part of developing an application, I should probably be able to standup an SQL server on AWS or similar with the required security.&lt;/p&gt;
&lt;p&gt;Much to do.&lt;/p&gt;</description></item><item><title>@ObservedObject v @StateObject</title><link>https://blog.iankulin.com/observedobject-v-stateobject/</link><pubDate>Sat, 13 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/observedobject-v-stateobject/</guid><description>&lt;p&gt;The Youtube algorithm thinks I need to watch more MVVM videos, and it turns out it&amp;rsquo;s probably right. A day or two ago in an &lt;a href="https://blog.iankulin.com/simple-mvvm/"&gt;MVVM&lt;/a&gt; post using a super simple example, I stored the view model as a property of the view using the @ObservedObject wrapper, as I created it.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@ObservedObject var light = LightViewModel()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 VStack{
 Spacer()
 if light.isOn(){
 drawLitBulb
 }
 else{
 Image(systemName: &amp;quot;lightbulb.fill&amp;quot;).font(.system(size: 72))
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But then today, Youtube served me up this video from &lt;a href="https://www.youtube.com/c/BeyondOnesAndZeros/videos"&gt;BeyondOnesAndZeros&lt;/a&gt;&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/LntH6moCuo0?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;They start off with @ObservableObject, but then say that if the View Model is instantiated there, that this is repeated every time the View is recreated (which happens every time it&amp;rsquo;s redrawn).&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t really understand the property wrappers (like @ObservableObject) but I assume this was a type property - ie static. Apparently not. The &lt;a href="https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app"&gt;Apple documentation on managing data&lt;/a&gt; says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SwiftUI might create or recreate a view at any time, so it’s important that initializing a view with a given set of inputs always results in the same view. As a result, it’s unsafe to create an observed object inside a view. Instead, SwiftUI provides the &lt;a href="https://developer.apple.com/documentation/swiftui/stateobject"&gt;&lt;code&gt;StateObject&lt;/code&gt;&lt;/a&gt; attribute for this purpose.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So that code should use @StateObject, and @ObservedObject should be used where I pass it down into other view structs in the hierarchy.&lt;/p&gt;
&lt;p&gt;Paul Hudson gives a good example in his explanation on this topic &lt;a href="https://www.avanderlee.com/swiftui/stateobject-observedobject-differences/"&gt;here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Rust</title><link>https://blog.iankulin.com/rust/</link><pubDate>Fri, 12 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rust/</guid><description>&lt;img src="https://blog.iankulin.com/images/rustmemelovetriangle_297886754.jpg" width="375" alt=""&gt;
&lt;p&gt;It&amp;rsquo;s been exciting to see some of the modern language features in Swift - it&amp;rsquo;s a real joy to work in when I think back to my C++ days which was some of the last commercial programming I did.&lt;/p&gt;
&lt;p&gt;The buzz about Carbon got me wondering about other new languages and what might be going on with them. Rust seems to keep popping up in conversations so I thought I&amp;rsquo;d have a quick look.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/br3GIIQeefY?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;Also read &lt;a href="https://fasterthanli.me/articles/a-half-hour-to-learn-rust"&gt;A half-hour to learn Rust&lt;/a&gt; and &lt;a href="https://faq.sealedabstract.com/rust/"&gt;A Swift Guide to Rust&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Those optionals, type inference, type safety, and exhaustive switch/match statements sure look familiar. Ranges too, and although the infinite range looks cool I&amp;rsquo;m not sure of the use-case.&lt;/p&gt;
&lt;p&gt;Blocks evaluating to a result is cute. This is allowed:&lt;/p&gt;
&lt;p&gt;let x = {
let y = 1; // first statement
let z = 2; // second statement
y + z // this is the *tail* - what the whole block will evaluate to
};&lt;/p&gt;
&lt;p&gt;Notice the missing semicolon - that&amp;rsquo;s sugar for the &amp;lsquo;return&amp;rsquo; that would be otherwise needed. Perhaps only from habit, I do miss the semicolons when writing Swift. I&amp;rsquo;m sure I&amp;rsquo;ll get used to it, but currently I start to feel uncomfortable when spreading an expression out over multiple lines (for clarity) and just expecting LVM to figure it all out.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Button(&amp;quot;Toggle Light&amp;quot;, action: {
 light.toggle()}
 )
 .padding()
 .font(.title)
 .foregroundColor(.white)
 .background(Color.accentColor)
 .cornerRadius(10)
 .padding()
 Spacer()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Underscore as a &amp;ldquo;throwaway&amp;rdquo; value turns up, but in the guise of default for the match statement.&lt;/p&gt;
&lt;p&gt;Variable bindings, by default (with &amp;rsquo;let&amp;rsquo;) are immutable in Rust, but can be marked as &amp;rsquo;let mut&amp;rsquo; to make them fully variable, as with Swift&amp;rsquo;s &amp;lsquo;var&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;Rust has &amp;ldquo;traits&amp;rdquo;, which currently my knowledge of Swift can&amp;rsquo;t do justice to a compare and contrast, but it definitely has a superclassy feel - like protocols and extensions.&lt;/p&gt;
&lt;p&gt;They both have closures, but again, I&amp;rsquo;m getting out of my current depth on Swift to make any worthwhile comment.&lt;/p&gt;
&lt;p&gt;I do enjoy about Swift how clear it is to read, from what I&amp;rsquo;ve seen of Rust, that&amp;rsquo;s not so much the case there, I guess there&amp;rsquo;s some other trade off involved.&lt;/p&gt;</description></item><item><title>Simple MVVM</title><link>https://blog.iankulin.com/simple-mvvm/</link><pubDate>Thu, 11 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/simple-mvvm/</guid><description>&lt;p&gt;MVVM (Model-View-View Model) is an architectural pattern for apps that separates the data (Model) from the user interface (View). The communication between these two parts is facilitated by a View Model.&lt;/p&gt;
&lt;p&gt;Model &amp;lt;-&amp;gt; View Model &amp;lt;-&amp;gt; View&lt;/p&gt;
&lt;h3 id="model"&gt;Model&lt;/h3&gt;
&lt;p&gt;The &lt;em&gt;Model&lt;/em&gt; is platform independent - we should be able to pluck it out and add it to a different application running on a different platform without any trouble. Any business rules will be part of the Model along with the data. For example, if it&amp;rsquo;s a rule that every customer has a sales contact, this can be enforced in the Model.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-08-06-at-4.20.38-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-06-at-4.20.38-pm.png" width="85" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Model (or Models - an app could have more than one) does not know anything about the &lt;em&gt;View&lt;/em&gt; or the &lt;em&gt;View Model&lt;/em&gt;. In a SwiftUI app, we&amp;rsquo;ll almost always have the model in its own file.&lt;/p&gt;
&lt;p&gt;Our simple example app for this post is going to be a light bulb app. There will be a picture of a light bulb, and a button which will toggle the light off an on. It&amp;rsquo;s difficult to think of a simpler Model. This is what I&amp;rsquo;ve come up with.&lt;/p&gt;
&lt;p&gt;struct Light{
var on: Bool = false
}&lt;/p&gt;
&lt;p&gt;A Model in a real application could be massive - with connections to online data stores and complex business rules. Our light just has two exclusive states - off and on. We could make it more complex - it could be an incandescent light with a particular resistance and a formula for the brightness output for any particular voltage that was applied. All of that would go into the Model. But for today, we&amp;rsquo;ll just have &lt;em&gt;on&lt;/em&gt; or not.&lt;/p&gt;
&lt;p&gt;In all of the SwiftUI examples I&amp;rsquo;ve seen so far, the Model has been a struct. Perhaps it can be other things, but Swift has deep magic (structs are mysteriously immutable, so they are &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/why-state-only-works-with-structs"&gt;actually rebuilt when any properties change&lt;/a&gt;) for efficiently knowing if structs have changed, so perhaps not.&lt;/p&gt;
&lt;h3 id="view-model"&gt;View Model&lt;/h3&gt;
&lt;p&gt;The &lt;em&gt;View Model&lt;/em&gt; will have the &lt;em&gt;Model&lt;/em&gt; as a property. That way it can do things to the Model, and to access the bits it needs to pass off to the View. The View Model is always a class, as it needs to comply with the protocol of ObservableObject. If &lt;em&gt;protocol&lt;/em&gt; and &lt;em&gt;ObservableObject&lt;/em&gt; are foreign to you, don&amp;rsquo;t panic. You don&amp;rsquo;t need to understand any more than that the View needs to know when the View Model changes, so it observes the View Model, and for that magic to happen, the View Model needs to be a class having that ability (of being observed) which it gets from having the protocol ObservableObject.&lt;/p&gt;
&lt;p&gt;The View Model will also have properties or methods that the View can use to access the Model data. Remember the Model is completely hidden from the View, so the View Model provides that access. In a real situation, it would also do whatever translation or packaging was required to make the View&amp;rsquo;s life easy. In our example it is rather simple.&lt;/p&gt;
&lt;p&gt;class LightViewModel: ObservableObject {
@Published private var lightBulb = Light(on: false)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func toggle() {
 lightBulb.on = !lightBulb.on
}

var isOn: Bool { return lightBulb.on }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Again, the View Model is in it&amp;rsquo;s own file. The only thing we haven&amp;rsquo;t mentioned is the &lt;em&gt;@Published&lt;/em&gt; used in the property for our Model. This is just part of the magic mentioned earlier in the discussion about &lt;a href="https://developer.apple.com/documentation/combine/observableobject"&gt;ObservableObject&lt;/a&gt; which allows the View to know that something has changed, and that it needs to react to this by rebuilding the View.&lt;/p&gt;
&lt;h3 id="view"&gt;View&lt;/h3&gt;
&lt;p&gt;The View is just our regular SwiftUI view. A crucial part is that it holds the View Model for the light as a property wrapped with the @ObservedObject. This completes out connections between the three parts of our architecture.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Model View is an @ObservableObject which has the Model as a @Published property&lt;/li&gt;
&lt;li&gt;The View contains the View Model as an @StateObject property&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These connections have these effects&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The View cannot ever access the Model directly&lt;/li&gt;
&lt;li&gt;If the Model changes, the View Model is aware, and broadcasts this in a way that the View knows about&lt;/li&gt;
&lt;li&gt;The View just redraws itself if that happens&lt;/li&gt;
&lt;li&gt;As the View does this, it asks the View Model for the parts of the data it needs&lt;/li&gt;
&lt;li&gt;This both protects and hides the Model from the view, and is an opportunity for the View Model to do any work it needs to to the data to make it easy for the View to put on the screen.&lt;/li&gt;
&lt;li&gt;Any user interaction that occurs in the View is passed to the View Model to deal with.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;struct ContentView: View {
@StateObject var light = LightViewModel()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 VStack{
 Spacer()
 if light.isOn {
 drawLitBulb
 }
 else {
 Image(systemName: &amp;quot;lightbulb.fill&amp;quot;).font(.system(size: 72))
 }
 Spacer()
 Button(&amp;quot;Toggle Light&amp;quot;, action: {
 light.toggle()}
 )
 .padding()
 .font(.title)
 .foregroundColor(.white)
 .background(Color.accentColor)
 .cornerRadius(10)
 .padding()
 }
}

var drawLitBulb: some View {
 // view of an iluminated bulb
 ZStack{
 Circle().fill(.yellow).frame(width: 150, height: 150)
 Image(systemName: &amp;quot;lightbulb&amp;quot;).font(.system(size: 72))
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;To make the code read nicely, the View Model is called &amp;ldquo;light&amp;rdquo;, and it&amp;rsquo;s an @StateObject so for the redrawing trigger to work correctly. The rest of the code should be reasonably clear if you&amp;rsquo;ve made a few SwiftUI views.&lt;/p&gt;
&lt;p&gt;We check if the light is on (by asking the View Model), if it is, we draw a lit bulb, if not, an unlit bulb is drawn. The last UI element is our Button() whose action is the toggle() method of light - our View Model.&lt;/p&gt;
&lt;p&gt;The source for this project is on &lt;a href="https://github.com/IanKulin/MVVMLight"&gt;GitHub here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Vim</title><link>https://blog.iankulin.com/vim/</link><pubDate>Wed, 10 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/vim/</guid><description>&lt;img src="https://blog.iankulin.com/images/1-_bwvjb2jzuuzyxgxm6xwqq.png" width="191" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve been working through the &lt;a href="https://missing.csail.mit.edu/"&gt;Missing Semester&lt;/a&gt; lectures from MIT, and recently completed the lecture about the &lt;a href="https://github.com/vim/vim"&gt;Vim editor&lt;/a&gt;. Vim is a test editor, called from the command line, and optimised for programming - in the sense that it assumes most of the use of the editor is navigating around a big text file making small changes rather than entering large amount of test.&lt;/p&gt;
&lt;p&gt;It uses simple, short key presses (as opposed to mouse movements or using menus or toolbars) to achieve things. This makes it highly efficient for good typists who know all the commands, and slightly incomprehensible to those who do not. An additional level of complexity is the idea of modes. Vim has several modes, the main ones being:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normal - for navigating around and making those little edits. To get into this mode press the esc key&lt;/li&gt;
&lt;li&gt;Insert - for entering text - ie the mode you&amp;rsquo;d assume you were in when opening an editor and not that you would have to press the letter &amp;lsquo;i&amp;rsquo; to make that happen&lt;/li&gt;
&lt;li&gt;Command - to run commands - like saving or closing VIM To get into this mode press the &amp;lsquo;:&amp;rsquo; key&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apart from &lt;a href="https://missing.csail.mit.edu/2020/editors/"&gt;this lecture&lt;/a&gt; here are many good guides for learning VIM, a couple I&amp;rsquo;ve looked at are &lt;a href="https://opensource.com/article/19/3/getting-started-vim"&gt;this one from OpenSource.com&lt;/a&gt; and &lt;a href="https://web.stanford.edu/class/cs107/resources/vim.html"&gt;this one from Stanford&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Even though I will never invest the time to become a power user of Vim, it&amp;rsquo;s available most places you&amp;rsquo;ll be using the command line, so the basics are a requirement for any programmer. Plus if you&amp;rsquo;re ever hired by a film production company to advise on a computer hacking scene, you&amp;rsquo;ll need it to scroll though some syntax highlighted Javascript.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.vimcheatsheet.com/"&gt;&lt;img src="https://blog.iankulin.com/images/iigrixvxp5ayn9ox7gr1dfi_rhlrotwllscafjjqjeq.webp" alt=""&gt;&lt;/a&gt;
&lt;em&gt;Great cheat sheet from &lt;a href="https://www.vimcheatsheet.com/"&gt;https://www.vimcheatsheet.com/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Firebase</title><link>https://blog.iankulin.com/firebase/</link><pubDate>Tue, 09 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/firebase/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-06-at-10.01.15-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In the category of &amp;ldquo;getting ahead of myself&amp;rdquo;, my list of potential future apps to develop includes an entry for a simple accounting system. I guess transactions could live in SQLite, on iCloud for sharing between Mac and mobile devices. Nevertheless, while noodling about it, I thought I should think about it being fully online, and I&amp;rsquo;d heard Google&amp;rsquo;s Firebase (probably referring for specifically to &lt;a href="https://firebase.google.com/products/firestore"&gt;Firestore&lt;/a&gt;) mentioned in several iOS developer podcasts and thought I should take a look.&lt;/p&gt;
&lt;p&gt;Having consumed a few &lt;a href="https://www.youtube.com/watch?v=XhD_Y6kLJqk"&gt;CodeWithChris videos&lt;/a&gt; on the topic, that certainly seems like it would fit the specifications. A simple SQL database on a device is probably a more realistic goal, but it leaves some syncing complications that a cloud based database at least partly addresses.&lt;/p&gt;</description></item><item><title>iOS Academy</title><link>https://blog.iankulin.com/ios-academy/</link><pubDate>Mon, 08 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-academy/</guid><description>&lt;p&gt;I seem to consume a lot of &lt;a href="https://iosacademy.io/"&gt;iOS Academy&lt;/a&gt; videos with great, short (&amp;lt; 10 minute) explanations of various Swift iOS programming topics - particularly little UI topics, like this one on the Grid View.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/A3jOHj9erQ4?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;I really appreciate the generous content provision in the Swift and iOS development community. Perhaps this is the same for lots of technologies, but for someone who started programming pre-internet, it&amp;rsquo;s a stark difference to how I used to learn - so many magazines, and so many 2&amp;quot; thick books.&lt;/p&gt;</description></item><item><title>MVVM Explained</title><link>https://blog.iankulin.com/mvvm-explained/</link><pubDate>Sun, 07 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/mvvm-explained/</guid><description>&lt;p&gt;The first nine minutes of &lt;a href="https://www.youtube.com/watch?v=sLHVxnRS75w"&gt;this video&lt;/a&gt; from &lt;a href="https://twitter.com/Its_Macco"&gt;Emmanuel Okwara&lt;/a&gt; finally gave me a clear understanding of the difference between MVC and MVVM.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/sLHVxnRS75w?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;In both MVC and MVVM the data &amp;amp; logic (Model) are separated from the part that the user interacts (View). Usually the View is a screen with controls and so on, but that&amp;rsquo;s not compulsory - for example a voice mail app interface would be all audio and DTMF. The point is that in both, the user interface (view) does not mess directly with the data (model) - it has to go through some sort of gatekeeper.&lt;/p&gt;
&lt;p&gt;The new understanding I got from Emmanuel is that in MVVM, the View Model does not know what is in the View. It does not alter the view, just broadcasts that there&amp;rsquo;s been a change and lets the view go ahead and update itself. It makes sense that this would be the paradigm for SwiftUI&amp;rsquo;s declarative interface style, and is also (I imagine but actually have no idea) the basis for React.js&lt;/p&gt;
&lt;p&gt;One thing Emmanuel mentions that I&amp;rsquo;m not clear on is that each View will have it&amp;rsquo;s own View Controller. Currently all my tiny apps have had one view in the SwiftUI sense. I have pulled out sub views, and some have had views within views - for example with the Navigation View. So I guess my question would be, &amp;ldquo;What constitutes a View in MVVM?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I noped out at the nine minute mark as soon as Interface Builder showed it&amp;rsquo;s face. I&amp;rsquo;m an iOS15 SwiftUI baby - I will, eventually, need to learn the old magic, but competence developing iOS apps using SwiftUI current methods is the MVP.&lt;/p&gt;
&lt;p&gt;With this understanding, and having finished lecture 4 from the cs193p series, I think a good project for today would be the simplest possible MVVM app with the correct separations and bindings.&lt;/p&gt;</description></item><item><title>Oh My Zsh</title><link>https://blog.iankulin.com/oh-my-zsh/</link><pubDate>Sat, 06 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/oh-my-zsh/</guid><description>&lt;p&gt;I&amp;rsquo;ve been playing in the zsh shell since I started on the &lt;a href="https://blog.iankulin.com/missing-semester/"&gt;Missing Semester&lt;/a&gt;, and was wondering how to get my git branch name in the prompt. A few googles later, I&amp;rsquo;ve installed Oh My Zsh, and added the git and macos plugins. Pretty.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-02-at-7.26.08-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Missing Semester</title><link>https://blog.iankulin.com/missing-semester/</link><pubDate>Fri, 05 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/missing-semester/</guid><description>&lt;p&gt;On my git odyssey yesterday, I came across which is an MIT class for CS about practical things CS students don&amp;rsquo;t strictly need for their degree, but will greatly benefit from. I was interested in their git introduction, but they explain &lt;a href="https://missing.csail.mit.edu/"&gt;the course&lt;/a&gt; by saying:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://missing.csail.mit.edu/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-01-at-8.50.12-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Videos of the lectures, and all the course notes and assignments are freely made available. I&amp;rsquo;ve only watched the first lecture about the shell, and their git lecture. Both were excellent, so I&amp;rsquo;ll add this series to my goals.&lt;/p&gt;</description></item><item><title>Gitting the hang of it</title><link>https://blog.iankulin.com/gitting-the-hang-of-it/</link><pubDate>Thu, 04 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gitting-the-hang-of-it/</guid><description>&lt;p&gt;&lt;a href="https://xkcd.com/1597/"&gt;&lt;img src="https://blog.iankulin.com/images/git_2x.png" width="253" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I spent most of the day learning about, and practicing with git. I&amp;rsquo;ll list some of the resources at the bottom, but for the moment, this is my understandings / cheat sheet for git. Since this could conceivably turn up in someone&amp;rsquo;s google search, and slightly less conceivably be of some use, I will come back and edit it if there&amp;rsquo;s something bad/wrong here. Comments would be great if you think that&amp;rsquo;s the case.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s most likely to be useful to someone using Xcode, GitHub, and the command line for git on MacOS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start a new repository (repo)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;on GitHub create the repo, it doesn&amp;rsquo;t matter if you create any files in it - they will be wiped shortly&lt;/li&gt;
&lt;li&gt;while you are on GitHub, grab the SSH address for the repo - it&amp;rsquo;s under the green &amp;ldquo;Code&amp;rdquo; button. If your username is IanKulin and the new repo is named GitTest it would look like this &lt;code&gt;[git@github.com](mailto:git@github.com):IanKulin/GitTest.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;create your Xcode (or whatever) project. I use the repo name as the project (and therefore directory name) but that&amp;rsquo;s not strictly necessary. I navigate to the Developer folder, so the new project is created as a folder inside that. Then in terminal inside that new directory:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;that marks it as a directory under source control, I usually drop a .gitignore in at this stage (more about that further down)&lt;/li&gt;
&lt;li&gt;we need to add the files and commit them to the local git repo:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git add -A&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git commit -m &amp;quot;Initial commit&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;now we connect the local repo to your github repo&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git remote add origin [git@github.com](mailto:git@github.com):IanKulin/GitTest.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;that just sets up the name so you can use &lt;em&gt;origin&lt;/em&gt; now to refer to the remote repo&lt;/li&gt;
&lt;li&gt;push the local files up to github with&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git push -u -f origin main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;main is the name of the branch - GitHub defaults to this. If you are looking at old tutorials they are probably using &lt;em&gt;master&lt;/em&gt; rather than &lt;em&gt;main.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Xcode&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;once a project is set up for git like this, Xcode will realise and use it to indicate changes in files, and mark them in the file navigator as &amp;ldquo;A&amp;rdquo; or &amp;ldquo;M&amp;rdquo; (need Added, or have been Modified). There is a Source Control menu that allows you to Add and Commit (more about these further down) to the local repo which work well, but I haven&amp;rsquo;t had any luck pushing up to GitHub with it - I do that from the command line. Doubtless this is something to do with the SSH key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;About .gitignore&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this config file is used by git to ignore the working files that shouldn&amp;rsquo;t be tracked - stuff like the different compile states and so on. Normally we&amp;rsquo;d only want the source, asset and make files etc - the stuff needed to recreate the project&lt;/li&gt;
&lt;li&gt;GitHub will create a default for the language you are using if you let it&lt;/li&gt;
&lt;li&gt;I usually add these lines to it:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;# MacOS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.DS_Store&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;to show hidden files like this in Finder, COMMAND-SHIFT-DOT and the same to rehide them&lt;/li&gt;
&lt;li&gt;if you do that, you&amp;rsquo;ll also see the .git folder which is where git does all it&amp;rsquo;s magic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;To check how things are going&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use terminal in the directory where the repo is&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;you&amp;rsquo;ll see any files changed, or that need to be added, and if the local files are ahead of the last clone/push but not if they are behind the remote (GitHub) - until you do a &lt;code&gt;git fetch&lt;/code&gt;, the local git doesn&amp;rsquo;t know what changes have happened on the remote&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;To add any files you&amp;rsquo;ve created in the project to version control from the CLI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git add .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;or of you just want to do a particular file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git add&lt;/code&gt; &lt;filename&gt;&lt;/li&gt;
&lt;li&gt;this adds the files to the &amp;ldquo;staging area&amp;rdquo;  - they are having changes tracked, but they have not been &amp;ldquo;committed&amp;rdquo; to the local repository, or the remote&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When done editing and adding files and they need to be &amp;ldquo;saved&amp;rdquo; to the local repository&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this is called committing them&lt;/li&gt;
&lt;li&gt;do from the XCode &lt;em&gt;Source Control&lt;/em&gt; menu, or from the CLI with&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git commit -a -m &amp;quot;Message for commit&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;now they&amp;rsquo;ve been added to the local repo. If you do a git status it will tell you you are ahead of the remote&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;To &amp;ldquo;push&amp;rdquo; all the changes up to GitHub from the CLI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git push origin main&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;origin&amp;rdquo; is the address for the GitHub repo, it was set for us when we cloned the repo. &amp;ldquo;main&amp;rdquo; is the branch we&amp;rsquo;re pushing to.&lt;/li&gt;
&lt;li&gt;You can&amp;rsquo;t just  create a new branch with this push?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;To tag all the files in the current local repo&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git tag -a v0.2 -m &amp;quot;Second  draft&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;v0.2&lt;/em&gt; is the tag and &lt;em&gt;&amp;ldquo;Second  draft&amp;rdquo;&lt;/em&gt; the description - change accordingly.&lt;/li&gt;
&lt;li&gt;This only does the local copy, so you need to push it to GitHub&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git push --tags&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;If you go and look on GitHub, the tags appear on the right under releases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To clone from a tag point&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git clone --depth 1 --branch v0.1 [git@github.com](mailto:git@github.com):IanKulin/TagTest.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;where v0.1 is the tag&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;--depth 1&lt;/code&gt; means you don&amp;rsquo;t get all the history with it&lt;/li&gt;
&lt;li&gt;alternatively, if you don&amp;rsquo;t need all the history you could just download the zip/tarball from GitHub&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some resources&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-push-an-existing-project-to-github"&gt;How to Push an Existing Project to GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://missing.csail.mit.edu/2020/version-control/"&gt;MIT Missing Semester lecture - Version Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/book/en/v2"&gt;Pro Git (book)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=DVRQoVRzMIY"&gt;Git Tutorial for Beginners - Git &amp;amp; GitHub Fundamentals In Depth&lt;/a&gt; (Tech with Tim video)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=USjZcfj8yxE"&gt;Learn git in 15 minutes&lt;/a&gt; - Colt Steele video&lt;/li&gt;
&lt;li&gt;&lt;a href="https://podcasts.apple.com/gb/podcast/ep-11-a-fail-story-for-every-topic/id1269435221?i=1000406471235"&gt;Fireside Swift podcast&lt;/a&gt; - Ep 11&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Chris Lattner</title><link>https://blog.iankulin.com/chris-lattner/</link><pubDate>Wed, 03 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/chris-lattner/</guid><description>&lt;p&gt;Thank you YouTube algorithm for this recommendation - Chris Lattner, the main author of Swift (amongst other things including LVM) chatting with Lex Fridman. Ignore the clickbait title. There is a good, brief discussion about the tradeoffs in value vs references types which is a topic I&amp;rsquo;ve been thinking a bit about this week.&lt;/p&gt;
&lt;p&gt;Also some interesting comments about how a language delivers it&amp;rsquo;s complexity. Chris gives the funny example of what &amp;ldquo;hello world&amp;rdquo; looks like in Swift vs C++. Here&amp;rsquo;s Swift: &lt;code&gt;Print(&amp;quot;Hello world&amp;quot;)&lt;/code&gt;, here&amp;rsquo;s C++:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main() {
 std::cout &amp;lt;&amp;lt; &amp;#34;Hello World!&amp;#34;;
 return 0;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Especially when part of my interest is in exciting kids in programming, that&amp;rsquo;s a stark difference. Swift does go on to do the hard things - it&amp;rsquo;s used for native apps and has some of the great modern language features, but the simple things are easy. I am very happy with the idea of Swift (especially plus Playgrounds) being a smooth introduction to coding. Less so with SwiftUI - that gets complicated quickly when things go wrong.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/UTFFR61xVbs?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Retain Cycle</title><link>https://blog.iankulin.com/retain-cycle/</link><pubDate>Tue, 02 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/retain-cycle/</guid><description>&lt;p&gt;Variables and constants in Swift can be a &lt;em&gt;value type&lt;/em&gt; (their data is copied when they are copied) or a &lt;em&gt;reference type&lt;/em&gt; (a pointer to the data is passed when they are copied.&lt;/p&gt;
&lt;p&gt;Structs, integers, and enums are value types, classes are reference types.&lt;/p&gt;
&lt;p&gt;Memory management of value types is relatively straightforward - there’s a 1:1 relationship between the variable name and its data, so if the variable goes out of scope it can get the chop. With reference types, it’s possible to have several variables (or class or struct properties etc) all pointing to the data, so a more sophisticated system is needed to know when it’s safe to delete the data.&lt;/p&gt;
&lt;p&gt;In Swift (and some other languages), this memory management is done by Automatic Reference Counting ARC. The compiler inserts code that keeps track of what references exist in scope that point to the data in memory that could potentially be freed. When there are zero references exisiting for an object, it can be freed. Here’s an example, meet SomeClass:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;class SomeClass{
 var name: String = &amp;#34;&amp;#34;
 var otherClass: SomeClass?
 
 init(name: String){
 self.name = name
 otherClass = nil
 print(&amp;#34;init&amp;#34;)
 }
 deinit{print(&amp;#34;deinit&amp;#34;)}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This class has a couple of properties; a name and a link to another object of the same class. The &lt;strong&gt;init&lt;/strong&gt; and &lt;strong&gt;deinit&lt;/strong&gt; methods are called at creation and destruction. I’ve added &lt;strong&gt;print()&lt;/strong&gt; so we can see them.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func classCreationFunction(){
 print(&amp;#34;start&amp;#34;)
 var class1 = SomeClass(name: &amp;#34;class1&amp;#34;)
 var class2 = SomeClass(name: &amp;#34;class2&amp;#34;)
 class1.otherClass = class2
 print(&amp;#34;end&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we run this function, the output will be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;start
init
init
end
deinit
deinit
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Two instances created, two released. All this is done for us (the Automatic in ARC), when I was programming in Delphi, it was often the responsibility of the programmer to explicitly deal with this problem.&lt;/p&gt;
&lt;p&gt;A potential problem with ARC is &lt;em&gt;retain cycles&lt;/em&gt;. A retain cycle is where objects (or often a chain of objects) hold references to each other. We’re done with the objects, but because they are holding the references to each other, the references have not been counted down to zero, and therefore ARC does not kill them off. In classCreationFunction above, one instance has a reference to another, and ARC cleans them both up.&lt;/p&gt;
&lt;p&gt;What happens if we have each instance hold a reference to each other? Something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func classCreationFunction(){
 print(&amp;#34;start&amp;#34;)
 var class1 = SomeClass(name: &amp;#34;class1&amp;#34;)
 var class2 = SomeClass(name: &amp;#34;class2&amp;#34;)
 class1.otherClass = class2
 class2.otherClass = class1
 print(&amp;#34;end&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The output of this is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;start
init
init
end
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Two instances of SomeClass created, none destroyed.&lt;/p&gt;</description></item><item><title>Bump One</title><link>https://blog.iankulin.com/bump-one/</link><pubDate>Mon, 01 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bump-one/</guid><description>&lt;p&gt;Most of the things I’ve learned so far have been familiar, interesting, or cool - but now I’ve ventured far enough into the Swift Language Programming book to find something that is definitely going to take a couple of readings to piece together.&lt;/p&gt;
&lt;p&gt;I was surprised, then pleased with functions as first class types, and the idea of passing closures around is powerful and useful.&lt;/p&gt;
&lt;p&gt;My current difficulty is getting my head around closures capturing variables. It was tolerable (but not safe) when I just thought of it as a pointer, but when turned out the captured variable continues to exist in some sort of zombie state even after the scope where the variable was contained has ended.&lt;/p&gt;
&lt;p&gt;To go back a bit, nested functions have access to the variables declared in the scope they are nested in.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func someFunction(){
 var someInt = 4
 nestedFunction()
 print(someInt)
 
 func nestedFunction(){
 someInt += 1
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I don’t approve of this. It has a global variable flavour. For most purposes I’d rather pass and return the values so the intent is all contained. Nevertheless, I can see it’s a valid approach that might be useful.&lt;/p&gt;
&lt;p&gt;I can’t explain the next bit any better than the Swift book, so here’s it’s opening on &lt;a href="https://docs.swift.org/swift-book/LanguageGuide/Closures.html"&gt;Capturing Values&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/34d3cb1a-730f-4afb-aee3-9c147dd3fabe.jpeg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here’s my slight edit to the code from the book to get it to print out the incrementing values. This prints ”10” and ”20” to the console.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func makeIncrementer(forIncrement amount: Int) -&amp;gt; () -&amp;gt; Int {
 var runningTotal = 0
 func incrementer() -&amp;gt; Int {
 runningTotal += amount
 return runningTotal
 }
 return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen())
print(incrementByTen())
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, makeIncrementer returns its nested incrementing function which has “captured” the runningTotal variable. The scope that runningTotal lives in has gone (when makeIncrementer finished) but runningTotal is still alive. I assume that this deep magic is made possible by our friend Automatic Reference Counting. This variable capture in closures seems like a weapon that needs wielded with great care.&lt;/p&gt;</description></item><item><title>Unwrap</title><link>https://blog.iankulin.com/unwrap/</link><pubDate>Sun, 31 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/unwrap/</guid><description>&lt;p&gt;Unwrap is the Paul Hudson app for Swift learning. It’s good for using those three minute gaps in life to digest a concept. I’ve incorporated it into my goals, as some days its the only progress I make.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/fa3cfadd-f6ef-4a05-9131-be5de8f38291.jpeg" width="501" alt=""&gt;</description></item><item><title>Xcode Refactor/Rename</title><link>https://blog.iankulin.com/xcode-refactor-rename/</link><pubDate>Sat, 30 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/xcode-refactor-rename/</guid><description>&lt;p&gt;This is cool. You can right click on a variable (and I guess any other) name and change it everywhere. No more tedious search and replace.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/SXSQgtGREKw?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>@ScaledMetric</title><link>https://blog.iankulin.com/scaledmetric/</link><pubDate>Fri, 29 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/scaledmetric/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-9.04.21-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I solved the problem (well, I googled a &lt;a href="https://stackoverflow.com/questions/72568296/sf-symbol-images-different-sizes"&gt;stackoverflow result&lt;/a&gt; to the problem) in the previous post about the different heights of the SF Symbols. The answer was to put them in a frame and lock the height. A problem that then arises from that is that when the user changes the text size, they&amp;rsquo;ll be out of wack. Apple&amp;rsquo;s solution to that, introduced in iOS 14 is the &lt;a href="https://developer.apple.com/documentation/swiftui/scaledmetric"&gt;@ScaledMetric property wrapper&lt;/a&gt; that does some magic I don&amp;rsquo;t fully understand yet.&lt;/p&gt;</description></item><item><title>Memorise Assignment 1</title><link>https://blog.iankulin.com/memorise-assignment-1/</link><pubDate>Thu, 28 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/memorise-assignment-1/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-7.33.03-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A small milestone achieved - I&amp;rsquo;ve completed the first assignment from the CS193p lecture series - some minor changes to the app being built in the lectures. There was a couple of things I was unhappy with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The text under the SF Symbols you can see in the preview above not being vertically aligned.&lt;/li&gt;
&lt;li&gt;Having duplicated code in my emoji arrays:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; let animalEmojis = [&amp;#34;🐠&amp;#34;, &amp;#34;🐢&amp;#34;, &amp;#34;🦋&amp;#34;, &amp;#34;🐥&amp;#34;, &amp;#34;🐣&amp;#34;, &amp;#34;🐰&amp;#34;, &amp;#34;🐝&amp;#34;, &amp;#34;🦄&amp;#34;, &amp;#34;🐵&amp;#34;, &amp;#34;🐛&amp;#34;]
 let weatherEmojis = [&amp;#34;🌪&amp;#34;, &amp;#34;🌝&amp;#34;, &amp;#34;🌈&amp;#34;, &amp;#34;🔥&amp;#34;, &amp;#34;🌧&amp;#34;, &amp;#34;🌙&amp;#34;, &amp;#34;🌬&amp;#34;, &amp;#34;☃️&amp;#34;, &amp;#34;☔️&amp;#34;, &amp;#34;🌫&amp;#34;]
 let transportEmojis = [&amp;#34;🚗&amp;#34;, &amp;#34;🚕&amp;#34;, &amp;#34;🚲&amp;#34;, &amp;#34;🚚&amp;#34;, &amp;#34;🛵&amp;#34;, &amp;#34;🚜&amp;#34;, &amp;#34;🛴&amp;#34;, &amp;#34;🛺&amp;#34;, &amp;#34;🚃&amp;#34;, &amp;#34;🚡&amp;#34;]

 // I&amp;#39;m not happy with this duplication //TODO
 @State var emojis = [&amp;#34;🐠&amp;#34;, &amp;#34;🐢&amp;#34;, &amp;#34;🦋&amp;#34;, &amp;#34;🐥&amp;#34;, &amp;#34;🐣&amp;#34;, &amp;#34;🐰&amp;#34;, &amp;#34;🐝&amp;#34;, &amp;#34;🦄&amp;#34;, &amp;#34;🐵&amp;#34;, &amp;#34;🐛&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This second problem is because I couldn&amp;rsquo;t just&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;@State var emojis = animalEmojis
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I tried it, I encountered the error:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Cannot use instance member &amp;#39;animalEmojis&amp;#39; within property initializer; property initializers run before &amp;#39;self&amp;#39; is available
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is vexing - the constants are defined on the lines above, so surely if this property exists, the ones before it do. Apparently that can&amp;rsquo;t be depended on - probably for some good reason that will be unveiled at some stage. It&amp;rsquo;s not because &lt;code&gt;emojis&lt;/code&gt; is wrapped in the &lt;code&gt;@State&lt;/code&gt; which probably does cause the variable to be created off somewhere else - I tried with just an ordinary var and had the same issue.&lt;/p&gt;
&lt;p&gt;Then I read further down into the assignment, there&amp;rsquo;s a &amp;ldquo;hints section&amp;rdquo; which I should clearly be reading before I being. C&amp;rsquo;s get degrees.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-7.42.54-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Message received. Don&amp;rsquo;t worry about the doubled up string array, and go back and watch the lecture again. There was something about locking an aspect ratio for an SF Symbol at some stage.&lt;/p&gt;</description></item><item><title>SwiftUI Essentials</title><link>https://blog.iankulin.com/swiftui-essentials/</link><pubDate>Wed, 27 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swiftui-essentials/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-4.12.38-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I hadn&amp;rsquo;t fully gotten my head around what&amp;rsquo;s going on with the declarative nature of SwiftUI, until I&amp;rsquo;d watched this video&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s from the &lt;a href="https://developer.apple.com/videos/play/wwdc2019/216/"&gt;2019 WWDC&lt;/a&gt; which is when (I guess) SwiftUI was new. I still don&amp;rsquo;t have a good handle on how the views are bound to their data, but there is a video from this same series about Data Flows which I imagine will also answer those questions.&lt;/p&gt;
&lt;p&gt;Of course, I could also have been pushing forwards on the two courses I&amp;rsquo;m undertaking - no doubt they are about to teach me these same things, but at least I&amp;rsquo;m procrastinating constructively.&lt;/p&gt;</description></item><item><title>iOS Dev Weekly</title><link>https://blog.iankulin.com/ios-dev-weekly/</link><pubDate>Tue, 26 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-dev-weekly/</guid><description>&lt;p&gt;Dave Verwer&amp;rsquo;s &lt;a href="https://iosdevweekly.com/"&gt;iOS Dev Weekly&lt;/a&gt; digest of links mainly about Swift libraries was mentioned on a podcast I was listening to last night - perhaps the &lt;em&gt;Swift with Sundell&lt;/em&gt; &lt;a href="https://www.swiftbysundell.com/podcast/16/"&gt;chat with Sommer Panage&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://iosdevweekly.com/issues/568?#start"&gt;first issue&lt;/a&gt; (it&amp;rsquo;s an email newsletter) arrived, and it&amp;rsquo;s pretty great. Not too long, chatty but on topic, and with links to follow for more info. As well as new or improved libraries, other topics are mentioned - I went down a rabbit hole on &lt;a href="https://useyourloaf.com/blog/swiftui-split-view-configuration/"&gt;SwiftUI Split View Configuration&lt;/a&gt;, ending up at this WWDC video about it.&lt;/p&gt;</description></item><item><title>Closures</title><link>https://blog.iankulin.com/closures/</link><pubDate>Mon, 25 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/closures/</guid><description>&lt;p&gt;I had one of those synchronicity in learning moments this morning. I am reading &lt;a href="https://docs.swift.org/swift-book/"&gt;The Swift Book&lt;/a&gt; - ie &lt;em&gt;The Swift Programming Language, Swift 5.7&lt;/em&gt; as part of my cs193p homework, and this morning, in a coffee shop was admiring what a clear, well written explanation was given for &lt;a href="https://docs.swift.org/swift-book/LanguageGuide/Closures.html"&gt;closures&lt;/a&gt;. It is super well written, stepping the reader through in logical (and digestible) steps.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve never carelessly passed around a pointer to a function and caused the Blue Screen of Death, or done much multi-threaded programming, the use-case for closures, and use of them is going to be challenging at first. Then Swift&amp;rsquo;s ability to cut the syntax down to very little will be challenging.&lt;/p&gt;
&lt;p&gt;By pure coincidence, on the way home (it&amp;rsquo;s the country - you have time to listen to most of a podcast on the drive home from town) I listened to &lt;em&gt;Fireside Swift&lt;/em&gt; podcast &lt;a href="https://podcasts.apple.com/gb/podcast/ep-7-i-cant-even-say-academic/id1269435221?i=1000406471223"&gt;episode 7&lt;/a&gt; which was also about closures.&lt;/p&gt;
&lt;p&gt;The to top it off, I sat down to lunch with this video which is part of &lt;a href="https://courses.iosacademy.io/"&gt;iOS Academy&lt;/a&gt;&amp;rsquo;s &lt;em&gt;Swift for Beginners (2022)&lt;/em&gt; &lt;a href="https://www.youtube.com/playlist?list=PL5PR3UyfTWvfacnfUsvNcxIiKIgidNRoW"&gt;playlist&lt;/a&gt;.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/8TpLDqOO6VE?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Xcode Tour</title><link>https://blog.iankulin.com/xcode-tour/</link><pubDate>Sun, 24 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/xcode-tour/</guid><description>&lt;p&gt;If you need a solid tour of the basics plus of Xcode, this is a great video from Karin Prater. Its the first video in her &amp;ldquo;Design-oriented course on SwiftUI&amp;rdquo;.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/EDHl1r5vw6Q?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Checkpoint 6</title><link>https://blog.iankulin.com/checkpoint-6/</link><pubDate>Sat, 23 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-6/</guid><description>&lt;pre tabindex="0"&gt;&lt;code&gt;/*
create a struct to store information about a car, including its model, number of seats, and current gear, then add a method to change gears up or down. Have a think about variables and access control: what data should be a variable rather than a constant, and what data should be exposed publicly? Should the gear-changing method validate its input somehow?
*/

struct Car {
 static let maxGear = 10
 static let minGear = 1
 var model = &amp;#34;no model&amp;#34;
 var seats = 4
 private (set) var currentGear = Car.minGear
 
 init (model: String, seats: Int) {
 self.model = model
 self.seats = seats
 }
 
 mutating func gearUp() {
 if currentGear &amp;lt; Car.maxGear{
 currentGear += 1
 }
 }
 
 mutating func gearDown() {
 if currentGear &amp;gt; Car.minGear{
 currentGear -= 1
 }
 }
 
}

var myUte = Car(model: &amp;#34;Rodeo&amp;#34;, seats:2)
print(&amp;#34;My \(myUte.model) has \(myUte.seats) seats and is in gear: \(myUte.currentGear)&amp;#34;)
myUte.gearDown()
print(&amp;#34;My \(myUte.model) has \(myUte.seats) seats and is in gear: \(myUte.currentGear)&amp;#34;)
myUte.gearUp()
print(&amp;#34;My \(myUte.model) has \(myUte.seats) seats and is in gear: \(myUte.currentGear)&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Swift Over Coffee</title><link>https://blog.iankulin.com/swift-over-coffee/</link><pubDate>Fri, 22 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swift-over-coffee/</guid><description>&lt;img src="https://blog.iankulin.com/images/screenshot-2022-07-17-at-07-44-36-swift-over-coffee-on-apple-podcasts.png" width="118" alt=""&gt;
&lt;p&gt;One of the iOS development podcasts in my current rotation is &amp;ldquo;Swift Over Coffee&amp;rdquo;, it&amp;rsquo;s blurb is:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Swift over Coffee is a podcast that helps you keep your Swift skills up to date the easy way, hosted by Paul Hudson and Erica Sadun. Each episode has news, our picks of the week, plus an open ballot where you can share your views on important topics.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And that is about how it goes. In Season One, it&amp;rsquo;s actually Paul and Sean Allen at the mic, they chat about news and topics related to Swift and iOS development, and each week there&amp;rsquo;s a Twitter question that people have chipped in on and the hosts go over these different views in some detail.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s only two seasons - in 2019 and 2020, but still there&amp;rsquo;s lots of good content for a beginning developer like me.&lt;/p&gt;</description></item><item><title>SF Symbols</title><link>https://blog.iankulin.com/sf-symbols/</link><pubDate>Thu, 21 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sf-symbols/</guid><description>&lt;p&gt;A couple of times in the App Development seminar I went to, we used system symbols in the place of images, and in his tutorial on Swift UI Basics, Sean Allen spent a few minutes talking about where they come from and how to choose them.&lt;/p&gt;
&lt;p&gt;First, here&amp;rsquo;s how they look in code - this is from the default Hello World app.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct ContentView: View {
 var body: some View {
 VStack {
 Image(systemName: &amp;#34;globe&amp;#34;)
 .imageScale(.large)
 .foregroundColor(.accentColor)
 Text(&amp;#34;Hello world&amp;#34;)
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-07-17-at-7.26.23-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-17-at-7.26.23-am.png" width="128" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;systemName&lt;/code&gt; parameter signifies the image is this type, and &lt;code&gt;&amp;quot;globe&amp;quot;&lt;/code&gt; is the name of the image. The code above draws a globe with some lat/long lines. So where does &amp;ldquo;globe&amp;rdquo; come from, and how can I find and choose them?&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where &amp;ldquo;SF Symbols&amp;rdquo; comes in. This is &lt;a href="https://developer.apple.com/sf-symbols/"&gt;an app&lt;/a&gt; containing a collection of over 4000 (in version 4) symbols that work well with the default San Francisco font, that can be scaled and in many cases coloured. These symbols are a kind of standard that (increasingly) users will recognise - so this also supports good user interfaces.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-17-at-6.50.30-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s straightforward to search for symbols and to view them in the colour combinations you&amp;rsquo;re using in your app (if that particular symbol supports it).&lt;/p&gt;</description></item><item><title>Passing Data</title><link>https://blog.iankulin.com/passing-data/</link><pubDate>Wed, 20 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/passing-data/</guid><description>&lt;p&gt;Sean Allen has come to my notice a couple of times, once where he was mentioned as freelance contractor who is a great contributor to the community (I think perhaps that was on &lt;a href="https://podcasts.apple.com/au/podcast/swiftcoders-interviews-with-swift-developers/id1082937962"&gt;Swiftcoders Podcast&lt;/a&gt;), and I&amp;rsquo;ve also bumped into him as co-host (with Paul Hudson) of the early episodes of the &amp;ldquo;&lt;a href="https://podcasts.apple.com/au/podcast/swift-over-coffee/id1435076502"&gt;Swift over Coffee&lt;/a&gt;&amp;rdquo; podcast.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/HXoVSbwWUIk?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;This video I watched last night is a compilation of the first few videos of &lt;a href="https://seanallen.teachable.com/p/swiftui-fundamentals"&gt;Sean&amp;rsquo;s SwiftUI course&lt;/a&gt;, and it&amp;rsquo;s pretty great. In particular he does a great job of explaining how to start to refactor child views out and call them, and how all the stacks go together to make a pretty interface. What he does not do is vist/explain any of the Swift language fundamentals. If you don&amp;rsquo;t already know what a struc is, and the Swift flavour of them, it may be a challenging place to start.&lt;/p&gt;
&lt;p&gt;In the last couple of tutorials he starts on the way the views are called, and how we can pass values into them. This is great marketing for me - it&amp;rsquo;s exactly where I&amp;rsquo;m up to in my journey - I&amp;rsquo;m perplexed about the structure of a SwiftUI app (where&amp;rsquo;s main?!) and the engine that&amp;rsquo;s watching when the UI needs updated and building the views. For example, I want to write a little hello world that just prints the time on the screen. I got this far:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct ContentView: View {
 let today = Date.now
 var body: some View {
 Text(today, style:.time)
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But then a minute later when it needs changed what happens? In traditional programming there would be a loop in main where I could check the minutes have changed, and force the redraw of the label.&lt;/p&gt;
&lt;p&gt;I feel this is the thing that&amp;rsquo;s going to be explained clearly in the next few videos of Sean&amp;rsquo;s course, which is cunningly behind the paywall. I&amp;rsquo;m very tempted, although my learning is already spread over two proper courses plus a shotgun blast of other reading, podcasts and self created programming tasks.&lt;/p&gt;
&lt;p&gt;I did have one moment of confusion when Sean passes down an @State variable to a child view and said the variable in the child view needed to be made @Binding. One of the top comments points this out as being unneeded and Sean agrees.&lt;/p&gt;</description></item><item><title>App Development in Swift Playgrounds</title><link>https://blog.iankulin.com/app-development-in-swift-playgrounds/</link><pubDate>Tue, 19 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/app-development-in-swift-playgrounds/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_1938.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;During the week I attended &amp;ldquo;App Development in Swift Playgrounds&amp;rdquo; run by &lt;a href="https://www.mattrichards.net.au/"&gt;Matt Richards&lt;/a&gt; with the support of some of the Apple team and hosted by &lt;a href="https://twitter.com/mssgellis"&gt;Dr Michelle Ellis&lt;/a&gt;. It was aimed a teachers looking at using Playgrounds for digi-tech teaching.&lt;/p&gt;
&lt;p&gt;The day included pulling apart one of the Playgrounds apps and rebuilding it - this being an example of a &amp;ldquo;top-down&amp;rdquo; approach - starting with a complete app and fiddling around with it - to better engage students. The alternative being a bottom-up approach where lesson one would be &amp;ldquo;good morning students, this is a variable, it can hold a value, it has a name we can use to refer to the value&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;This is of particular interest since in my learning journey I am trying to do both - I&amp;rsquo;m working through the &lt;em&gt;100 days of&lt;/em&gt;, as well as having a stab at building my first app. The &lt;a href="https://cs193p.sites.stanford.edu/"&gt;cs193p&lt;/a&gt; course also is tackling the problem from both ends - students start building the app in their first lecture, but also their first home reading assignment is 80% of the &lt;a href="https://docs.swift.org/swift-book/"&gt;Swift Programming Language book&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Clearly this top-down approach is highly engaging, but the cost is that when students (or me in this case) run into problems, they get super-complex super-quick. Even Swift noobs at the course were able to confidently alter the existing app to look quite different, but many (including me) ran aground in the afternoon when they tried to create an app to suit a different purpose.&lt;/p&gt;
&lt;p&gt;In any case, it was a great environment to learn in - being able to put your hand up and get expert assistance is a magical experience when you&amp;rsquo;ve previously been self-teaching and don&amp;rsquo;t even know the form of words you need to google to address your current problem, or that the answers you find at are a higher level than you can comprehend.&lt;/p&gt;
&lt;p&gt;Matt was a big proponent of using the example apps in the Playgrounds App Gallery to pinch ideas of how to accomplish things. For example, my app needed a scrolling list from where an entry could be selected and edited in another view, Matt straight away suggested I should have a look at the &amp;ldquo;Date Planner&amp;rdquo; app that had some similar functionality.&lt;/p&gt;</description></item><item><title>struct</title><link>https://blog.iankulin.com/struct/</link><pubDate>Mon, 18 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/struct/</guid><description>&lt;p&gt;Started on &lt;a href="https://www.hackingwithswift.com/100/swiftui/10"&gt;Day 10 of 100 days of etc etc&lt;/a&gt; today which is about structs. It was immediately clear when I first started looking at Swift and Swift UI that structs were going to be a big deal. I am used to structs being able to contain a collection of other types, but not methods. So I was confused at why tuples existed; that is now cleared up.&lt;/p&gt;
&lt;p&gt;If structs can have methods as well as properties, it answers the question of why tuples exist, but immediately asks the question, why have classes since structs have all this power? I already know (from my podcast consumption) one of the answers for this is that structs are value types rather than references. When you:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;let someConstant = someClass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;someConstant now contains a copy of the pointer to someClass, as opposed to making a copy as similar code would do for a struct. So this code:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;struct SomeStruct {
 var counter: Int = 0
}

var structInstance = SomeStruct()
var someOtherStruct = structInstance

print(&amp;#34;structInstance:\(structInstance.counter)&amp;#34;)
print(&amp;#34;someOtherStruct:\(someOtherStruct.counter)&amp;#34;)

someOtherStruct.counter += 1

print(&amp;#34;structInstance:\(structInstance.counter)&amp;#34;)
print(&amp;#34;someOtherStruct:\(someOtherStruct.counter)&amp;#34;)

print(&amp;#34;&amp;#34;)

class SomeClass {
 var counter: Int = 0
}

var classInstance = SomeClass()
var someOtherClass = classInstance

print(&amp;#34;classInstance:\(classInstance.counter)&amp;#34;)
print(&amp;#34;someOtherClass:\(someOtherClass.counter)&amp;#34;)

someOtherClass.counter += 1

print(&amp;#34;classInstance:\(classInstance.counter)&amp;#34;)
print(&amp;#34;someOtherClass:\(someOtherClass.counter)&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Produces the output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;structInstance:0
someOtherStruct:0
structInstance:0
someOtherStruct:1

classInstance:0
someOtherClass:0
classInstance:1
someOtherClass:1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The sample code for the Hello World app in playgrounds ONLY contains structs. The app is a struct, containing a view which is a struct. That&amp;rsquo;s basically all there is, so clearly structs are going to be a big deal.&lt;/p&gt;</description></item><item><title>Fireside Swift</title><link>https://blog.iankulin.com/fireside-swift/</link><pubDate>Sun, 17 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fireside-swift/</guid><description>&lt;p&gt;One of the ways I keep engaged in a topic is to listen to podcasts about it. Currently &lt;a href="https://podcasts.apple.com/au/podcast/fireside-swift/id1269435221"&gt;Fireside Swift&lt;/a&gt; is one of the Swift/SwiftUI/iOS Development podcasts that I have in the rotation.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/firesideswift.jpg" width="283" alt=""&gt;
&lt;p&gt;The blurb for the show is:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;Fireside Swift is a popular iOS Development podcast where four buddies discuss a new Swift programming topic each week. They try to stay informal while also conveying the information they know about each topic with bits of humor sprinkled throughout. Have a seat by the fire, and enjoy some nerdy discussion with friends!&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Which about sums it up. I listened to one later episode when the four hosts have a big ole chat before getting to the Swift topic, and although I didn&amp;rsquo;t love that - probably because I&amp;rsquo;m coming cold to it, when they got to the Swift topic it was pretty great - a good mix of experience and knowledge easily created a good conversation with some clear explanations.&lt;/p&gt;
&lt;p&gt;Currently there are four hosts, but I&amp;rsquo;ve subscribed and gone back to the beginning when the hosts are just Steve Bernard &amp;amp; Zach Falgout - they are getting into the meat of the Swift topic quicker (while still being very chatty and casual) but they don&amp;rsquo;t always get to the bottom of their subjects, but I&amp;rsquo;m still finding it great listening. In particular they seem to focus on topics that would be of interest to someone with experience in different languages.&lt;/p&gt;</description></item><item><title>Checkpoint 5</title><link>https://blog.iankulin.com/checkpoint-5/</link><pubDate>Sat, 16 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-5/</guid><description>&lt;pre tabindex="0"&gt;&lt;code&gt;/*
 Your input is this:
 
 let luckyNumbers = [7, 4, 38, 21, 16, 15, 12, 33, 31, 49]
 
 Your job is to:
 
 Filter out any numbers that are even
 Sort the array in ascending order
 Map them to strings in the format “7 is a lucky number”
 Print the resulting array, one item per line
 
 So, your output should be as follows:
 
 7 is a lucky number
 15 is a lucky number
 21 is a lucky number
 31 is a lucky number
 33 is a lucky number
 49 is a lucky number
 */

let luckyNumbers = [7, 4, 38, 21, 16, 15, 12, 33, 31, 49]

func isNumberOdd(number:Int) -&amp;gt; Bool {
 return number%2 == 1
}

let filteredNumbers = luckyNumbers.filter(isNumberOdd)

// this closure effectively does nothing
let sortedNumbers = filteredNumbers.sorted(by: {$0&amp;lt;$1}) 

let mappedNumbers = sortedNumbers.map({ String($0)+&amp;#34; is a lucky number&amp;#34; })

for i in 0..&amp;lt;mappedNumbers.count {
 print(mappedNumbers[i])
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>IanKulin</title><link>https://blog.iankulin.com/about/</link><pubDate>Sat, 16 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/about/</guid><description>&lt;p&gt;I&amp;rsquo;m Ian Bailey, a programmer/educator from Western Australia.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve revived my software development skills after a break from the industry by learning web and iOS development. I started this blog as an accountability hack while I was learning, but I enjoy writing, so I&amp;rsquo;ve continued to cover (mostly beginner) software, devops, and homelab topics.&lt;/p&gt;
&lt;p&gt;This was all originally hosted at devendevour.wordpress.com, but you know, wordpress. So now it&amp;rsquo;s a self-hosted Hugo repo.&lt;/p&gt;
&lt;p&gt;I hope you, or the bots, find something useful here.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/IanKulin"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Create an Empty Folder on GitHub</title><link>https://blog.iankulin.com/create-an-empty-folder-on-github/</link><pubDate>Fri, 15 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/create-an-empty-folder-on-github/</guid><description>&lt;p&gt;You can&amp;rsquo;t, but you can create a folder by adding a file through the web interface and using &lt;code&gt;&amp;lt;folder name&amp;gt;/&amp;lt;file name&amp;gt;&lt;/code&gt; - so add a README.md or whatever. Then you can drag your source into the new folder as normal.&lt;/p&gt;
&lt;p&gt;I learned this from Zack West &lt;a href="https://www.alpharithms.com/how-to-create-a-folder-in-github-repos-463022/"&gt;here&lt;/a&gt;, where there is also a better explanation with pictures.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-11-at-11.11.39-am.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Minimum Functionality for App Store</title><link>https://blog.iankulin.com/minimum-functionality-for-app-store/</link><pubDate>Fri, 15 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/minimum-functionality-for-app-store/</guid><description>&lt;p&gt;In my &lt;a href="https://blog.iankulin.com/app-idea/"&gt;post about&lt;/a&gt; my first app, EasterDay, I mentioned that it might be too trivial for an App Store submission. I&amp;rsquo;ve just been reading the App Store Review Guidelines, and there is a section on &lt;a href="https://developer.apple.com/app-store/review/guidelines/#minimum-functionality"&gt;Minimum Functionality&lt;/a&gt; that seems like it might be too pointless for acceptance.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-11-at-9.22.55-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll push on and build it anyway as I still need the rest of the learning experience, but may not submit it. Of course I&amp;rsquo;ve got other ideas for apps (like everyone who has ever met an iOS developer) but they are outside my expertise for the time being.&lt;/p&gt;</description></item><item><title>awesome-ios list on GitHub</title><link>https://blog.iankulin.com/awesome-ios-list-on-github/</link><pubDate>Thu, 14 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/awesome-ios-list-on-github/</guid><description>&lt;p&gt;I was looking for some more podcasts with Swift fundamentals content when I came across &lt;a href="https://github.com/vsouza/awesome-ios"&gt;this&lt;/a&gt; great community built awesome list.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/vsouza/awesome-ios"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-11-at-8.45.25-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a few podcasts on the list I have not come across, so I&amp;rsquo;ll check them out.&lt;/p&gt;</description></item><item><title>Learning Retention</title><link>https://blog.iankulin.com/learning-retention/</link><pubDate>Thu, 14 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/learning-retention/</guid><description>&lt;p&gt;In order to have something to put up on GitHub (as part of working all that out) I went back to re-write the Checkpoint 2 code that I&amp;rsquo;d written, but not saved, three or four days ago.&lt;/p&gt;
&lt;p&gt;The task was to count the unique elements in an array. The teaching had been about the complex data types, so clearly the hint was to cast the array to a set. Although the idea of sets is new to me this year, I&amp;rsquo;ve come across them twice. Once in the 100 days course (the same day as having to write this code) and once from a few days earlier from a &lt;a href="https://firesideswift.fireside.fm/157"&gt;podcast episode&lt;/a&gt;. This is high quality learning - getting the same topic a couple of different ways a few days apart, then having to use the information for real.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/*
 This time the challenge is to create an array of strings, then write some code that prints the number of items in the array and also the number of unique items in the array.
 */

let strArray = [&amp;#34;one&amp;#34;, &amp;#34;two&amp;#34;, &amp;#34;one&amp;#34;, &amp;#34;three&amp;#34;]
let strSet = Set(strArray)
print(&amp;#34;Array size:\(strArray.count) unique count:\(strSet.count)&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Checkpoint 4</title><link>https://blog.iankulin.com/checkpoint-4/</link><pubDate>Wed, 13 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-4/</guid><description>&lt;pre tabindex="0"&gt;&lt;code&gt;/*
 The challenge is this: write a function that accepts an integer from 1 through 10,000, and returns the integer square root of that number. That sounds easy, but there are some catches:
 
 You can’t use Swift’s built-in sqrt() function or similar – you need to find the square root yourself.
 If the number is less than 1 or greater than 10,000 you should throw an “out of bounds” error.
 You should only consider integer square roots – don’t worry about the square root of 3 being 1.732, for example.
 If you can’t find the square root, throw a “no root” error.
 */

enum IntSqrtError: Error {
 case low, high, noIntRoot
}

func calculateIntSqrt(_ number:Int) throws -&amp;gt; Int {
 let lowerBound = 1
 let upperBound = 10_000
 if number &amp;lt; lowerBound {throw IntSqrtError.low}
 if number &amp;gt; upperBound {throw IntSqrtError.high}
 // brute force sqrt finder
 for i in lowerBound...number {
 if i*i == number {
 return i
 }
 }
 // none found or we would have returned by now
 throw IntSqrtError.noIntRoot
}

do {
 try print(calculateIntSqrt(5929))
} catch IntSqrtError.low {
 print(&amp;#34;Lower bound error&amp;#34;)
} catch IntSqrtError.high {
 print(&amp;#34;Upper bound error&amp;#34;)
} catch IntSqrtError.noIntRoot {
 print(&amp;#34;No integer root&amp;#34;)
} catch {
 assert(false)
 print(&amp;#34;Unknown error&amp;#34;)
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Checkpoint 4 optimisation</title><link>https://blog.iankulin.com/checkpoint-4-optimisation/</link><pubDate>Wed, 13 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-4-optimisation/</guid><description>&lt;p&gt;The &lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-4"&gt;Checkpoint 4&lt;/a&gt; task was to find an integer square root of numbers up to 10000. My &lt;a href="https://blog.iankulin.com/checkpoint-4/"&gt;first pass solution&lt;/a&gt; was:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func calculateIntSqrt(_ number:Int) throws -&amp;gt; Int {
 let lowerBound = 1
 let upperBound = 10_000
 if number &amp;lt; lowerBound {throw IntSqrtError.low}
 if number &amp;gt; upperBound {throw IntSqrtError.high}
 // brute force sqrt finder
 for i in lowerBound...number {
 if i*i == number {
 return i
 }
 }
 // none found or we would have returned by now
 throw IntSqrtError.noIntRoot
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Although not coded for speed, there are a couple of subtle optimisations here; the first is that it gives up once it gets to the number instead of going up to the end (it assumes the square root of a number can&amp;rsquo;t be greater than the number), and the second is that it counts up from the bottom rather than down from the top - I&amp;rsquo;m assuming the bottom of the range is richer in square roots than the top.&lt;/p&gt;
&lt;p&gt;This code works fine (there was no discernible time difference between 4 and 9999) and meets the specification, so I wouldn&amp;rsquo;t want to bill the client for any further work, or to make the code uglier. Obviously however, it does spark the question of what I&amp;rsquo;d do if the function had to work over a much larger range. When I altered it to allow maxInt and passed 1,000,000,000 to it, it took just under four minutes to find in Playgrounds on an M1 MacBook.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible a good mathematics student would know some good tricks for calculating square roots, the first couple of links I googled suggested a method of looking for multiples that themselves were squares. While that&amp;rsquo;s a viable strategy mentally, and for reasonable size numbers, it didn&amp;rsquo;t lend itself to being coded. So I guess the computer science guys would pull out the binary search.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;func calculateIntSqrt(_ number:Int) throws -&amp;gt; Int {
 var lowerBound = 1
 var upperBound = 2_147_483_646
 if number &amp;lt; lowerBound {throw IntSqrtError.low}
 if number &amp;gt; upperBound {throw IntSqrtError.high}
 
 var guess: Int
 var guessSquared: Int
 // ensure the answer is not in one of the bounds
 lowerBound = lowerBound-1
 upperBound = number+1
 while upperBound &amp;gt; lowerBound+1 {
 // pick a guess number halfway between the bounds
 guess = integerMean(lowerBound, upperBound)
 guessSquared = guess*guess
 // then use it to reduce the range between the bounds
 if guessSquared &amp;lt; number {
 lowerBound = guess
 } else if guessSquared &amp;gt; number {
 upperBound = guess
 } else { 
 // our guess was the integer square root
 return guess
 }
 }
 // none found or we would have returned by now
 throw IntSqrtError.noIntRoot
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This version has no discernible speed difference between 4 and 2,147,483,644. It&amp;rsquo;s a great example of the trade-offs to be considered when making programming choices. It&amp;rsquo;s double the lines of code, and wanted more comments to explain the intent.&lt;/p&gt;
&lt;p&gt;Full code available on &lt;a href="https://github.com/IanKulin/HackingWithSwift/blob/main/CheckPoint04b.swift"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Gitting Started</title><link>https://blog.iankulin.com/gitting-started/</link><pubDate>Wed, 13 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gitting-started/</guid><description>&lt;p&gt;One of my early goals was to get in the habit of using version control with Git/Github, and I&amp;rsquo;ve got that sorted out today. My source was this excellent, very clear video from &lt;a href="https://www.youtube.com/channel/UCxA99Yr6P_tZF9_BgtMGAWA"&gt;Gwen Faraday&lt;/a&gt;. I highly recommend it if you are just starting.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/RGOj5yH7evk?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;It possibly helped that I&amp;rsquo;m also on mac, so I didn&amp;rsquo;t have to deal with the &amp;ldquo;or however that&amp;rsquo;s done on your system&amp;rdquo; type problems. Also, where things didn&amp;rsquo;t work as expected, the explanation about what was being done was clear enough that the problem was solvable. For example, the push command Gwen used was:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git push origin master
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;but GitHub had defaulted my initial branch to &amp;ldquo;main&amp;rdquo; rather than &amp;ldquo;master&amp;rdquo;. Easily fixed since she immediately explained what both of those modifiers were. The only other tiny bit of troubleshooting was that my git global config wasn&amp;rsquo;t set up, so my commit was followed by a big message pointing out that my real email address wasn&amp;rsquo;t used for the commit:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; Committer: User Name &amp;lt;username@Ians-MacBook-Pro.local&amp;gt;
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

 git config --global --edit

After doing this, you may fix the identity used for this commit with:

 git commit --amend --reset-author
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It didn&amp;rsquo;t make any difference - the file I&amp;rsquo;d created locally pushed up to the GitHub repo just fine. When I did follow those instructions to edit the file, I suddenly needed to know how to use Vim (hint: &amp;ldquo;i&amp;rdquo; to go into insert mode for editing, then &amp;ldquo;:&amp;rdquo; for command mode and &amp;ldquo;x&amp;rdquo; to exit and save).&lt;/p&gt;
&lt;p&gt;The only real complexity in the whole process was generating the SSH key and saving that on GitHub to allow the push from your local directory up to the GitHub repository.&lt;/p&gt;
&lt;p&gt;Ignoring that, the process was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating the repository via on GitHub&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;git clone&lt;/code&gt; to download the repository to the local machine and set it up for tracking in git&lt;/li&gt;
&lt;li&gt;Edit/create the files, however. Gwen used Visual Studio code, I used my tools&lt;/li&gt;
&lt;li&gt;Check status with &lt;code&gt;git status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git add .&lt;/code&gt; to stage the new files and freshly edited files&lt;/li&gt;
&lt;li&gt;Commit those changes with &lt;code&gt;git commit -m &amp;quot;commit title&amp;quot; -m &amp;quot;description&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Then push them up to GitHub with &lt;code&gt;git push origin main&lt;/code&gt; where &amp;ldquo;main&amp;rdquo; is the branch name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of that was by about the 25 minute mark in the video, and is probably enough for me to go away and get some practice with. The rest covers getting an already established local git repository to GitHub, branching, forking, undoing.&lt;/p&gt;</description></item><item><title>Tuple Pronunciation</title><link>https://blog.iankulin.com/tuple-pronunciation/</link><pubDate>Tue, 12 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/tuple-pronunciation/</guid><description>&lt;p&gt;Another advantage of the videos, that hadn&amp;rsquo;t occurred to me when I &lt;a href="https://blog.iankulin.com/cs193p/"&gt;mentioned it the other day&lt;/a&gt;, is learning the correct pronunciation of things you&amp;rsquo;ve only ever read in books.&lt;/p&gt;
&lt;p&gt;Apparently, tuple is pronounced two-pull, and not with the tup to rhyme with cup as I&amp;rsquo;d always imagined. Google has confirmed, so it&amp;rsquo;s not just a UK thing.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-09-at-12.06.59-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Learn to Code 1 - Finished</title><link>https://blog.iankulin.com/learn-to-code-1-finished/</link><pubDate>Mon, 11 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/learn-to-code-1-finished/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2691.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A little milestone passed - I&amp;rsquo;ve finished the &amp;ldquo;Learn to Code 1&amp;rdquo; Playgrounds book. Next is &amp;ldquo;Learn to Code 2&amp;rdquo;, and I also see there&amp;rsquo;s a few more in the default load, including &amp;ldquo;Get Started with Apps&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2692.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then there&amp;rsquo;s a heap more under &amp;ldquo;More Playgrounds&amp;rdquo; which I guess is like a mini app store for Playgrounds. Some look like complete (but small) apps - such as &amp;ldquo;Meme Creator&amp;rdquo; and &amp;ldquo;Bubble Level&amp;rdquo;. Others are filed under &amp;ldquo;Extend your App&amp;rdquo; and seem to be focused on particular features such as &amp;ldquo;Organizing with Grids&amp;rdquo;.&lt;/p&gt;</description></item><item><title>Instant Errors</title><link>https://blog.iankulin.com/instant-errors/</link><pubDate>Sun, 10 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/instant-errors/</guid><description>&lt;p&gt;I&amp;rsquo;m loving how, in XCode and Playgrounds, it&amp;rsquo;s constantly sort of compiling or interpreting in the background so errors are being flagged as you&amp;rsquo;re working. I tried to google the proper name for this but it&amp;rsquo;s clearly so unremarkable as to be un-remarked on. I guess maybe it&amp;rsquo;s a commonplace feature of modern IDEs, but for someone who literally used to go to make a coffee when compiling a medium size Clipper or even years later Visual Studio C++ project, it&amp;rsquo;s a revelation.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s so built in for me to think of errors as failures, that at first it was a quite unnerving. I was thinking &amp;ldquo;Hang on, hang on - at least let me finish typing before you judge me!&amp;rdquo;. But increasingly, I love the little second or so pause when I finish a section and wait for it to clear all the errors.&lt;/p&gt;</description></item><item><title>Watch or Read</title><link>https://blog.iankulin.com/watch-or-read/</link><pubDate>Sun, 10 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/watch-or-read/</guid><description>&lt;p&gt;I&amp;rsquo;m appreciating, in the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of Swift UI&lt;/a&gt;, that &lt;a href="https://twitter.com/twostraws"&gt;Paul Hudson&lt;/a&gt; has provided a video and a text description for each of the topics. Usually, I&amp;rsquo;ll read the text - a lot of these early topics cover ground well-trodden from my previous experience, so I can skim forwards to find the Swift specific bits I need. However, if I&amp;rsquo;m making up my hour per day of Swift by multitasking it with a meal, then the video is handy.&lt;/p&gt;
&lt;p&gt;The content in the videos and the text is not identical either (in the couple I&amp;rsquo;ve watched), so if a concept is not clear, having some slightly different explanations is handy.&lt;/p&gt;</description></item><item><title>iPad Pros - Swift Playgrounds</title><link>https://blog.iankulin.com/ipad-pros-swift-playgrounds/</link><pubDate>Sat, 09 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ipad-pros-swift-playgrounds/</guid><description>&lt;p&gt;I&amp;rsquo;m loving Swift Playgrounds - it&amp;rsquo;s getting daily use switching back and forwards between the iPad and MacBook. It&amp;rsquo;s sort of amazing that a tool to support education - it seems designed for primary school students, and is certainly being used that way - scales right up to &amp;ldquo;commercial&amp;rdquo; level app production.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2022-07-07-at-08-01-11-ipad-pros-on-apple-podcasts.png" width="187" alt=""&gt;
&lt;p&gt;&lt;a href="https://ipadpros.net/"&gt;iPad Pros&lt;/a&gt; is a podcast about iPads (unsurprisingly) by &lt;a href="https://twitter.com/iPadProsPodcast"&gt;Tim Chaten&lt;/a&gt; and I listened to a &lt;a href="https://podcasts.apple.com/us/podcast/swift-playgrounds-4-with-frank-foster-ipad-pros-0132/id1264565547?i=1000546955634"&gt;2017 episode about the launch of Playgrounds 4&lt;/a&gt; with guest Frank Foster. The focus was more about using the iPad as a serious development tool - a la XCode for iPad - than the education possibilities. I&amp;rsquo;m all for XCode (or something closer) on iPad, but I&amp;rsquo;d be disappointed if Playgrounds was changed in any way that made it more intimidating for children.&lt;/p&gt;</description></item><item><title>A Couple of Favourite Places</title><link>https://blog.iankulin.com/a-couple-of-favourite-places/</link><pubDate>Fri, 08 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/a-couple-of-favourite-places/</guid><description>&lt;p&gt;The Swift documentation is &lt;a href="https://www.swift.org/documentation/"&gt;here&lt;/a&gt; on the Swift.org website. if you were expecting it to be at &lt;a href="http://Developer.apple.com"&gt;developer.apple.com&lt;/a&gt;, that’d be fair enough, there is plenty there for iOS developers. Swift however, is it’s own Open Source thing, so it gets its own site. I use the &lt;a href="https://docs.swift.org/swift-book/TheSwiftProgrammingLanguageSwift57.epub"&gt;epub version&lt;/a&gt; of the &lt;a href="https://docs.swift.org/swift-book/"&gt;Swift Programming Languag&lt;/a&gt;e book - its just a bit easier to keep my place.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://cs193p.sites.stanford.edu/"&gt;cs193p.sites.stanford.edu&lt;/a&gt; is where I go for the videos and homework assignments for that course.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/"&gt;hackingwithswift.com&lt;/a&gt; is the home of Paul Hudson, a prolific Swift community member who is the author of the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 days bootcamp&lt;/a&gt; I’m allegedly working through.&lt;/p&gt;</description></item><item><title>Checkpoint 3</title><link>https://blog.iankulin.com/checkpoint-2/</link><pubDate>Fri, 08 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-2/</guid><description>&lt;pre tabindex="0"&gt;&lt;code&gt; /*
 If it’s a multiple of 3, print “Fizz”
 If it’s a multiple of 5, print “Buzz”
 If it’s a multiple of 3 and 5, print “FizzBuzz”
 Otherwise, just print the number.
 */

for i in 1...100 {

 let isMultOfThree = (i % 3 == 0)
 let isMultOfFive = (i % 5 == 0)

 if (isMultOfFive &amp;amp;&amp;amp; isMultOfThree) {
 print(&amp;#34;FizzBuzz&amp;#34;)
 } else if isMultOfThree {
 print(&amp;#34;Fizz&amp;#34;)
 } else if isMultOfFive {
 print(&amp;#34;Buzz&amp;#34;)
 } else {
 print(i)
 }
}
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>CS193p</title><link>https://blog.iankulin.com/cs193p/</link><pubDate>Thu, 07 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/cs193p/</guid><description>&lt;p&gt;I&amp;rsquo;ve loved the first couple of these &amp;ldquo;Getting Started with SwiftUI&amp;rdquo; &lt;a href="https://www.youtube.com/watch?v=bqu6BquVi2M"&gt;lectures from Paul Hegarty&lt;/a&gt; at Stanford. He&amp;rsquo;s put a lot of thought into the sequence, and seems to address the questions that float up in my mind (with super clear explanations) just as I&amp;rsquo;m thinking of them. They also generously make the reading and homework assignments available at &lt;a href="https://cs193p.sites.stanford.edu/"&gt;cs193p.sites.stanford.edu&lt;/a&gt; so it&amp;rsquo;s possible to treat it as a course which I have made a bit of a start on, before being distracted by building my own simple app.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m currently on lecture 3 which was enough to get an MVP (minimum viable product) of EasterDay working in Playgrounds. I had intended it to be a demo of a simple MMVV setup, but by the time I&amp;rsquo;d refactored my model down to a struct containing a single int and a function it seemed silly.&lt;/p&gt;
&lt;p&gt;I want to get back to following these lectures through, so I might combine that with figuring out GitHub.&lt;/p&gt;</description></item><item><title>Playgrounds - Learn to Code</title><link>https://blog.iankulin.com/playgrounds-learn-to-code/</link><pubDate>Thu, 07 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/playgrounds-learn-to-code/</guid><description>&lt;p&gt;I mentioned the ”Learn to Code” ”books” that come with Playgrounds (on IPad and Mac) earlier. Here’s a quick example of the sort of challenge:&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/SIm4FrdOZ1M?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;This is towards the end of “Learn to Code 1”, there are ”Learn to code 2”, and ”Get Stared with Apps” books as well.&lt;/p&gt;</description></item><item><title>Playgrounds</title><link>https://blog.iankulin.com/playgrounds/</link><pubDate>Wed, 06 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/playgrounds/</guid><description>&lt;p&gt;One of the threads that&amp;rsquo;s led me to learning iOS development is last year&amp;rsquo;s release of the &lt;a href="https://developer.apple.com/swift-playgrounds/"&gt;Swift Playgrounds&lt;/a&gt; 4 app. I&amp;rsquo;ve long had a hankering for a tool to create IOS apps, and a few years ago invested a bit of time in &lt;a href="https://apps.apple.com/us/app/codea/id439571171"&gt;Codea&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Playgrounds possibly started as a little live scratchpad for code in Xcode, but now it&amp;rsquo;s big news in STEM education for getting kids started on coding. It&amp;rsquo;s possible to create (and share) Playground &amp;ldquo;books&amp;rdquo; that lead users through steps in programming. Playgrounds is supplied with one that covers the beginnings of programing - functions, loops, conditions etc and many more are downloadable. This is actually one of the methods I&amp;rsquo;m using for picking up Swift basics - I can pick it up in any spare five minutes and solve a puzzle to progress my learning.&lt;/p&gt;
&lt;p&gt;The release of Playgrounds 4 at the end of 2021 was a big step, since now it&amp;rsquo;s theoretically possible to fully develop an app and submit it to the app store from the iPad. That&amp;rsquo;s where I&amp;rsquo;ve started EasterDay. There&amp;rsquo;s also a Mac app, and the iCloud experience of moving between them is quite seamless.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2681.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I will move across to Xcode with this app soon though - I&amp;rsquo;m loving Playgrounds, but I&amp;rsquo;ve been remiss in writing unit tests and I should probably also work out how version control works in 2022.&lt;/p&gt;</description></item><item><title>100 days of SwiftUI</title><link>https://blog.iankulin.com/100-days-of-swiftui/</link><pubDate>Tue, 05 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/100-days-of-swiftui/</guid><description>&lt;p&gt;Paul Hudson&amp;rsquo;s intro to his &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;IOS development course&lt;/a&gt; asks participants to post on social media each day as part of an effort to keep them motivated. The Insta tag &lt;a href="https://www.instagram.com/explore/tags/100daysofswiftui/?hl=en"&gt;#100daysofswiftui&lt;/a&gt; is full of curated photos of MacBook Pros showing Xcode 14. This also seems like handy marketing for Paul.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a crazy idea, so I&amp;rsquo;ll add this to the long list of excitedly started and eventually abandoned blogs of the world. Perhaps a post will even help a forlorn google searcher out on day. I&amp;rsquo;ll aim to keep them short, so it does not become a large enough task to trigger it&amp;rsquo;s own procrastination.&lt;/p&gt;
&lt;p&gt;Other things I&amp;rsquo;ve found in the past to help conserve a sustained effort in new learning is to find some good weekly podcasts, and to have a clear short term goal. This will be to get a simple, but minimally useful app up published on the App Store.&lt;/p&gt;</description></item><item><title>App Idea</title><link>https://blog.iankulin.com/app-idea/</link><pubDate>Tue, 05 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/app-idea/</guid><description>&lt;p&gt;Here&amp;rsquo;s a rough plan for a &amp;ldquo;hello world&amp;rdquo; that&amp;rsquo;s simple enough to be achieved in the short term, but hopefully not too trivial to be accepted into the app store.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2680.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This is sketched out on GoodNotes 5. I started with the Apple Notes app, but was frustrated by not being able to mix text and drawing. Noteabilty looked great, but the once off purchase for GoodNotes was easier to swallow.&lt;/p&gt;
&lt;p&gt;Ignore the &amp;ldquo;Friday&amp;quot;s in the design above - they should be Sundays. Basically, the user selects a year, and the app says the day for the Easter Sunday that year. If the user needs the date for something, they click on it and it&amp;rsquo;s copied to the clipboard, or perhaps opens up in their calendar.&lt;/p&gt;
&lt;p&gt;I do have an accessibility concern about those scrolling number pickers - what&amp;rsquo;s the sight impaired experience? Maybe Apple&amp;rsquo;s already solved that since they seem like a common UI thing.&lt;/p&gt;</description></item></channel></rss>