Christian Fei's Blog - cri.devA blog about Programming, DIY and personal ramblings2025-08-20T00:00:00Zhttps://cri.devChristian FeiHow to fix hanging tests in Node.js2025-08-20T00:00:00Zhttps://cri.dev/posts/2025-08-20-how-to-fix-node-js-mock-timers-enable-setinterval-tests-hanging-pending/<p>Some context: you might want to test your node.js application in an integration test and your code might have a <code>setInterval</code> call to repeatedly perform an action.</p>
<p>In this case I found that my tests would hang and timeout after some time.</p>
<p>The solution in my case was to include the following line in my test suite (note: I’m using <code>node:test</code> built-in testing library):</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> test<span class="token punctuation">,</span> describe<span class="token punctuation">,</span> mock <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:test'</span>
mock<span class="token punctuation">.</span>timers<span class="token punctuation">.</span><span class="token function">enable</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">apis</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'setInterval'</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>This will ensure that your <code>setInterval</code> calls are properly mocked during the test, preventing them from interfering with your tests.</p>
<p>You could alternatively, as described <a href="https://nodejs.org/api/test.html#class-mocktimers">in the docs</a>, just use <code>mock.timers.enable()</code> without any arguments.</p>
How to use an iPad Pro as an external display2025-08-15T00:00:00Zhttps://cri.dev/posts/2025-08-15-how-to-use-connect-ipad-as-primary-monitor-display-for-mac-mini-computer-nintendo-switch-playstation/<h2 id="tldr%3B" tabindex="-1">TLDR;</h2>
<ol>
<li>Get an USB-C HDMI capture card</li>
<li>Install Orion on your iPad Pro and open it</li>
<li>Connect the HDMI cable from the capture card to the HDMI port on your device</li>
<li>If you’re using a Mac, start mirroring to use iPad’s native resolution</li>
</ol>
<h2 id="introduction" tabindex="-1">Introduction</h2>
<p>A few years ago, I bought an iPad Pro M1 and started using it as my “PC”. It suited me well over the years, but I felt the need of a real PC experience…</p>
<p>So I recently got hold of a Mac Mini M4 and since I use it sometimes while traveling, I needed a way to connect my iPad Pro as an external display.</p>
<p>As most of the times, Apple likes to make things complicated, and you need to find half-assed workarounds to get something working.</p>
<p>Using your iPad as your Mac Mini main display is one of them.</p>
<h2 id="what-you%E2%80%99ll-need" tabindex="-1">What you’ll need</h2>
<p>Just a cheap USB-C HDMI capture card. Any will do the job.</p>
<p>Download <a href="https://orion.tube/">“Orion”</a> on your iPad and start it up.</p>
<p>Now connect the HDMI cable from the capture card to your Mac Mini or other device.</p>
<p>You should see your Mac’s screen on your iPad, although with black bands on the top and bottom of the screen.</p>
<p>So annoying.</p>
<h2 id="mirroring-to-the-rescue" tabindex="-1">Mirroring to the rescue</h2>
<p>To fix the black bands issue and use your iPad’s native resolution, I found a trick.</p>
<p>Just start mirroring the screen from your Mac to your iPad.</p>
<p>Go to <code>System Preferences</code> > <code>Display</code>.</p>
<p>You’ll need to select the menu option “Mirror or extend to” and choose your iPad.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-mac-1.png" alt="ipad mac display" loading="lazy" /></p>
<p>The under your iPad screen, you should see the option “Use as” and select “Mirror for iPad”.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-mac-2.png" alt="ipad mac display" loading="lazy" /></p>
<p>Now you can use the full screen real estate of your iPad.</p>
<p>You still need to have the HDMI capture card connected, though.</p>
<p>As always, Apple makes things complicated, but once you figure out the workarounds, it’s a piece of cake (until the next breaking update)</p>
<h2 id="update%3A-ux" tabindex="-1">Update: UX</h2>
<h3 id="trigger-via-keyboard-shortcut" tabindex="-1">Trigger via keyboard shortcut</h3>
<p>After some time it gets old to enter the settings and adjust the screen each time you want to mirror.</p>
<p>An alternative, found through various YouTube videos, is to create a dedicated keyboard shortcut to essentially run the AppleScript <code>"Move to [name of iPad]"</code>.</p>
<p>Go to <code>System settings</code>.</p>
<p>Scroll down and at the bottom you’ll find the <code>Keyboard</code> settings.</p>
<p>Click on <code>Keyboard Shortcuts...</code>.</p>
<p>At the bottom of the list you’ll find <code>App Shortcuts</code>.</p>
<p>There create your keyboard shortcut with custom keybinding, by clicking on the <code>+</code> icon.</p>
<p>There set the <code>Menu title</code> to <code>Move to [name of ipad]</code>, make sure to change “name of ipad” to yours. And the keyboard shortcut to trigger it.</p>
<p>Now after connecting the HDMI capture card to your iPad and Mac Mini, you can press your custom keybinding (to me it works best if I open a new Finder window) to trigger the screen mirroring.</p>
<h3 id="trick-to-get-screen-mirroring-after-sleep" tabindex="-1">Trick to get screen mirroring after sleep</h3>
<p>If your Mac Mini goes to sleep, if you unlock your iPad, you’ll likely get back to the screen resolution with black bands at the top and bottom of your iPad’s screen.</p>
<p>There a trick I found is to simply unlock your iPad first, then resume your Mac Mini from sleep, and you’ll get screen mirroring at full screen without triggering the shortcut.</p>
Implementing HTTP range requests in Node.js2025-06-18T00:00:00Zhttps://cri.dev/posts/2025-06-18-how-to-http-range-requests-video-nodejs/<p>Ever paused a download, to later resume it? Or played back a video from a specific point, without downloading <strong>the whole</strong> video?</p>
<p>You already used HTTP range requests without even realizing it.</p>
<p>Recently had the chance to use them in a personal project, <a href="https://github.com/christian-fei/my-yt"><code>my-yt</code></a>, and I learned a ton, so here we go.</p>
<h2 id="in-a-nutshell" tabindex="-1">In a nutshell</h2>
<p><strong>In simple terms</strong>: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests">HTTP range requests</a> allow clients to ask for only a portion of the resource, given a start and end byte marker.</p>
<p>How? Using the HTTP <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Range"><code>Range</code></a> header.</p>
<p>E.g. <code>Range: bytes=0-2048</code> means “give me bytes from 0 to 2048”. You can also request multiple ranges and a resource given only a starting range.</p>
<p>The server then send back the requested resource portion. It does that using the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Range"><code>Content-Range</code></a> HTTP header to inform the client.</p>
<p>There is also the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Ranges"><code>Accept-Ranges</code></a> used by the server to tell the client that range requests are supported.</p>
<p>And have you ever heard about the HTTP status code <code>206</code>? It’s the one used when a server sends back a portion of the resource, like in this case.</p>
<p>(BTW, there is waaaaay more stuff to get into when talking about this, so here I am only scratching the surface)</p>
<hr />
<h2 id="show-me-some-code" tabindex="-1">Show me some code</h2>
<p>A basic example (to see the full code, check out <a href="https://github.com/christian-fei/my-yt/blob/ca4fbf6b43d83969eedb24b2609c7a9c33d55c37/server/router/api.js#L293-L372">my-yt</a>)</p>
<p>The client (e.g. a video player in your HTML) requests the first X bytes of a video file.</p>
<p>The server then send back the requested resource portion. As mentioned above, some HTTP headers actions comes into play here.</p>
<p>E.g.</p>
<p>The HTTP function handler responsible for replying with the correct chunk of the video:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">watchVideoHandler</span> <span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> location <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./your/video.mp4</span><span class="token template-punctuation string">`</span></span>
<span class="token keyword">const</span> contentType <span class="token operator">=</span> <span class="token string">'video/mp4'</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>location<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
res<span class="token punctuation">.</span><span class="token function">writeHead</span><span class="token punctuation">(</span><span class="token number">404</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'text/plain'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token string">'Video not found'</span><span class="token punctuation">)</span>
<span class="token keyword">return</span>
<span class="token punctuation">}</span>
res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'content-type'</span><span class="token punctuation">,</span> contentType<span class="token punctuation">)</span></code></pre>
<p>Parse the requested HTTP range</p>
<pre class="language-javascript"><code class="language-javascript"> <span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token keyword">let</span> start
<span class="token keyword">let</span> end
<span class="token keyword">const</span> range <span class="token operator">=</span> req<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>range
<span class="token keyword">if</span> <span class="token punctuation">(</span>range<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> bytesPrefix <span class="token operator">=</span> <span class="token string">'bytes='</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>range<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span>bytesPrefix<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> bytesRange <span class="token operator">=</span> range<span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span>bytesPrefix<span class="token punctuation">.</span>length<span class="token punctuation">)</span>
<span class="token keyword">const</span> parts <span class="token operator">=</span> bytesRange<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'-'</span><span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>parts<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">2</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> rangeStart <span class="token operator">=</span> parts<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">&&</span> parts<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>rangeStart <span class="token operator">&&</span> rangeStart<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
options<span class="token punctuation">.</span>start <span class="token operator">=</span> start <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>rangeStart<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> rangeEnd <span class="token operator">=</span> parts<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">&&</span> parts<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>rangeEnd <span class="token operator">&&</span> rangeEnd<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
options<span class="token punctuation">.</span>end <span class="token operator">=</span> end <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>rangeEnd<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Determine the file size and return it if its a HEAD request</p>
<pre class="language-javascript"><code class="language-javascript"> <span class="token keyword">const</span> stat <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">statSync</span><span class="token punctuation">(</span>location<span class="token punctuation">)</span>
<span class="token keyword">const</span> contentLength <span class="token operator">=</span> stat<span class="token punctuation">.</span>size
<span class="token keyword">if</span> <span class="token punctuation">(</span>req<span class="token punctuation">.</span>method <span class="token operator">===</span> <span class="token string">'HEAD'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
res<span class="token punctuation">.</span>statusCode <span class="token operator">=</span> <span class="token number">200</span>
res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'accept-ranges'</span><span class="token punctuation">,</span> <span class="token string">'bytes'</span><span class="token punctuation">)</span>
res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'content-length'</span><span class="token punctuation">,</span> contentLength<span class="token punctuation">)</span>
<span class="token keyword">return</span> res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<p>Calculate the bytes that are being sent back to the client</p>
<pre class="language-javascript"><code class="language-javascript"> <span class="token keyword">let</span> retrievedLength <span class="token operator">=</span> contentLength
<span class="token keyword">if</span> <span class="token punctuation">(</span>start <span class="token operator">!==</span> <span class="token keyword">undefined</span> <span class="token operator">&&</span> end <span class="token operator">!==</span> <span class="token keyword">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
retrievedLength <span class="token operator">=</span> end <span class="token operator">-</span> start <span class="token operator">+</span> <span class="token number">1</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>start <span class="token operator">!==</span> <span class="token keyword">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
retrievedLength <span class="token operator">=</span> contentLength <span class="token operator">-</span> start
<span class="token punctuation">}</span>
res<span class="token punctuation">.</span>statusCode <span class="token operator">=</span> <span class="token punctuation">(</span>start <span class="token operator">!==</span> <span class="token keyword">undefined</span> <span class="token operator">||</span> end <span class="token operator">!==</span> <span class="token keyword">undefined</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token number">206</span> <span class="token operator">:</span> <span class="token number">200</span>
res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'content-length'</span><span class="token punctuation">,</span> retrievedLength<span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>range <span class="token operator">!==</span> <span class="token keyword">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'accept-ranges'</span><span class="token punctuation">,</span> <span class="token string">'bytes'</span><span class="token punctuation">)</span>
res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'content-range'</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">bytes </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>start <span class="token operator">||</span> <span class="token number">0</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>end <span class="token operator">||</span> <span class="token punctuation">(</span>contentLength <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>contentLength<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<p>Pipe the file stream to the response</p>
<pre class="language-javascript"><code class="language-javascript"> <span class="token keyword">const</span> fileStream <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">createReadStream</span><span class="token punctuation">(</span>location<span class="token punctuation">,</span> options<span class="token punctuation">)</span>
fileStream<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'error'</span><span class="token punctuation">,</span> <span class="token parameter">error</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error reading file </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>location<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span>
res<span class="token punctuation">.</span><span class="token function">writeHead</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span>
res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
fileStream<span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span>res<span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
Crazy CSS experiments2024-12-19T00:00:00Zhttps://cri.dev/posts/2024-12-19-crazy-css-experiments-visualization-animation/<p>The following Codepen’s are quite old (mostly done in 2012/2013 when I was super into CSS and getting started with programming), but are super cool.</p>
<p>They showcase some of my early experiments with CSS3 animations, transformations, and visualizations.</p>
<p class="codepen" data-height="600" data-theme-id="dark" data-default-tab="result" data-slug-hash="AoQxzM" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/AoQxzM">
CSS Gyroscope</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="1200" data-theme-id="dark" data-default-tab="result" data-slug-hash="AmBapK" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/AmBapK">
animation-timing-function visualized with an oscilloscope</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="600" data-theme-id="dark" data-default-tab="result" data-slug-hash="kGwowY" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/kGwowY">
CSS cube</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="300" data-theme-id="dark" data-default-tab="result" data-slug-hash="DRMGor" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/DRMGor">
Minimal loader</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="result" data-slug-hash="npWMaz" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/npWMaz">
I have no idea what to call this</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="result" data-slug-hash="kOxYvV" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/kOxYvV">
Transform-rotation-multi-something</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="result" data-slug-hash="DgjrzW" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/DgjrzW">
Jiggly cube HAML + SCSS</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="result" data-slug-hash="AbqBjm" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/AbqBjm">
Flag</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<p class="codepen" data-height="600" data-theme-id="dark" data-default-tab="result" data-slug-hash="nQJymX" data-pen-title="CSS Gyroscope" data-preview="false" data-user="christian-fei" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/christian-fei/pen/nQJymX">
Perspective</a> by Christian Fei (<a href="https://codepen.io/christian-fei">@christian-fei</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>Exclude specific tags in Eleventy using a custom filter2024-09-21T00:00:00Zhttps://cri.dev/posts/2024-09-21-how-to-exclude-tags-collection-filter-eleventy/<p>Let’s say you have an eleventy blog, and want to show the tags related to a given blog post.</p>
<p>E.g. this post is tagged with <a href="https://cri.dev/tags/eleventy/"><code>eleventy</code></a> as you can see above.</p>
<p>But, it has also other tags attached, e.g. <code>post</code> and <code>featured</code> for example, that are mainly used for creating the <code>posts</code> and <code>featured</code> collections.</p>
<p>The HTML/nunjucks code for showing the taglist on this very site is the following:</p>
<pre class="language-html"><code class="language-html">
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>taglist<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
Tagged with
{% for tag in tags | exclude("post") | exclude("featured") | limit(3) %}
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">aria-hidden</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/tags/{{ tag }}/<span class="token punctuation">"</span></span> <span class="token attr-name">data-track</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>click-post-tag<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ tag }}<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>sup</span><span class="token punctuation">></span></span>{{collections.tagList | findTagCount(tag)}}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>sup</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
{% endfor %}
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
</code></pre>
<p>There are a few things going on here:</p>
<ul>
<li>the post exposes its <code>tags</code> property, that’s straightforward</li>
<li>the filter <code>exclude</code> comes up twice, it’s chainable</li>
<li>the filter <code>limit</code> does exactly what you think</li>
<li>the link to the specific tag is <code>/tags/</code>, so there is a <a href="https://www.11ty.dev/docs/quicktips/tag-pages/">dedicated page for each tag</a></li>
<li>the collection <code>tagList</code> comes into play, together with the filter <code>findTagCount</code></li>
</ul>
<p>Let’s break it down and go into each step and needed code</p>
<h2 id="the-exclude-filter" tabindex="-1">The <code>exclude</code> filter</h2>
<p>You can simply add the next few filters and collections to your eleventy config file.</p>
<p>The <code>exclude</code> filter takes an array (namely the tags array as a list of tags/string) and excludes the first parameter passed to the filter</p>
<pre class="language-js"><code class="language-js">eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><span class="token string">"exclude"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">arr<span class="token punctuation">,</span> exclude</span><span class="token punctuation">)</span> <span class="token operator">=></span> arr<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">el</span> <span class="token operator">=></span> el <span class="token operator">!==</span> exclude<span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
<h2 id="the-limit-filter" tabindex="-1">The <code>limit</code> filter</h2>
<p>Nothing fancy, limits a given array to a specified number of items:</p>
<pre class="language-js"><code class="language-js">eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><span class="token string">"limit"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">arr<span class="token punctuation">,</span> limit</span><span class="token punctuation">)</span> <span class="token operator">=></span> arr<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> limit<span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
<h2 id="the-taglist-collection-and-findtagcount-filter" tabindex="-1">The <code>tagList</code> collection and <code>findTagCount</code> filter</h2>
<p>The <code>tagList</code> collection goes over all blog posts, reduces them to an array of objects in the form <code>{tag, count}</code>:</p>
<pre class="language-js"><code class="language-js">eleventyConfig<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">"tagList"</span><span class="token punctuation">,</span> <span class="token parameter">collections</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> tags <span class="token operator">=</span> collections
<span class="token punctuation">.</span><span class="token function">getAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">tags<span class="token punctuation">,</span> item</span><span class="token punctuation">)</span> <span class="token operator">=></span> tags<span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>data<span class="token punctuation">.</span>tags<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">tag</span> <span class="token operator">=></span> <span class="token operator">!</span><span class="token operator">!</span>tag <span class="token operator">&&</span> <span class="token operator">!</span><span class="token punctuation">[</span><span class="token string">"post"</span><span class="token punctuation">,</span> <span class="token string">"featured"</span><span class="token punctuation">,</span> <span class="token string">"popular"</span><span class="token punctuation">,</span> <span class="token string">"opinion"</span><span class="token punctuation">,</span> <span class="token string">"all"</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>tag<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">return</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Set</span><span class="token punctuation">(</span>tags<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">tag</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span>
tag<span class="token punctuation">,</span>
<span class="token literal-property property">count</span><span class="token operator">:</span> collections<span class="token punctuation">.</span><span class="token function">getFilteredByTag</span><span class="token punctuation">(</span>tag<span class="token punctuation">)</span><span class="token punctuation">.</span>length<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>The <code>findTagCount</code> filter takes the above <code>tagList</code> and finds the number of occurrencies across all blog posts:</p>
<pre class="language-js"><code class="language-js">eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><span class="token string">"findTagCount"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">tagList<span class="token punctuation">,</span> findTag</span><span class="token punctuation">)</span> <span class="token operator">=></span> tagList<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span>tag<span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> tag <span class="token operator">===</span> findTag<span class="token punctuation">)</span><span class="token operator">?.</span>count<span class="token punctuation">)</span></code></pre>
Start Kindle script on system boot with init.d2024-09-15T00:00:00Zhttps://cri.dev/posts/2024-09-15-Start-Kindle-script-on-system-boot-with-init/<p>To manage your custom Kindle scripts I found the following trick super useful.</p>
<p>Let’s say you have a script that shows the time on your Kindle e-ink display, under <code>/bin/custom-clock</code></p>
<p>Place your script configuration under <code>/etc/init/<your-script>.conf</code> (e.g. <code>/etc/init/custom-clock.conf</code>) with the following content:</p>
<pre class="language-sh"><code class="language-sh">start on started lab126_gui
stop on stopping lab126_gui
pre-start script
<span class="token comment"># important test to verify your script exists, else it could impact the boot of the kindle</span>
<span class="token builtin class-name">test</span> <span class="token parameter variable">-x</span> /bin/custom-clock <span class="token operator">||</span> <span class="token punctuation">{</span> stop<span class="token punctuation">;</span> <span class="token builtin class-name">exit</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
end script
<span class="token builtin class-name">exec</span> /bin/custom-clock</code></pre>
<p>After a reboot, you’ll be able to manage your service (if needed) using the following commands:</p>
<pre class="language-sh"><code class="language-sh">stop custom-clock
start custom-clock</code></pre>
Converting my Kindle to a digital photo frame2024-09-15T00:00:00Zhttps://cri.dev/posts/2024-09-16-Kindle-image-carousel-custom-script/<p>Recently I converted my old Kindle Paperwhite 3 (jailbroken) to a <a href="https://cri.dev/posts/2024-09-13-display-tapo-surveillance-camera-kindle-ffmpeg-jailbreak-root/">Tapo surveillance cam viewer</a>.</p>
<p>It was fun, but I recently had the idea to make a better use for it.</p>
<p>Namely making an image carousel showing nice pics I took over the years.</p>
<p>–</p>
<h2 id="project-overview" tabindex="-1">Project overview</h2>
<p>The idea is quite simple:</p>
<ul>
<li>create an <a href="https://cri.dev/posts/2024-09-15-Start-Kindle-script-on-system-boot-with-init/"><code>init.d</code> conf</a> to start the script at boot</li>
<li><a href="https://cri.dev/posts/2024-09-13-no-imagemagick-no-problem-use-ffmpeg/">prepare the images for the Kindle</a> I want to show</li>
<li>finally write the script that cycles through the images indefinitely and <a href="https://cri.dev/posts/2024-09-13-first-kindle-rooted-jailbreak-script-eips-show-ip-address/">prints them on the Kindle e-ink screen</a> using <code>eips</code></li>
</ul>
<h2 id="init.d-conf-and-carousel-script" tabindex="-1"><code>init.d</code> conf and carousel script</h2>
<p>Create a file under <code>/etc/init/image-carousel.conf</code> with the following content:</p>
<pre class="language-sh"><code class="language-sh">start on started lab126_gui
stop on stopping lab126_gui
pre-start script
<span class="token builtin class-name">test</span> <span class="token parameter variable">-x</span> /mnt/us/image-carousel.sh <span class="token operator">||</span> <span class="token punctuation">{</span> stop<span class="token punctuation">;</span> <span class="token builtin class-name">exit</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
end script
<span class="token builtin class-name">exec</span> /mnt/us/image-carousel.sh</code></pre>
<p>Next, you’ll need to create an executable script under <code>/mnt/us/image-carousel.sh</code></p>
<pre class="language-sh"><code class="language-sh"><span class="token shebang important">#!/bin/sh</span>
<span class="token comment"># prevent the screen from going to sleep and showing the screensaver</span>
lipc-set-prop <span class="token parameter variable">-i</span> com.lab126.powerd preventScreenSaver <span class="token number">1</span>
<span class="token keyword">while</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token keyword">do</span>
<span class="token keyword">for</span> <span class="token for-or-select variable">image</span> <span class="token keyword">in</span> /mnt/us/images/prepared/*.png<span class="token punctuation">;</span> <span class="token keyword">do</span>
<span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">$image</span>"</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token parameter variable">-f</span> <span class="token string">"<span class="token variable">$image</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
eips <span class="token parameter variable">-c</span> <span class="token parameter variable">-f</span> <span class="token parameter variable">-g</span> <span class="token string">"<span class="token variable">$image</span>"</span>
<span class="token keyword">fi</span>
<span class="token function">sleep</span> <span class="token number">30</span>
<span class="token keyword">done</span>
<span class="token keyword">done</span></code></pre>
<p>Make it executable with <code>chmod +x /mnt/us/image-carousel.sh</code></p>
<h2 id="prepare-the-images" tabindex="-1">Prepare the images</h2>
<p>If you want, you can prepare the images on your PC using ImageMagick, but I’m going to do that on the Kindle <a href="https://cri.dev/posts/2024-09-13-no-imagemagick-no-problem-use-ffmpeg/">using ffmpeg</a></p>
<p>Create two folders</p>
<ul>
<li><code>/mnt/us/images</code></li>
<li><code>/mnt/us/images/prepared</code></li>
</ul>
<p>Here is a little helper script to convert the images on the Kindle, place it under <code>/mnt/us/images/prepare.sh</code> and make it executable</p>
<pre class="language-sh"><code class="language-sh"><span class="token shebang important">#!/bin/sh</span>
<span class="token keyword">for</span> <span class="token for-or-select variable">image</span> <span class="token keyword">in</span> /mnt/us/images/*.png<span class="token punctuation">;</span> <span class="token keyword">do</span>
<span class="token builtin class-name">echo</span> <span class="token string">"preparing <span class="token variable">$image</span>"</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token parameter variable">-f</span> <span class="token string">"<span class="token variable">$image</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">" -> /mnt/us/images/prepared/<span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $image<span class="token variable">)</span></span>"</span>
ffmpeg <span class="token parameter variable">-y</span> <span class="token parameter variable">-i</span> <span class="token string">"<span class="token variable">$image</span>"</span> <span class="token parameter variable">-f</span> image2 <span class="token punctuation">\</span>
<span class="token parameter variable">-pix_fmt</span> gray <span class="token punctuation">\</span>
<span class="token parameter variable">-vf</span> <span class="token string">"scale=1448:1072,transpose=1"</span> <span class="token punctuation">\</span>
<span class="token parameter variable">-vframes</span> <span class="token number">1</span> <span class="token punctuation">\</span>
<span class="token string">"/mnt/us/images/prepared/<span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $image<span class="token variable">)</span></span>"</span>
<span class="token keyword">fi</span>
<span class="token keyword">done</span></code></pre>
<p>This will place the prepared images in <code>/mnt/us/images/prepared/</code> (the folder the image-carousel script uses)</p>
<p>–</p>
<p>Now, reboot your Kindle and when the boot process finished your images will cycle and change every 30s.</p>
Display jailbroken Kindle info using lipc-get-prop2024-09-14T00:00:00Zhttps://cri.dev/posts/2024-09-14-Display-jailbroken-Kindle-info-using-lipc-get-prop/<p>On my <a href="https://cri.dev/posts/2024-08-28-rooted-my-kindle-paperwhite/">jailbroken Kindle</a> I wanted to display information about it (e.g. <a href="https://cri.dev/posts/2024-09-13-first-kindle-rooted-jailbreak-script-eips-show-ip-address/">its IP address</a>, battery level etc.)</p>
<p>There is a command on the Kindle, namely <code>lipc-get-prop</code> that you can use to gather information about your device, e.g.:</p>
<pre class="language-sh"><code class="language-sh">lipc-get-prop <span class="token parameter variable">-i</span> com.lab126.powerd battLevel</code></pre>
<p>Additionally, to find the information you’re looking for, you can leverage <code>lipc-probe</code> like so</p>
<pre class="language-sh"><code class="language-sh">lipc-probe <span class="token parameter variable">-a</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-C</span> <span class="token number">20</span> <span class="token parameter variable">-e</span> batt</code></pre>
<p>This way you can discover what keys seem interesting for your use case.</p>
<p>Furthermore, you can use the built-in command to <a href="https://cri.dev/posts/2024-09-13-first-kindle-rooted-jailbreak-script-eips-show-ip-address/">interact with the e-ink display named <code>eips</code></a></p>
<p>More information <a href="https://wiki.mobileread.com/wiki/Kindle_Touch_Hacking#Interesting_handlers_.2F_actions">about lipc</a></p>
How to install Epic Games store on iPad with iOS 182024-10-12T00:00:00Zhttps://cri.dev/posts/2024-09-14-how-to-install-epic-games-store-ipad-ios-18/<p>If you navigate to <a href="https://store.epicgames.com/en-US/mobile">store.epicgames.com</a> on an iPad with iOS 18 beta you’ll have the ability to scan a QR code on your iPhone to download Epic’s app store.</p>
<p>But you don’t want that! You want to install it on your iPad to play Fortnite, Fall Guys and Rocket League!</p>
<p>There is an easy way around it.</p>
<p>–</p>
<h2 id="update-2024-10-12" tabindex="-1">update 2024-10-12</h2>
<p>The procedure to install the store on iPad <a href="https://x.com/EpicGames/status/1835761597941154114">should work out of the box now</a></p>
<p>–</p>
<p>Go to <a href="https://store.epicgames.com/en-US/mobile">store.epicgames.com</a> on your iPad.</p>
<p>Select the options menu in the location bar.</p>
<p>Click on the three dots menu.</p>
<p><img src="https://cri.dev/assets/images/posts/epic-games-store-ipad/IMG_0857.png" alt="" loading="lazy" /></p>
<p>Select “Request Mobile Website”.</p>
<p><img src="https://cri.dev/assets/images/posts/epic-games-store-ipad/IMG_0858.png" alt="" loading="lazy" /></p>
<p>Done. Now you can install it using Apple’s convoluted procedure to install an alternative App Store and download its apps as if you were on your iPhone.</p>
<p><img src="https://cri.dev/assets/images/posts/epic-games-store-ipad/IMG_0859.png" alt="" loading="lazy" /></p>
<p><img src="https://cri.dev/assets/images/posts/epic-games-store-ipad/IMG_0860.png" alt="" loading="lazy" /></p>
Displaying RTSP stream on a jailbroken Kindle Paperwhite2024-09-13T00:00:00Zhttps://cri.dev/posts/2024-09-13-display-tapo-surveillance-camera-kindle-ffmpeg-jailbreak-root/<p>As crazy/stupid as it sounds, I did it.</p>
<p>Managed to show the local RTSP stream from a Tapo surveillance camera on a rooted / jailbroken Kindle Paperwhite, without servers, just <code>ffmpeg</code> and <code>eips</code></p>
<p>–</p>
<h2 id="the-idea" tabindex="-1">The idea</h2>
<p><code>eips</code> is a custom e-ink support program for kindles</p>
<p>Find more <a href="https://wiki.mobileread.com/wiki/Eips">info here</a></p>
<p>Fortunately on a Kindle (rooted) there is also a build of the <code>ffmpeg</code> lib, which will come in very handy.</p>
<p>The idea of the script that is running on the Kindle (no servers! “serverless” in the real sense) is the following:</p>
<p>I want to use the <a href="https://cri.dev/posts/2024-08-03-how-to-add-tplink-tapo-camera-c210-homeassistant/">RTSP stream of my Tapo camera</a> and grab a snapshot (1 frame of the stream)</p>
<p>Since <a href="https://cri.dev/posts/2024-09-13-no-imagemagick-no-problem-use-ffmpeg/">I don’t have ImageMagick installed</a> (and can’t compile it from source) on my Kindle, I’ll need to do everything with one ffmpeg command.</p>
<p><code>eips</code> will come into play to render the image every few seconds.</p>
<p>This is going to be great-scale (alright, sorry for the pun)</p>
<p>–</p>
<h2 id="proof-of-kindle" tabindex="-1">Proof of Kindle</h2>
<p><img src="https://cri.dev/assets/images/posts/kindle-tapo-ffmpeg.jpeg" alt="kindle tapo cam ffmpeg" loading="lazy" /></p>
<h2 id="the-script" tabindex="-1">The script</h2>
<pre class="language-sh"><code class="language-sh"><span class="token comment">#/bin/sh</span>
<span class="token keyword">while</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token keyword">do</span>
ffmpeg <span class="token parameter variable">-y</span> <span class="token parameter variable">-rtsp_transport</span> tcp <span class="token punctuation">\</span>
<span class="token parameter variable">-i</span> <span class="token string">"rtsp://<username>:<password>@<tapocamip>:554/stream1"</span> <span class="token punctuation">\</span>
<span class="token parameter variable">-f</span> image2 <span class="token punctuation">\</span>
<span class="token parameter variable">-pix_fmt</span> gray <span class="token punctuation">\</span>
<span class="token parameter variable">-vf</span> <span class="token string">"scale=1920:-1,transpose=1"</span> <span class="token punctuation">\</span>
<span class="token parameter variable">-vframes</span> <span class="token number">1</span> <span class="token punctuation">\</span>
output.png
eips <span class="token parameter variable">-cfg</span> output.png
<span class="token function">sleep</span> <span class="token number">5</span>
<span class="token keyword">done</span></code></pre>
<h2 id="ffmpeg" tabindex="-1">FFmpeg</h2>
<p>To break down the <code>ffmpeg</code> command:</p>
<ul>
<li><code>-y</code> to overwrite the <code>output.png</code> file at every run</li>
<li><code>-rtsp_transport tcp</code> because I was getting dropped chunks during the transport with UDP on my spotty network</li>
<li><code>-i "rtsp://..."</code> well that’s the source where I am getting the rstp stream duh</li>
<li><code>-f image2</code> tells ffmpeg to store a sequence of individual image frames as separate files (in conjuction with <code>-vframes 1</code>)</li>
<li><code>-pix_fmt gray</code> because Kindles work best with 8-bit grayscale PNGs</li>
<li><code>-vf "scale=1920:-1,transpose=1"</code> to scale the image maintaining the aspect ratio and rotate it 90deg clockwise, so it fills the whole screen of the Kindle</li>
</ul>
<h2 id="eips" tabindex="-1">eips</h2>
<p>The <code>eips -cfg</code> does the following:</p>
<ul>
<li><code>-c</code> clears the screen before updating it</li>
<li><code>-f</code> perform a full screen update</li>
<li><code>-g</code> to tell it it’s a PNG file</li>
</ul>
<h2 id="prevent-kindle-going-to-sleep" tabindex="-1">Prevent Kindle going to sleep</h2>
<p>Borrowed <a href="https://github.com/forestpurnell/kindletron/blob/master/kindle/extensions/kindletron/bin/kindletron.sh">from this repo</a></p>
<p>Prepend this to the script:</p>
<pre class="language-sh"><code class="language-sh">lipc-set-prop <span class="token parameter variable">-i</span> com.lab126.powerd preventScreenSaver <span class="token number">1</span>
stop powerd</code></pre>
<p>More info <a href="https://cri.dev/posts/2024-09-14-Display-jailbroken-Kindle-info-using-lipc-get-prop/">about lipc-set-prop</a></p>
<p>or use the KUAL Helper extension to prevent the screensaver manually.</p>
<h2 id="installation" tabindex="-1">Installation</h2>
<p>You could spin this up in a cron, or <a href="https://cri.dev/posts/2024-09-15-Start-Kindle-script-on-system-boot-with-init/">through <code>init.d</code></a></p>
<p>Or by using a generic cron that runs every minute an make a different script without the <code>while</code> loop, e.g.</p>
<pre class="language-sh"><code class="language-sh">* * * * * /mnt/us/cam-once.sh</code></pre>
<p>To do that, first make your filesystem writable</p>
<pre class="language-sh"><code class="language-sh">mntroot rw</code></pre>
<p>Then edit the crontab</p>
<pre class="language-sh"><code class="language-sh"><span class="token function">vi</span> /etc/crontab/root</code></pre>
No ImageMagick? No problem, use FFmpeg!2024-09-13T00:00:00Zhttps://cri.dev/posts/2024-09-13-no-imagemagick-no-problem-use-ffmpeg/<p>Ok this is clickbait, but hear me out.</p>
<p>The alternative to imagemagick for simple scaling, rotation and image conversion actually works.</p>
<p>And I’ll explain why I didn’t/couldn’t go with ImageMagick (<em>on my Kindle</em>), which <em>would</em> be my go-to tool.</p>
<p>–</p>
<p><a href="https://cri.dev/posts/2024-08-28-rooted-my-kindle-paperwhite/">I rooted my Kindle.</a></p>
<p>The shell is very limited.</p>
<p>The Kindle runs on</p>
<pre><code>Linux kindle 3.0.35-lab126 #8 PREEMPT Fri Sep 13 12:49:59 UTC 2024 armv7l GNU/Linux
</code></pre>
<p>I don’t have a package manager, I could install Alpine on it and get <code>apk</code> but couldn’t get it to work after enough fiddling, and I actually didn’t need it turns out.</p>
<p>I wanted to display an image from my Tapo camera, but it doesn’t have a snapshot endpoint, only an RTSP stream.</p>
<p>So here comes FFmpeg to the rescue.</p>
<p>Here is the command I needed to get the image from the stream, and more importantly resize it, convert to gray-scale 8-bit PNG and rotate it:</p>
<pre class="language-sh"><code class="language-sh">ffmpeg <span class="token parameter variable">-i</span> <span class="token string">"rtsp://<username>:<password>@<ip>:554/stream1"</span>
<span class="token parameter variable">-f</span> image2 <span class="token comment"># stores sequence of individual image frames as separate files</span>
<span class="token parameter variable">-pix_fmt</span> gray <span class="token comment"># convert to grayscale, 8bit PNG</span>
<span class="token parameter variable">-vf</span> <span class="token string">"scale=1920:-1,transpose=1"</span> <span class="token comment"># scale it, maintain aspect ratio, and rotate by 90degrees clockwise</span>
<span class="token parameter variable">-vframes</span> <span class="token number">1</span> <span class="token comment"># I just need 1 frame</span>
output.png</code></pre>
<p>I hear you shouting at the screen “BUT I DON’T HAVE A STREAM, I JUST HAVE AN IMAGE!”</p>
<p>Simply replace the stream url with your local image.</p>
<p>Here is a link to the <a href="https://ffmpeg.org/ffmpeg-filters.html#scale">FFmpeg docs about its filters</a></p>
Display IP address on a jailbroken Kindle using eips2024-09-11T00:00:00Zhttps://cri.dev/posts/2024-09-13-first-kindle-rooted-jailbreak-script-eips-show-ip-address/<p>Wanna get started fiddling around with your <a href="https://cri.dev/posts/2024-08-28-rooted-my-kindle-paperwhite/">freshly jailbroken Kindle</a>?</p>
<p>Below I’ll show you a script to display the IP address using <code>eips</code> on your Kindle e-ink screen.</p>
<p>–</p>
<p>The idea is simple:</p>
<ul>
<li>run a cron every minute</li>
<li>get the Kindle’s current IP address</li>
<li>display it on the top-left corner using <code>eips</code></li>
</ul>
<h2 id="make-the-filesystem-writable" tabindex="-1">Make the filesystem writable</h2>
<p>This will give you the ability to write files on the Kindle (you just need this once)</p>
<pre class="language-sh"><code class="language-sh">mntroot rw</code></pre>
<h2 id="the-script" tabindex="-1">The script</h2>
<p>Place the script under <code>/bin/print-ip-info</code> (or where you want actually)</p>
<pre class="language-sh"><code class="language-sh"><span class="token shebang important">#!/bin/sh</span>
<span class="token assign-left variable">IP</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">ifconfig</span> wlan0 <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"inet addr:"</span> <span class="token operator">|</span> <span class="token function">cut</span> -d: <span class="token parameter variable">-f2</span> <span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{ print $1 }'</span><span class="token variable">)</span></span>
eips <span class="token parameter variable">-h</span> <span class="token string">"<span class="token variable">$IP</span> <span class="token variable"><span class="token variable">$(</span><span class="token function">date</span><span class="token variable">)</span></span>"</span></code></pre>
<p>This will parse through <code>ifconfig</code> and grep/cut/awk through all the fluff of the output.</p>
<p>Yes, I’m sure there is a better way, but who cares?</p>
<h2 id="install-the-cron" tabindex="-1">Install the cron</h2>
<p>Edit <code>/etc/crontab/root</code> and insert the following</p>
<pre class="language-sh"><code class="language-sh">* * * * * /bin/print-ip-info</code></pre>
Rooted / Jailbroke my Kindle Paperwhite2024-08-28T00:00:00Zhttps://cri.dev/posts/2024-08-28-rooted-my-kindle-paperwhite/<p>Found a way to make good use of my older Kindle Paperwhite 3 thanks to the Jailbreak “LanguageBreak”</p>
<p>You need to be at version <strong>5.16.2.1.1</strong> or else you’re effed.</p>
<p>Here is the <a href="https://www.mobileread.com/forums/showthread.php?t=356872">official guide</a></p>
<p>Wanted to make this post because I tried several days to jailbreak it without success.</p>
<p>One day, I noticed the collapsable section at the end of the original post that said, “Can’t get it to work?”</p>
<p>Downloaded the original file corresponding to my Kindle type and version and it just worked after that!</p>
<p>It was definitely a rollercoaster, had lots of fun learning about and reading through the Mobileread community.</p>
<p>It was cool to find out about “Demo mode” that is used by retail to show the Kindle functionality and screen resolution using a carousel of stock images.</p>
<p>What a mess that forum is, holy kindle</p>
How to add a TP-link Tapo camera to homeassistant2024-08-03T00:00:00Zhttps://cri.dev/posts/2024-08-03-how-to-add-tplink-tapo-camera-c210-homeassistant/<p>After struggling to add my tp-link tapo devices (specifically cameras) to my homeassistant instance, found a way to get everything working properly.</p>
<h2 id="everyone-gets-a-static-ip" tabindex="-1">Everyone gets a static IP</h2>
<p>First of all, I suggest you assign your cameras a static IP.</p>
<p>If you struggle to find your camera’s IP in your router settings, check out the MAC address of your camera (you’ll find it on the bottom side) and assign that a static IP.</p>
<p>This will save you several headaches and your future self will thank you.</p>
<h2 id="create-camera-credentials-in-tp-link-app" tabindex="-1">create camera credentials in tp-link app</h2>
<p>At this link you can find all the info to set up a camera account that you’ll use to access your cameras outside of the tp-link app</p>
<p><a href="https://www.tp-link.com/en/support/faq/2790/">https://www.tp-link.com/en/support/faq/2790/</a></p>
<h2 id="ffmpeg-dry-run" tabindex="-1">ffmpeg dry-run</h2>
<p>Once you have your camera’s IP and access credentials, you can try things out in the terminal.</p>
<p>You’ll need <code>ffmpeg</code> installed.</p>
<pre class="language-sh"><code class="language-sh">ffmpeg <span class="token parameter variable">-i</span> <span class="token string">"rtsp://<USERNAME>:<PASSWORD>@<IP>:554/stream1"</span> <span class="token parameter variable">-f</span> image2 <span class="token parameter variable">-vframes</span> <span class="token number">1</span> <span class="token parameter variable">-pix_fmt</span> yuvj420p output.jpg</code></pre>
<p>If the image is created, things are looking good.</p>
<h2 id="homeassistant-setup" tabindex="-1">Homeassistant setup</h2>
<p>In the “Integrations” page, just add a new integration named “Generic Camera”.</p>
<p>When adding a new camera, these are the settings I used:</p>
<p>“Stream source URL”: rtsp://YOUR_IP:554/stream1</p>
<p>“RTSP transport protocol”: UDP</p>
<p>“Authentication”: Basic, and provide username and password in the dedicated fields.</p>
<p>Disable “Verify SSL certificate”</p>
PETG I love you, but you're bringing me down2024-07-11T00:00:00Zhttps://cri.dev/posts/2024-07-11-3d-printing-petg-stringing-blobs-first-layer-tips-tricks/<p>I really do like 3d-printing stuff, but lately I had some trouble with PETG.</p>
<p>My main problems were stringing on the first layer and the damned PETG blobs that impacted print quality and ultimate the end result.</p>
<p>After a few days of trying to get PETG to print like it should, these are my findings for a better overall printing experience.</p>
<p><img src="https://cri.dev/assets/images/posts/3d-printing/petg-fect.jpeg" alt="petg perfect print" loading="lazy" /></p>
<p>If you’re experiencing stringing, first of all my heartfelt condolences.</p>
<p>Lower the nozzle temperature (e.g. from 240°C to 230°C) and experiment with lowering the print speed.</p>
<p>This should help layer adhesion and reduce stringing.</p>
<p>If you feel so inclined you can try to apply some gluestick and thoroughly clean your bed.</p>
<p>Using a brim as layer adhesion helps a lot for me.</p>
<p>PETG leaves a mess on my nozzle, due to all the small filament strings getting caught in it and fusing together to a single blob on the nozzle. Cleaning the nozzle because of this is a must.</p>
<p>Additionally, if you can, try to baby-sit your print on the first layer.</p>
<p>I found that decreasing the retraction speed and z-hop helps.</p>
<p>Another thing is to maintain a greater distance between the nozzle and the bed, since PETG tends to ooze and harden slower.</p>
Google domains / Squarespace transfer (using Porkbun and Cloudflare)2024-07-07T00:00:00Zhttps://cri.dev/posts/2024-07-07-google-domains-squarespace-transfer-using-porkbun-cloudflare-dns/<p><a href="https://support.google.com/domains/answer/13689670?hl=en">Squarespace acquired Google Domains</a> this year…</p>
<p>This is yet another reminder that Google services are ephemeral.</p>
<p>If you want to move away from Squarespace, and you’re using Cloudflare’s nameservers, this is the guide for you.</p>
<p>–</p>
<p>Consider this process might take a few days, so first of all: calm down, and most importantly prepare your wallet.<br />
Because it’s going to cost you the renewal fee of your domain to transfer it away from Squarespace…</p>
<p>There’s actually only a few steps needed to get everything working properly.</p>
<p>First, login to Squarespace and select the domain you want to transfer.</p>
<p>Unlock it, by disabling the switch under “Domain lock”.</p>
<p>Click “Request a transfer code”, wait up to 24h to receive the transfer authorization code.</p>
<p>Once you received your authorization code, you can login to porkbun and open the “Transfer” domain page.</p>
<p>There you’ll see a form with the domain name you want to transfer and authorization code, enter both and submit.</p>
<p>Transfers can take about five days to complete, so go do your thing meanwhile.</p>
<p>I started the process on July 2nd and received a confirmation email from both squarespace and porkbun on July 7th.</p>
<p><img src="https://cri.dev/assets/images/posts/squarespace-porkbun/porkbun-dashboard.jpeg" alt="porkbun dashboard" loading="lazy" /></p>
<p>The nice thing about having a third party DNS provider is that your nameserver settings are automatically migrated to porkbun, and you can continue managing your DNS with Cloudflare, without any changes (and downtime) on porkbun or squarespace.</p>
<p>Once you receive the confirmation emails from both registrars, you’ll see that the domain will be marked as “Third-party” on the Squarespace dashboard.</p>
<p><img src="https://cri.dev/assets/images/posts/squarespace-porkbun/squarespace-dashboard.jpeg" alt="squarspace dashboard" loading="lazy" /></p>
Google Sheets got fixed on iPad2024-06-27T00:00:00Zhttps://cri.dev/posts/2024-02-13-google-sheets-ipad-working/<p><strong>update: just found out <a href="https://web.dev/case-studies/google-sheets-wasmgc">why it was slow</a></strong></p>
<p>Yess!</p>
<p>I don’t know which update on which side got things finally working.</p>
<p>Whether it was on Apple’s or Google’s side.</p>
<p>Google Sheets is finally usable on the web on an iPad!</p>
<p>What a glorious day.</p>
<p>PS: In november of last year I <a href="https://cri.dev/posts/2023-11-04-google-sheets-ipad-slow/">complained</a> about how the experience of using Sheets on the web on an iPad was terrible.</p>
Today is my Lucky Backup Day2024-01-26T00:00:00Zhttps://cri.dev/posts/2024-01-26-Today-is-my-Lucky-Backup-Day/<p>Today is my lucky backup day.</p>
<p>TLDR; restored ~ 1 year of analytics data for Plausible analytics.</p>
<p><img src="https://cri.dev/assets/images/posts/lucky-backup-day.png" alt="lucky-backup-day" loading="lazy" /></p>
<p><a href="https://cri.dev/posts/2021-05-23-how-many-plausible-analytics-untracked-visitors-adblock/">I</a> <a href="https://cri.dev/posts/2020-10-20-Show-realtime-visitors-on-your-website-with-Plausibleio-and-CloudFlare-Workers/">really</a> <a href="https://cri.dev/posts/2020-04-24-Resuming-Elixir-by-self-hosting-plausible-analytics/">do</a> <a href="https://cri.dev/posts/2020-11-05-How-to-make-Polls-with-Plausible-Analytics/">care</a> <a href="https://cri.dev/posts/2020-07-14-Simple-event-tracking-with-Plausible-Analytics/">about</a> <a href="https://cri.dev/posts/2022-07-18-One-year-minimal-analytics/">my</a> <a href="https://cri.dev/posts/2021-04-28-fullstack-nodejs-preact-minimal-web-analytics-introduction/">data</a>, but apparently not enough to have a proper backup strategy.</p>
<p>That’s at least what this lesson taught me.</p>
<p>So let me elaborate/rant a bit.</p>
<p>I am using a simple Ansible playbook to provision my server with all the stuff that it needs, e.g. hardening, self-hosted services mostly with docker, nginx reverse proxy, vpn, etc.</p>
<p>You can already see there’s the something missing about “backup/restore” in the playbook… But I’m getting ahead of myself.</p>
<p>For Plausible, I am using the official docker-compose setup from the <a href="https://github.com/plausible/hosting/tree/master">hosting repository</a>. The docs are the best in town and are simple to follow.</p>
<p>On January 25th, that tragic day that cost me a whole lot of nerves, I was doing some maintenance on my server.</p>
<p>Checking out the Ansible playbook, browsing around files and setting to see if I could update or improve something.</p>
<p>Then after some cleanup, I run my usual <code>ansible-playbook playbook.yml</code> command and boom.</p>
<p>Something went wrong. Some nginx directive was wrong (totally unrelated to Plausible).</p>
<p>Fix it, run it again, everything ok.</p>
<p>Except all my Plausible data was “gone”, apparently.</p>
<p>It was showing “0 visitors” for the last 24 hours.</p>
<p>“Oh oh, weird”, I thought.</p>
<p>Checking the monthly chart, also 0 visitors.</p>
<p>Oh shit.</p>
<hr />
<p>I want to dedicate this section to all data lost and the poor souls that suffered from it.</p>
<hr />
<p>Then after sleeping on it, I found out there were other people in the same boat.</p>
<p>After following <a href="https://github.com/plausible/analytics/discussions/3132">this discussion about upgrading to 2.0.0</a>, I successfully restored all my data.</p>
<p>Even though I wasn’t even upgrading to 2.0.0</p>
<p>It was simple a minor update.</p>
<p>The following did the trick for me:</p>
<pre><code>$ docker compose stop plausible plausible_events_db
$ docker compose rm plausible_events_db
$ docker compose up -d
$ docker compose stop plausible
$ docker compose rm plausible
$ docker compose up -d
$ docker compose exec plausible bin/plausible rpc Plausible.DataMigration.NumericIDs.run
</code></pre>
<p>Here you can find the official <a href="https://github.com/plausible/hosting/blob/master/upgrade/postgres.md">upgrade guide for postgres</a></p>
<hr />
<p>So today, January 26th, marks the date for my personal Happy Lucky Backup Day 😅</p>
<p>(Alongside the World Backup Day on March 31st)</p>
Google Sheets on iPad? More like 'Google Shiits' 💩2024-06-27T00:00:00Zhttps://cri.dev/posts/2023-11-04-google-sheets-ipad-slow/<p><strong>update: just found out <a href="https://web.dev/case-studies/google-sheets-wasmgc">why it was slow</a></strong></p>
<p>If you ever needed to access your spreadsheets via web on an iPad, I feel you.</p>
<p>It’s quite a strategy game you have to play: you need to understand/feel how Google Sheets <strong>will</strong> lag in the next few seconds, so that you can optimize your next move, in foresight of the next lag while scrolling or changing cell - it’s also fun sometimes, for 10 seconds, then you would like to throw your iPad to the wall.</p>
<p>Just use the Google Sheets native application, right? It seriously sucks, lacks of functionality and it’s UX is weird to me. I simply don’t like it.</p>
<p>And besides, why shouldn’t I want to use the web version?</p>
<p>Also on my PC I always preferred the web version of things, I spend a lot of my time on the PC surfing the web anyway…</p>
<p>But I’m trying to justify myself here…</p>
<p>I’m hopeful that, like for some <a href="https://cri.dev/posts/2023-10-15-apple-added-ipad-audio-control-external-monitor/">other</a> <a href="https://cri.dev/posts/2023-03-22-ipad-programming-github-codespaces-raspberry-pi-vscode/">things</a>, we’ll be able to find a workaround/solution for sheets on the web too.</p>
htmx and Alpine.js are actually pretty awesome2023-10-19T00:00:00Zhttps://cri.dev/posts/2023-10-19-htmx-alpine-awesome-javascript-frontend-stack/<p>I’ve been using <a href="https://htmx.org/">htmx</a> and <a href="https://alpinejs.dev/">Alpine.js</a> in the past few weeks, and I have to say, they’re pretty awesome.</p>
<p>Made a stupid <a href="https://cri.dev/posts/2023-10-17-zengpt-chapgpt-alternative-frontend-opensource-self-hosting/">ChatGPT UI clone</a> with <a href="https://cri.dev/posts/2023-10-16-functions-as-views-javascript-node-javascript-template-htmx-alpine/">both of them</a> and they brought a breeze of fresh air to my frontend development.</p>
<p>How will I use them in the future? I don’t know, but I’ll definitely try to use them more often.</p>
<p>Especially for small projects, dashboards/frontends for internal APIs etc.</p>
<p>The learning curve is a bit steep in my opinion, but once you get the hang of it, you can get stuff done very quickly.</p>
ZenGPT: a simple ChapGPT alternative frontend2023-10-22T00:00:00Zhttps://cri.dev/posts/2023-10-17-zengpt-chapgpt-alternative-frontend-opensource-self-hosting/<p>I’ve been playing around with a home-made, super simple ChatGPT UI clone, mainly with the excuse to try out <a href="https://cri.dev/posts/2023-10-16-functions-as-views-javascript-node-javascript-template-htmx-alpine/">htmx and Alpine.js</a></p>
<p>It’s a fun little project that I’ve been working on for a few days.</p>
<p>I’ve been programming it on an <a href="https://cri.dev/posts/2023-03-22-ipad-programming-github-codespaces-raspberry-pi-vscode/">iPad Pro (as my main device)</a>, and it’s been a fun experience.</p>
<p>In this post I want to get deeper into the technical details of the project, and share some of the things I’ve learned while working on it.</p>
<p>–</p>
<p><img src="https://raw.githubusercontent.com/christian-fei/zengpt/main/zengpt.jpeg" alt="zengpt" loading="lazy" /></p>
<h2 id="node.js-server-and-bare-functions-as-views" tabindex="-1">node.js server and bare functions as views</h2>
<p>The other day I wrote about <a href="https://cri.dev/posts/2023-10-16-functions-as-views-javascript-node-javascript-template-htmx-alpine/">functions as views</a>, and how I’ve been using them in this project.</p>
<p>Namely, I’m using the native <code>http</code> node.js module, a simple home made router with if statements and a few functions that return HTML strings.</p>
<p>The functions are called with optional additional data and they return a string that is sent back to the client.</p>
<p>E.g.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>req<span class="token punctuation">.</span>url <span class="token operator">===</span> <span class="token string">'/'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'Content-Type'</span><span class="token punctuation">,</span> <span class="token string">'text/html'</span><span class="token punctuation">)</span>
<span class="token keyword">return</span> res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token function">mainView</span><span class="token punctuation">(</span>messages<span class="token punctuation">,</span> <span class="token function">listing</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span></code></pre>
<h2 id="integrating-with-openai%E2%80%99s-api" tabindex="-1">Integrating with OpenAI’s API</h2>
<p>The <a href="https://platform.openai.com/docs/api-reference/completions">Completions API</a> is pretty straightforward too:</p>
<p>You can use the <code>chat.completions.create</code> method to send the conversation to the API, and get back a text completion (llm response/message).</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> completion <span class="token operator">=</span> <span class="token keyword">await</span> ai<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>completions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">model</span><span class="token operator">:</span> <span class="token string">'gpt-3.5-turbo'</span><span class="token punctuation">,</span>
<span class="token literal-property property">messages</span><span class="token operator">:</span> messages
<span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">{</span> <span class="token literal-property property">role</span><span class="token operator">:</span> <span class="token string">'user'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> newUserMessage <span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">let</span> llmText <span class="token operator">=</span> completion<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>message<span class="token punctuation">.</span>content<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<h2 id="htmx" tabindex="-1">htmx</h2>
<p>As mentioned before, the main reason I started this project was to try out <a href="https://htmx.org/">htmx</a> (and Alpine.js)</p>
<p>The coolest thing I refreshed during this excursus was the concept of rethinking what we consider RESTful APIs (spoiler: they are actually HTTP JSON RPC APIs), HATEOAS, hypertext, and much more honestly. The htmx website is a goldmine of resources.</p>
<p>In short: our websites and “RESTful” APIs should be way more discoverable (for humans, not machines) and self-contained.</p>
<p>By making use of existing powerful technology like HTML and HTTP, with sprinkles of JavaScript, to make the web more accessible, lightweight, more reliable and easier to use.</p>
<p>But let’s got back to <a href="https://htmx.org/">htmx</a>.</p>
<blockquote>
<p>build modern user interfaces with the simplicity and power of hypertext</p>
</blockquote>
<p>The emphasis here is leveraging the power of HTML.</p>
<p>E.g. in this small project, the client is loaded with a simple HTML page, preloaded with messages from the server.</p>
<p>The rest (sorry for the poor choice of words) is done by the client, that makes requests to load small snippets of HTML, and updates the DOM with the response.</p>
<p>This is an oversimplification, but it’s the gist of it.</p>
<p>By using a declarative approach on the HTML, you can get a quite robust and powerful UI, with very little code.</p>
<p>The main focus of ZenGPT is the UI, this is the input and conversation part:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>messages<span class="token punctuation">"</span></span> <span class="token attr-name">hx-swap</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>scroll:bottom<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
${renderMessages(messages)}
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span>
<span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-post</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/chat<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-trigger</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>keyup[keyCode==13]<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#messages<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-swap</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>beforeend scroll:bottom<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-indicator</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#loading-message<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">hx-on:</span><span class="token namespace">htmx:</span>before-request</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>this.disabled=true<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">hx-on:</span><span class="token namespace">htmx:</span>after-request</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>this.disabled=false;setTimeout(() => this.focus(), 20)<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">x-bind:</span>disabled</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>messageDisabled<span class="token punctuation">"</span></span>
<span class="token attr-name">x-ref</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span>
<span class="token attr-name">x-model</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">x-on:</span>keyup.enter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>setTimeout(() => {message = '';pristineChat = false}, 10)<span class="token punctuation">"</span></span>
<span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my-message<span class="token punctuation">"</span></span> <span class="token attr-name">autofocus</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>your message<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">position</span><span class="token punctuation">:</span>fixed<span class="token punctuation">;</span><span class="token property">bottom</span><span class="token punctuation">:</span>3em<span class="token punctuation">;</span><span class="token property">right</span><span class="token punctuation">:</span>2em<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>htmx-indicator<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loading-message<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>svg</span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span> <span class="token attr-name">version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.1<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>64<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>64<span class="token punctuation">"</span></span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 24 24<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>path</span> <span class="token attr-name">fill</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>none<span class="token punctuation">"</span></span> <span class="token attr-name">stroke</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#000<span class="token punctuation">"</span></span> <span class="token attr-name">stroke-width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span> <span class="token attr-name">stroke-linecap</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>round<span class="token punctuation">"</span></span> <span class="token attr-name">d</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>M12 2 L12 6 M12 18 L12 22 M4.93 4.93 L7.76 7.76 M16.24 16.24 L19.07 19.07 M2 12 L6 12 M18 12 L22 12<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>path</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>This is so-called “no-javascript”, where the JS is simply hidden (remember you need to include a <code><script src="//unpkg.com/htmx.org"></code>). I think it’s a refreshing approach.</p>
<h2 id="alpine.js" tabindex="-1">Alpine.js</h2>
<p>In ZenGPT, Alpine.js is used to manage the state of the UI, and to make client-side only UI changes and updates.</p>
<p>It is used to add interactivity to the UI.</p>
<p>E.g. it handles the display state of the action buttons in the header</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">display</span><span class="token punctuation">:</span>flex</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">flex</span><span class="token punctuation">:</span>1</span><span class="token punctuation">"</span></span></span><span class="token attr-name">;</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>zengpt<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">x-show</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>!pristineChat<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">flex</span><span class="token punctuation">:</span>1<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token attr-name">;</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">display</span><span class="token punctuation">:</span>block<span class="token punctuation">;</span><span class="token property">padding</span><span class="token punctuation">:</span>1rem<span class="token punctuation">;</span><span class="token property">font-size</span><span class="token punctuation">:</span>1.5rem<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">hx-delete</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/chat<span class="token punctuation">"</span></span> <span class="token attr-name">hx-target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#messages<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">x-on:</span>click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$refs.message.focus();messageDisabled=false;pristineChat=true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
new chat
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">x-show</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>!pristineChat<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">flex</span><span class="token punctuation">:</span>1<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token attr-name">;</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">display</span><span class="token punctuation">:</span>block<span class="token punctuation">;</span><span class="token property">padding</span><span class="token punctuation">:</span>1rem<span class="token punctuation">;</span><span class="token property">font-size</span><span class="token punctuation">:</span>1.5rem<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">hx-post</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/chats<span class="token punctuation">"</span></span> <span class="token attr-name">hx-target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#messages<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">x-on:</span>click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$refs.message.value = '';messageDisabled=false;pristineChat=true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
save chat
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">x-show</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewingPreviousChat<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">flex</span><span class="token punctuation">:</span>1<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token attr-name">;</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">display</span><span class="token punctuation">:</span>block<span class="token punctuation">;</span><span class="token property">padding</span><span class="token punctuation">:</span>1rem<span class="token punctuation">;</span><span class="token property">font-size</span><span class="token punctuation">:</span>1.5rem<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">hx-get</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/chat<span class="token punctuation">"</span></span> <span class="token attr-name">hx-target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#messages<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">x-on:</span>click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$refs.message.value = '';messageDisabled=false;<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
go back
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<h2 id="open-source" tabindex="-1">open source</h2>
<p>You can find the project on <a href="https://github.com/christian-fei/zengpt">github.com/christian-fei/zengpt</a></p>
<h2 id="server-sent-events-(sse)" tabindex="-1">Server-sent events (SSE)</h2>
<p>Update 2023-10-22: I’ve added support for <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Server-sent events (SSE)</a> to the project!</p>
<p>This was super a interesting endeavour, and I’ve learned a lot about htmx extensions and SSE.</p>
Using pure functions as views (with htmx and alpine.js)2023-10-16T00:00:00Zhttps://cri.dev/posts/2023-10-16-functions-as-views-javascript-node-javascript-template-htmx-alpine/<p>In the past days I got my hands dirty with htmx and alpine.js</p>
<p>It was super refreshing and an smooth learning curve.</p>
<p>A lil steep and kind of depressing at the beginning, but once you get the hang of it, it was a breeze of fresh air in my development toolkit.</p>
<p>Here I want to get a bit into the details of how I used htmx and alpine.js to create a simple app, by focusing on the backend part.</p>
<p>Namely into how I used pure functions as views.</p>
<p>–</p>
<p>The idea is simple: instead of using a template engine (like nunjucks, jsx ssr, etc.), I just use a pure function that returns a string.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token function-variable function">view</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
<div>
<h1></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></h1>
<p></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>content<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"></p>
</div>
</span><span class="token template-punctuation string">`</span></span>
<span class="token punctuation">}</span></code></pre>
<p>this can be used in your routes/controllers simply by calling the function, passing in data if needed, and return it as a response.</p>
<h2 id="example-with-htmx-and-alpine" tabindex="-1">example with htmx and alpine</h2>
<p>A view part of a web app I’m currently working on:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>chats<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>details</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>summary</span><span class="token punctuation">></span></span>chats<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>summary</span><span class="token punctuation">></span></span>
${chats
.map(chat => `
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span>
<span class="token attr-name"><span class="token namespace">x-on:</span>click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>messageDisabled=true;pristineChat=true;viewingPreviousChat=true<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-get</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/chats/${chat}<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#messages<span class="token punctuation">"</span></span>
<span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/chats/${chat}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
${chat.replace('.json', '')}
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span>
`).join('<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>br</span><span class="token punctuation">></span></span>')}
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>details</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>In this case, I’m returning parts of the html that will be used by htmx to update the DOM and by alpine to update the state and the UI.</p>
<p>Another example I like is the following:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>messages<span class="token punctuation">"</span></span> <span class="token attr-name">hx-swap</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>scroll:bottom<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
${messagesView(messages)}
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span>
<span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-post</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/chat<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-trigger</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>keyup[keyCode==13]<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#messages<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-swap</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>beforeend scroll:bottom<span class="token punctuation">"</span></span>
<span class="token attr-name">hx-indicator</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#loading-message<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">hx-on:</span><span class="token namespace">htmx:</span>before-request</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>this.disabled=true<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">hx-on:</span><span class="token namespace">htmx:</span>after-request</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>this.disabled=false;setTimeout(() => this.focus(), 20)<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">x-bind:</span>disabled</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>messageDisabled<span class="token punctuation">"</span></span>
<span class="token attr-name">x-ref</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span>
<span class="token attr-name">x-model</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span>
<span class="token attr-name"><span class="token namespace">x-on:</span>keyup.enter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>setTimeout(() => {message = '';pristineChat = false}, 10)<span class="token punctuation">"</span></span>
<span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my-message<span class="token punctuation">"</span></span> <span class="token attr-name">autofocus</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>your message<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">position</span><span class="token punctuation">:</span>fixed<span class="token punctuation">;</span><span class="token property">bottom</span><span class="token punctuation">:</span>3em<span class="token punctuation">;</span><span class="token property">right</span><span class="token punctuation">:</span>2em<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>htmx-indicator<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loading-message<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>svg</span> <span class="token attr-name">...</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>svg</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>htmx is used to send the message to the server, and update the DOM with the new messages.</p>
<p>it also disables the input and attaches a loading indicator to the input, so that the user knows that the message is being sent.</p>
<p>another cool part i find are the <code>hx-trigger</code> and <code>hx-swap</code> part.</p>
<p><code>hx-trigger</code> is used to trigger the request when the user presses the enter key.</p>
<p><code>hx-swap</code> in conjunction with <code>beforeend scroll:bottom</code> is used to append the new messages to the list of messages, and scroll to the bottom.</p>
<p>The <code>POST /chat</code> endpoint receives the user entered input and returns the LLM message in return.</p>
<h2 id="views%2Fmessages.mjs" tabindex="-1"><code>views/messages.mjs</code></h2>
<p>Here an mjs script that I use to render the messages:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">messagesView</span><span class="token punctuation">(</span><span class="token parameter">messages <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> messages<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">message</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
<div hx-transition class="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>message<span class="token punctuation">.</span>role<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-message">
</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>message<span class="token punctuation">.</span>content<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
</div>
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>This is a simple function that takes an array of messages and returns a string of html.</p>
<p>And on the <code>router.mjs</code> file:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token operator">...</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>req<span class="token punctuation">.</span>url <span class="token operator">===</span> <span class="token string">'/chat'</span> <span class="token operator">&&</span> req<span class="token punctuation">.</span>method <span class="token operator">===</span> <span class="token string">'GET'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
res<span class="token punctuation">.</span>statusCode <span class="token operator">=</span> <span class="token number">200</span>
<span class="token keyword">return</span> res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token function">messagesView</span><span class="token punctuation">(</span>messages<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token operator">...</span></code></pre>
iPad can now control the audio when connected to an external monitor2023-10-16T00:00:00Zhttps://cri.dev/posts/2023-10-15-apple-added-ipad-audio-control-external-monitor/<p>Today I noticed I can finally listen audio on my iPad when connected to an external display!</p>
<p>I am currently on iPadOS 17.1 (with Developer Beta active).</p>
<p>This changes a lot for me, as I can now use my iPad as a second screen and still listen to music or podcasts on it.</p>
<p>I know, this sounds obvious, but it wasn’t possible a few weeks ago.</p>
<p>This solves <a href="https://cri.dev/posts/2023-09-27-ipad-ios-17-wish-list-2024/">one of the issues</a> I mentioned in a previous post.</p>
<p><a href="https://www.reddit.com/r/iPadPro/comments/zutbai/is_it_possible_to_use_the_ipad_as_the_audio/">There</a> <a href="https://www.reddit.com/r/apple/comments/8z2uwb/its_completely_ridiculous_that_you_cant_switch/">were</a> <a href="https://www.reddit.com/r/ipad/comments/ztdxl0/audio_with_an_external_monitor_m2_ipad_pro/">too</a> <a href="https://www.reddit.com/r/iPadOS/comments/st4nkk/ipad_and_hdmi_dock_for_movies/">many</a> <a href="https://www.reddit.com/r/ipad/comments/ldjqkt/ipad_pro_no_sound_when_connected_to_monitor/">people</a> looking for a workaround, and the only ones I found were:</p>
<ul>
<li>connect an aux cable to the monitor and an external speaker, but without the ability to control the volume from the iPad!</li>
<li>connect to an external bluetooth speaker, with the ability to control the volume</li>
</ul>
Improving my tags using OpenAI's Chat completions API2023-10-08T00:00:00Zhttps://cri.dev/posts/2023-10-08-improve-tags-blogging-openai-chatgpt-nodejs-ai/<p>Here I want to share how I <em>slightly</em> improved the tags on each of my blog posts.</p>
<p>The idea is super simple, a script should do the following:</p>
<ul>
<li>read each blog posts content</li>
<li>parse the front matter containing the tags (the header part at the beginning of markdown files)</li>
<li>create the prompt containing an excerpt of the blog post</li>
<li>call OpenAI’s API and parse the suggested tags</li>
<li>update the frontmatter with the tags</li>
<li>save the file back</li>
</ul>
<p>Find the code and more details below.</p>
<p>–</p>
<h2 id="the-prompt" tabindex="-1">The prompt</h2>
<p>This is the prompt I crafted that returns acceptable tags:</p>
<pre><code>Given a blog post, suggest appropriate tags for it.
The blog is about software programming.
Extract the 5 most important tags from the blog post.
The tags need to satisfy the following:
- short
- simple
- super generic
- one word (avoid putting words together)
- lowercase, no camelcase
- no duplicate concepts in the tags, e.g. githubcodespaces and github should become github and codespaces
- no numbers as tags
- when a tag gets too long, split it in two or generalize it
- avoid very specific tags
- do not use generic tags like "blog", "post", "programming", "software" etc. (if they're useful to categorize the post it's ok)
- the output should not contain any other extra text
Return just 5 tags in comma separated list form, avoid adding any other text before or after the tags.
Blog Post:
${blogPost.substring(0,2000)}...
</code></pre>
<p>It’s surely not perfect, but it works for now.</p>
<h2 id="processing-the-files" tabindex="-1">Processing the files</h2>
<p>A handy <code>find</code> and <code>xargs</code> magic does the trick just fine.</p>
<p>E.g. finding all blog posts from 2023</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">find</span> posts <span class="token parameter variable">-name</span> <span class="token string">"*.md"</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"2023-"</span> <span class="token operator">|</span> <span class="token function">xargs</span> <span class="token parameter variable">-I</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> ./scripts/improve-tags-gpt.mjs <span class="token punctuation">{</span><span class="token punctuation">}</span></code></pre>
<h2 id="the-script" tabindex="-1">The script</h2>
<p>I am using <code>gray-matter</code> to easily parse the posts frontmatter.</p>
<p>Also <code>openai</code>’s npm package for ease of use.</p>
<p>You’ll just need to set the env variable OPENAI_API_KEY and you’re good to go.</p>
<p>The main part looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token hashbang comment">#!/usr/bin/env node</span>
<span class="token keyword">import</span> fs <span class="token keyword">from</span> <span class="token string">'fs/promises'</span>
<span class="token keyword">import</span> OpenAI <span class="token keyword">from</span> <span class="token string">'openai'</span>
<span class="token keyword">import</span> matter <span class="token keyword">from</span> <span class="token string">'gray-matter'</span>
<span class="token keyword">const</span> ai <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OpenAI</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> filepath <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>
<span class="token function">processMarkdownFile</span><span class="token punctuation">(</span>filepath<span class="token punctuation">)</span></code></pre>
<h3 id="processing-the-markdown" tabindex="-1">Processing the markdown</h3>
<p>As simple as reading the file, parsing the frontmatter, skipping drafts and finally finding tags for the given blog post:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">processMarkdownFile</span><span class="token punctuation">(</span><span class="token parameter">filePath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> fileContent <span class="token operator">=</span> <span class="token keyword">await</span> fs<span class="token punctuation">.</span><span class="token function">readFile</span><span class="token punctuation">(</span>filePath<span class="token punctuation">,</span> <span class="token string">'utf-8'</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> content<span class="token punctuation">,</span> data <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">matter</span><span class="token punctuation">(</span>fileContent<span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>tags<span class="token operator">?.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'draft'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">return</span>
<span class="token keyword">const</span> suggestedTags <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">findTagsForBlogPost</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span>
data<span class="token punctuation">.</span>tags <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">'post'</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span>suggestedTags<span class="token punctuation">)</span>
<span class="token keyword">const</span> updatedContent <span class="token operator">=</span> matter<span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>content<span class="token punctuation">,</span> data<span class="token punctuation">)</span>
<span class="token keyword">await</span> fs<span class="token punctuation">.</span><span class="token function">writeFile</span><span class="token punctuation">(</span>filePath<span class="token punctuation">,</span> updatedContent<span class="token punctuation">,</span> <span class="token string">'utf-8'</span><span class="token punctuation">)</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Updated tags for </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error processing </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<h3 id="prompting-the-model" tabindex="-1">Prompting the model</h3>
<p>You’ll just need to call <code>ai.chat.completions.create</code>, specify the message and model to use.</p>
<p>Then, given the response, you can cleanup and parse the returned tags.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">findTagsForBlogPost</span><span class="token punctuation">(</span><span class="token parameter">blogPost</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> prompt <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
Given a blog post, suggest appropriate tags for it.
The blog is about software programming.
Extract the 5 most important tags from the blog post.
The tags need to satisfy the following:
- short
- simple
- super generic
- one word (avoid putting words together)
- lowercase, no camelcase
- no duplicate concepts in the tags, e.g. githubcodespaces and github should become github and codespaces
- no numbers as tags
- when a tag gets too long, split it in two or generalize it
- avoid very specific tags
- do not use generic tags like "blog", "post", "programming", "software" etc. (if they're useful to categorize the post it's ok)
- the output should not contain any other extra text
Return just 5 tags in comma separated list form, avoid adding any other text before or after the tags.
Blog Post:
</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>blogPost<span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span><span class="token number">2000</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">...
</span><span class="token template-punctuation string">`</span></span>
<span class="token keyword">const</span> completion <span class="token operator">=</span> <span class="token keyword">await</span> ai<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>completions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">messages</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span> <span class="token literal-property property">role</span><span class="token operator">:</span> <span class="token string">'user'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> prompt <span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token literal-property property">model</span><span class="token operator">:</span> <span class="token string">'gpt-3.5-turbo'</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> suggestedTags <span class="token operator">=</span> completion<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>message<span class="token punctuation">.</span>content<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">Tags\:</span><span class="token regex-delimiter">/</span><span class="token regex-flags">gi</span></span><span class="token punctuation">,</span><span class="token string">''</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> tags <span class="token operator">=</span> suggestedTags<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">','</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">acc<span class="token punctuation">,</span> tag</span><span class="token punctuation">)</span> <span class="token operator">=></span> acc<span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span>tag<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span>i</span><span class="token punctuation">)</span> <span class="token operator">=></span> i <span class="token operator"><</span> <span class="token number">10</span><span class="token punctuation">)</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>tags<span class="token punctuation">)</span>
<span class="token keyword">return</span> tags
<span class="token punctuation">}</span></code></pre>
<h2 id="dogfooding" tabindex="-1">Dogfooding</h2>
<p>I obviously ran the script on this very blog post and the suggested tags were the following:</p>
<pre><code>- post
- software
- programming
- blog
- script
- openai
</code></pre>
<p>Works for me.</p>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>A more sensible approach would be to somehow feed (e.g. in the prompt) the most used tags among the other posts.</p>
<p>I am not 100% fond of the categorization the model returns, but it’s a little better than what I did manually over the years.</p>
<p>It would probably work better with a classic NLP solution, but for fun and convenience this is done with the model API.</p>
<p>PS: It cost about 0.5$ to process ~300 blog posts (a few times).</p>
ChatGPT for learning Jamaican Patois2023-10-07T00:00:00Zhttps://cri.dev/posts/2023-10-07-chatgpt-language-learning-pidgin-creole-patois/<p>Wah gwaan, man?</p>
<p>Lately I have been trying to get a basic knowledge of Jamaican Patois.</p>
<p>And it’s known that it can be helpful for language learning.</p>
<p>But never would I have thought that I could use it as an additional tool for learning Jamaican or other Creole languages.</p>
<p><img src="https://cri.dev/assets/images/posts/apparently-chagtpt-language.jpeg" alt="chatgpt language learning jamaican" loading="lazy" /></p>
<blockquote>
<p>Recent di time, mi a try fi get a basic knowledge a di Jamaican Patois.<br />
An yuh done know seh it can come in handy fi di language learning.<br />
Mi never woulda even tink seh mi could use it as an extra tool fi learn Jamaican or any oddah Creole languages.</p>
</blockquote>
<p>–</p>
A stupid web component2023-10-06T00:00:00Zhttps://cri.dev/posts/2023-10-06-a-stupid-web-component/<p>Below you can find a stupid simple web component, that’s it.</p>
<p>–</p>
<p>On this page I defined the following template:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
<span class="token selector">#cig-component-container button</span> <span class="token punctuation">{</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> 1.5rem<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">#cig-component-container h1</span> <span class="token punctuation">{</span>
<span class="token property">background</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span>
<span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span>
<span class="token property">font-weight</span><span class="token punctuation">:</span> bold<span class="token punctuation">;</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span>
<span class="token property">padding</span><span class="token punctuation">:</span> 0.75rem 1.5rem<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>cig-component-container<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span><span class="token punctuation">></span></span>next<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>br</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span></code></pre>
<p>This gets then used by the <code>cig.mjs</code> file, which defines the webcomponent itself:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">CigComponent</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
<span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">attachEvents</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>handleButtonClick<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> template <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'template'</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> clone <span class="token operator">=</span> template<span class="token punctuation">.</span>content<span class="token punctuation">.</span><span class="token function">cloneNode</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> title <span class="token operator">=</span> clone<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#cig-component-container h1'</span><span class="token punctuation">)</span>
title<span class="token punctuation">.</span>innerText <span class="token operator">=</span> <span class="token function">randomSentence</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>clone<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token function">attachEvents</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> button <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#cig-component-container button'</span><span class="token punctuation">)</span>
button<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">handleButtonClick</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token function">handleButtonClick</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> title <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#cig-component-container h1'</span><span class="token punctuation">)</span>
title<span class="token punctuation">.</span>innerText <span class="token operator">=</span> <span class="token function">randomSentence</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">function</span> <span class="token function">randomSentence</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> sentences <span class="token operator">=</span> <span class="token punctuation">[</span>
<span class="token string">"Programming can harm you and others around you"</span><span class="token punctuation">,</span>
<span class="token string">"Programming harms you, and smoking harms others."</span><span class="token punctuation">,</span>
<span class="token string">"Prioritize well-being, avoid excessive coding and smoking."</span><span class="token punctuation">,</span>
<span class="token string">"Coding and smoking carry risks for you and those nearby."</span><span class="token punctuation">,</span>
<span class="token string">"Healthy habits: code wisely, quit smoking."</span><span class="token punctuation">,</span>
<span class="token string">"Protect yourself and the environment by avoiding smoking while coding."</span><span class="token punctuation">,</span>
<span class="token punctuation">]</span>
<span class="token keyword">return</span> sentences<span class="token punctuation">[</span>Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> sentences<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span>
customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'cig-component'</span><span class="token punctuation">,</span> CigComponent<span class="token punctuation">)</span></code></pre>
<p>Here I am using the <code><cig-component></code>, you can play with it here 👇</p>
<template>
<style>
#cig-component-container button {
font-size: 1.5rem;
}
#cig-component-container h1 {
background: black;
color: white;
font-weight: bold;
font-size: 2rem;
padding: 0.75rem 1.5rem;
}
</style>
<div id="cig-component-container">
<button>next</button>
<br />
<h1></h1>
</div>
</template>
<p><cig-component></cig-component></p>
3d printed stuff over the years2023-10-13T00:00:00Zhttps://cri.dev/posts/2023-10-06-3d-printed-stuff-useful-office-gadgets-travel/<h2 id="cable-reel" tabindex="-1">Cable reel</h2>
<p>It took 5+ hours to print all three parts for the bigger version, and perhaps 2 for the small one.</p>
<p>I suggest to print it in high quality since it has a screw mechanism.</p>
<p>👉 <a href="https://www.thingiverse.com/thing:2856991">https://www.thingiverse.com/thing:2856991</a></p>
<p><img src="https://cdn.thingiverse.com/renders/00/3d/fb/c7/77/67747b83cbf494375b673b06a4ba5c42_display_large.jpg" alt="cable reel" loading="lazy" /></p>
<h2 id="raspberry-pi-4-case" tabindex="-1">Raspberry Pi 4 case</h2>
<p>This one is a level-up to the one I purchased, because it has way better ventilation, plus I printed it in yellow so it looks cool.</p>
<p>On <a href="https://www.printables.com/model/31298-raspberry-pi-4-case">printables</a></p>
<p><img src="https://cri.dev/assets/images/posts/3d-printing/rpi-case-3d-printed.png" alt="raspberry pi case 3d printed" loading="lazy" /></p>
<h2 id="cable-organizer" tabindex="-1">Cable organizer</h2>
<p>A must-print as soon as you get a 3d printer.</p>
<p>Get it on <a href="https://www.thingiverse.com/thing:A282474">thingiverse</a></p>
<p><img src="https://cdn.thingiverse.com/renders/15/da/32/78/ab/cable-organizer_display_large.jpg" alt="cable organizer" loading="lazy" /></p>
<h2 id="esp32-cases" tabindex="-1">ESP32 cases</h2>
<p>I have a few of these lying around in case I need one right up</p>
<p>On <a href="https://www.thingiverse.com/thing:A4125952">thingiverse</a></p>
<p><img src="https://cdn.thingiverse.com/assets/24/21/98/61/2a/large_display_IMG_9870.jpg" alt="esp32 case" loading="lazy" /></p>
<h2 id="airtag-holder" tabindex="-1">AirTag holder</h2>
<p>On <a href="https://www.thingiverse.com/thing:A4845088">thingiverse</a></p>
<p><img src="https://cdn.thingiverse.com/assets/c1/57/4e/26/0e/large_display_AT1_Key.png" alt="airtag holder" loading="lazy" /></p>
<h2 id="apple-watch-desk-stand" tabindex="-1">Apple Watch desk stand</h2>
<p>On <a href="https://www.thingiverse.com/thing:A1056923">thingiverse</a></p>
<p><img src="https://cdn.thingiverse.com/renders/44/45/7c/54/52/IMG_0093_display_large.JPG" alt="apple watch desk stand" loading="lazy" /></p>
<h2 id="rubber-duck" tabindex="-1">Rubber duck</h2>
<p>On <a href="https://www.thingiverse.com/thing:A4820726">thingiverse</a></p>
<p><img src="https://cdn.thingiverse.com/assets/93/46/40/bc/3d/large_display_Rubber_Duck.png" alt="rubber duck" loading="lazy" /></p>
Apple watch personal use cases2023-10-04T00:00:00Zhttps://cri.dev/posts/2023-10-04-apple-watch-personal-use-cases/<p>Here I want to write down my personal use cases for the Apple watch.</p>
<p>–</p>
<p>View time as you please, together with other widgets, analogue, or plain vanilla digits.</p>
<p>To me the Apple watch is like an iPhone remote.</p>
<p>Quickly lookup information with Siri, start timers while cooking, etc.</p>
<p>You can check notifications, also respond to messages directly from the watch, play next/pause the currently playing media, etc.</p>
<p>Call and respond to calls is also pretty useful.</p>
<p>For what’s worth, it keeps me motivated to get out and close my daily activity rings.</p>
<p>Sleep tracking works pretty well. In conjunction with Focus mode, or better Sleep mode, during the night the watch shouldn’t cause any interupption or your sleep.</p>
<p>Keep track of stocks at a glance.</p>
<p>Pay on the go using your watch, also quickly access transportation tickets and such.</p>
<p>Use the Apple watch as a bedside/office clock.</p>
<p>Record important thoughts/conversations on the go.</p>
My iPad/iOS 17 wish list for 20242023-10-16T00:00:00Zhttps://cri.dev/posts/2023-09-27-ipad-ios-17-wish-list-2024/<p>I’ve been using an iPad Pro since the beginning of 2023 and it’s been a great experience so far.</p>
<p>This digital “toy” is a real power-station and <a href="https://cri.dev/posts/2023-03-22-ipad-programming-github-codespaces-raspberry-pi-vscode/">suits most of my needs</a>.</p>
<p>Although I don’t expect certain features to ever be available, I’m still hopeful that Apple will come up with some great implementations.</p>
<p>btw, check out my <a href="https://cri.dev/uses/">/uses</a> page</p>
<p>–</p>
<h2 id="1.-better-apple-watch-integration" tabindex="-1">1. Better Apple watch integration</h2>
<p>I don’t know, perhaps the ability to use the Fitness app like on the phone?</p>
<p>Or unlocking the iPad with the watch would be cool too.</p>
<p>Remote media playback controls between the watch and the iPad.</p>
<h2 id="2.-audio-source-control-like-on-macos" tabindex="-1">2. Audio source control like on MacOS</h2>
<p>When using an external monitor to the iPad, you rely on the audio capabilities of the monitor.</p>
<p>That’s a pity when the monitor doesn’t have audio.</p>
<p>What solutions do you have? Disconnect the iPad, or connect your headphones or bluetooth speaker.</p>
<p>I would like to be able to use the iPad speakers even though I’m connected to an external monitor.</p>
<p>Update 2023-10-16: Apple finally added <a href="https://cri.dev/posts/2023-10-15-apple-added-ipad-audio-control-external-monitor/">iPad audio control</a> when connected to an external monitor!</p>
<h2 id="3.-let-me-have-a-shell%2C-damn-it!" tabindex="-1">3. Let me have a shell, damn it!</h2>
<p>I’m currently kind of escaping the lack of a terminal experience by using Blink shell.</p>
<p>Do you imagine being able to run docker images on this machine?!</p>
<p>Yeah I know, dream on 🤣</p>
<h2 id="4.-native-vscode-app" tabindex="-1">4. Native VSCode app</h2>
<p>That would be dope.</p>
<p>Even a simplified version could be fine, I don’t want to ask too much.</p>
<p>I’m currently using vscode/github.dev or Blink shell with VSCode integration.</p>
<h1 id="finally" tabindex="-1">Finally</h1>
<p>I got pretty much sold to the iPad Pro once Stage Manager was released.</p>
<p>Also vscode.dev/github.dev were a game changer, meaning Visual Studio Code integration on the web.</p>
<p>Additionally, the release of VSCode tunnels and VSCode server by using my Raspberry Pi 4, made the whole experiment of having the iPad Pro as my main device very compelling.</p>
<p>There are definitely some rough edges, but I can find workarounds for most of them.</p>
<p>Although very annoying, you can get a lot of developer work done on this machine</p>
<h1 id="update-2023-10-15" tabindex="-1">Update 2023-10-15</h1>
<p>Apple finally added <a href="https://cri.dev/posts/2023-10-15-apple-added-ipad-audio-control-external-monitor/">iPad audio control</a> when connected to an external monitor!</p>
Protonmail iOS team seems super reactive to user feedback2023-09-19T00:00:00Zhttps://cri.dev/posts/2023-09-19-protonmail-ios-team-super-reactive-user-feedback/<p>Have been using Protonmail since 2020 I think, and it’s been a positive experience.</p>
<p>Recently I gave them some feedback and the team prompty implemented or solved the issue. Awesome!</p>
<p>This post is not sponsored or whatever.</p>
<p>–</p>
<h2 id="ipad-second-display-issue" tabindex="-1">iPad second display issue</h2>
<p>There was a strange quirk when opening the Protonmail app on the iPad main display.</p>
<p>Another window of Protonmail would pop-up, not scaled properly and without the ability to interact with it.</p>
<p>It was super strange, especially since opening the app from the main dipslay worked fine!</p>
<h2 id="customize-toolbar" tabindex="-1">Customize toolbar</h2>
<p>I had the need to quickly archive email once read, as part of my email workflow.</p>
<p>In the iOS app I needed to read the email, open a menu and the archive it.</p>
<p>Now you can quickly access the archive button from the toolbar via a button.</p>
<p>–</p>
<p>Awesome work, Protonmail iOS team!</p>
Southworking2023-09-14T00:00:00Zhttps://cri.dev/posts/2023-09-14-southworking/<blockquote>
<p>Per fare soldi?<br />
<a href="https://www.reddit.com/r/ItaliaPersonalFinance/comments/16hlit5/ho_pisciato_fuori_dal_vaso/k0ekksr/">Full remote per azienda estera e trasferimento a Catanzaro</a></p>
</blockquote>
<p>Mi ha fatto sorridere questo commento su un post sul subreddit di ItaliaPersonalFinance, ma poi ho scoperto il concetto di <a href="https://www.southworking.org/">“SouthWorking”</a></p>
<blockquote>
<p>Lavorare dove desideri fa bene a te e ai territori</p>
</blockquote>
<p><a href="https://www.reddit.com/r/ItaliaPersonalFinance/comments/16hlit5/comment/k0ekksr/">Commento su Reddit ItaliaPersonalFinance</a></p>
Wanna learn something? Recreate it2024-11-16T00:00:00Zhttps://cri.dev/posts/2023-09-03-learning-recreate-software-framework/<p>In the past few years I embraced the mantra “Wanna learn something? Recreate it”.</p>
<p>What do I mean by it?</p>
<p>Here some examples:</p>
<ol>
<li>
<p>I wanted to better understand how a static site generator (SSG) worked, so I created <a href="https://cri.dev/posts/2020-04-19-devblog-yet-another-static-site-generator-seriously/">devblog</a>, and even converted this very site to be compatible with it</p>
</li>
<li>
<p>Are you easily stunned by charts, real-time updates, and perhaps crypto (oops, I said it)? <a href="https://github.com/christian-fei/crypto_watch">Recreated (in part)</a> the Coinbase Trading UI and that’s how I got into Elixir and landed my first job where I daily used Elixir. That’s fucking awesome, I think</p>
</li>
<li>
<p>When in my team we were frequently using Trello, I had <a href="https://github.com/christian-fei/simple-trello-api">quite</a> some <a href="https://github.com/christian-fei/trellogram">fun</a> creating <a href="https://github.com/christian-fei/trello-recap">libs</a> around Trello that suited <a href="https://github.com/christian-fei/trello-replay">my personal needs</a></p>
</li>
<li>
<p>At some point in my career I had to deal with scraping product reviews from popular shops, and do it reliably. After evaluating various techniques and tools, for some part of the scraping we were using <a href="https://github.com/christian-fei/mega-scraper">mega-scraper</a> and <a href="https://github.com/christian-fei/bull-dashboard">bull-dashboard</a></p>
</li>
</ol>
<p>I learned the most by diving deep into a certain topic, almost like a maniac, and produced something as an output. If it’s public and dirty, no need to be ashamed.</p>
<p>Found out about an awesome repo with countless examples of building recreating stuff from scratch:</p>
<p><a href="https://github.com/codecrafters-io/build-your-own-x?tab=readme-ov-file">build-your-own-x</a></p>
Learning about Web Components2023-08-25T00:00:00Zhttps://cri.dev/posts/2023-08-25-learning-web-components/<p>For now this is just a collection of useful links while organizing my thoughts on web components.</p>
<h1 id="learning" tabindex="-1">learning</h1>
<p><a href="https://www.webcomponents.org/">https://www.webcomponents.org/</a></p>
<p><a href="https://javascript.info/shadow-dom">https://javascript.info/shadow-dom</a></p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots">https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots</a></p>
<p><a href="https://open-wc.org/codelabs/basics/web-components">https://open-wc.org/codelabs/basics/web-components</a></p>
<p><a href="https://www.bitovi.com/academy/learn-web-components.html">https://www.bitovi.com/academy/learn-web-components.html</a></p>
<h1 id="code" tabindex="-1">code</h1>
<p><a href="https://github.com/mdn/web-components-examples">https://github.com/mdn/web-components-examples</a></p>
<p><a href="https://github.com/GoogleChromeLabs/file-drop/tree/master">https://github.com/GoogleChromeLabs/file-drop/tree/master</a></p>
<p><a href="https://genericcomponents.netlify.app/">https://genericcomponents.netlify.app/</a></p>
<h1 id="documentation" tabindex="-1">documentation</h1>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template">https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template</a></p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot">https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot</a></p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scoping">https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scoping</a></p>
<p><a href="https://javascript.info/template-element">https://javascript.info/template-element</a></p>
<h1 id="blog-posts" tabindex="-1">blog posts</h1>
<p><a href="https://kinsta.com/blog/web-components/">https://kinsta.com/blog/web-components/</a></p>
<p><a href="https://nolanlawson.com/2023/08/23/use-web-components-for-what-theyre-good-at/">https://nolanlawson.com/2023/08/23/use-web-components-for-what-theyre-good-at/</a></p>
<p><a href="https://daverupert.com/2023/07/why-not-webcomponents/">https://daverupert.com/2023/07/why-not-webcomponents/</a></p>
Unblock single domain in AdGuard Home2023-07-24T00:00:00Zhttps://cri.dev/posts/2023-07-24-unblock-single-domain-adguard-home/<p>Needed to unblock <code>t.co</code> because it was getting annoying over time.</p>
<p>Finding the menu item was simple and intuitive, but the suggestions on that page to unblock a certain domain were not working.</p>
<p>I resorted to the following to get <code>t.co</code> links to work:</p>
<pre><code>@@||t.co^$important
</code></pre>
Airtag battery still low after replacing with new CR 2032 batteries2023-07-08T00:00:00Zhttps://cri.dev/posts/2023-07-08-airtag-battery-still-low-cr-2032-bitterant-coating-duracell/<p>Upon receiving the notification of low battery on your Airtag, you purchase fresh CR 2032 batteries, unpack them, and proceed to replace the Airtag battery.</p>
<p>You open the “Find My” app to check the battery status, only to find that it is still low.</p>
<p>After conducting a quick Google search and referring to some Apple documentation (found <a href="https://support.apple.com/en-gb/HT211670">here</a>), you discover that CR 2032 batteries with a bitterant coating may not work properly with Airtags.</p>
<p>It then dawns on you that you unintentionally bought batteries with the bitterant coating, as indicated by the image of a kid with a disgusted expression on the packaging.</p>
<h2 id="resolving-the-bitterant-issue" tabindex="-1">Resolving the Bitterant Issue</h2>
<p>How did I resolve this problem?</p>
<p>I gently scraped off the top coating from the negative pole with the bitterant and optionally cleaned it with isopropyl alcohol.</p>
<p><img src="https://cri.dev/assets/images/posts/airtag-bitterant-coating.jpeg" alt="airtag bitterant coating" loading="lazy" /></p>
<p>Please note: Don’t Do This At Home 🤫</p>
Opinions on working fully remotely2023-07-03T00:00:00Zhttps://cri.dev/posts/2023-07-03-remote-work-opinions-software-development/<p>The COVID-19 pandemic changed the way we work, forcing many organizations to adopt remote work as the new norm.<br />
Some years have passed and many want to continue this trend, while others wish to go back to the office.<br />
Let’s take a look at what people are saying <a href="https://news.ycombinator.com/item?id=36458885">on HackerNews</a>:</p>
<h1 id="advocates" tabindex="-1">Advocates</h1>
<p>Quite a few people see the advantages of working fully remotely, like:</p>
<ul>
<li>freedom and flexibility</li>
<li>eliminates the dreaded commute</li>
<li>increased focus at home</li>
<li>fewer distractions</li>
<li>setting their own hours</li>
<li>stay connected with family</li>
<li>prioritize their well-being</li>
<li>healthier work-life balance</li>
</ul>
<p>But there are also a few cons: some dread the forced team-building exercises that often accompany remote working.</p>
<p>Some have fully embraced the advantages of remote work. They have been working remotely for many years and can’t imagine going back.</p>
<h2 id="opponents" tabindex="-1">Opponents</h2>
<p>On the other hand, some people have fond memories of in-person collaboration.<br />
They remember the camaraderie and close teamwork that happened when everyone was together in a physical office. They believe that if the team is not fully present, there is little value in commuting for a partially remote setup.</p>
<h2 id="the-hybrid-approach" tabindex="-1">The hybrid approach</h2>
<p>However, there are also those who prefer the hybrid approach.<br />
They recognize the benefits of both remote and in-person work and believe that a balance needs to be found.<br />
They suggest a mix of 2 to 3 days in the office per week to maintain flexibility and collaboration.</p>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>In conclusion, the perspectives on fully remote work are diverse. Some praise the autonomy and flexibility it offers, while others emphasize the value of in-person collaboration and team cohesion. Ultimately, the ideal work arrangement depends on personal preferences, the nature of the job, and the dynamics of the team. Striking a balance between remote and in-person work may be the key to ensuring employee satisfaction and productivity as we move forward</p>
<h2 id="in-my-opinion" tabindex="-1">In my opinion</h2>
<p>I think that a hybrid approach <em>would</em> be awesome, but the implementation most of the times is terrible. Someone is going to be cut off from decisions and discussions. So I personally don’t like the hybrid approach.</p>
<p>If I would have the strong need to be able to work fully remotely (e.g. living in the countryside, far away from central hubs), all or nothing for me. Either fully remote or nothing.</p>
Favorite music while programming2023-06-27T00:00:00Zhttps://cri.dev/posts/2023-06-27-favorite-music-programming-chill-lofi-trap-2023/<p>Below you can find my favorite channels I use to listen to music while programming</p>
<blockquote>
<p>You can even rap about Node.js and JavaScript on these beats if you feel like it 😅</p>
</blockquote>
<h2 id="anabolic-beatz" tabindex="-1"><a href="https://youtu.be/ohiWZiGCI8I">Anabolic Beatz</a></h2>
<p>My all-time favorite producer.</p>
<h2 id="lofi-girl" tabindex="-1"><a href="https://www.youtube.com/live/jfKfPfyJRdk">Lofi Girl</a></h2>
<p>That famous girl that studies 24/7.</p>
<p>Alternatively via <a href="https://www.lofi.cafe/">lofi.cafe</a> or Spotify</p>
<h2 id="fat-cat-beats" tabindex="-1"><a href="https://youtu.be/AjZNAUz3kes">Fat Cat Beats</a></h2>
<p>Amazing old school / boom bap beats</p>
<h2 id="bupa-beats" tabindex="-1"><a href="https://youtu.be/2m4Uct95_Vk">Bupa Beats</a></h2>
<p>Super engaging trap/rap instrumentals, repetitive, perfect for getting in the flow</p>
<h2 id="darkside" tabindex="-1"><a href="https://youtu.be/S_a2IeeO2CA">Darkside</a></h2>
<p>“Darkside, light this ***** up”</p>
WebAuthn and Secure Payments Confirmation (SPC)2023-06-15T00:00:00Zhttps://cri.dev/posts/2023-06-15-web-authentication-and-payments-future/<p>The World Wide Web Consortium (W3C) has introduced a RC version of the web standard called Secure Payment Confirmation (SPC) to streamline user authentication and improve security when making payments on the web.</p>
<p>It uses Web Authentication (WebAuthn) to register and authenticate users, enabling them to make purchases using Touch ID or Windows Hello.</p>
<p><strong>Links</strong></p>
<p>On SPC</p>
<p><a href="https://www.applemust.com/w3c-announces-new-web-standard-for-online-payments/">https://www.applemust.com/w3c-announces-new-web-standard-for-online-payments/</a></p>
<p><a href="https://www.w3.org/2023/06/pressrelease-spc-cr.html.en">https://www.w3.org/2023/06/pressrelease-spc-cr.html.en</a></p>
<p><a href="https://developer.chrome.com/articles/secure-payment-confirmation/">https://developer.chrome.com/articles/secure-payment-confirmation/</a></p>
<p><a href="https://www.w3.org/TR/secure-payment-confirmation/">https://www.w3.org/TR/secure-payment-confirmation/</a></p>
<p>On Payment Services Directive (PSD2)</p>
<p><a href="https://www.ecb.europa.eu/paym/intro/mip-online/2018/html/1803_revisedpsd.en.html">https://www.ecb.europa.eu/paym/intro/mip-online/2018/html/1803_revisedpsd.en.html</a></p>
Ti ricordi di Jumpy?2023-06-13T00:00:00Zhttps://cri.dev/posts/2023-06-13-chi-si-ricorda-di-jumpy/<p>Voglio portarti un pò indietro nel tempo in questo esercizio di ricordi sparsi e parziali dei miei primi passi su internet.</p>
<p>Un paio di settimane fa mi è venuto in mente “Jumpy”, l’ISP che utilizzavo nei primi anni 2000.</p>
<p>Costava un occhio della testa ai miei e si <s>navigava</s>barcollava a 56 Kbit/s.</p>
<blockquote>
<p>Quali sono le principali differenze tra il nuovo servizio Jumpy e un accesso ad Internet tradizionale? La principale differenza è che con Jumpy non è necessario pagare alcun costo di abbonamento, canone o costo di attivazione. La tariffa è semplice e chiara: la spesa per l’uso di Internet viene inclusa nella normale bolletta telefonica. In più Jumpy ti regala un indirizzo di email personalizzato</p>
</blockquote>
<p><a href="http://web.tiscali.it/asseuropa93/">Dal sito archiviato</a>, ancora semi accessibile</p>
<p>Era avanti anni luce in poche parole.</p>
<p>Avevo 7-8 anni quando ho iniziato a fare i miei primi passi su internet, con Windows ME, “Millenium Edition”.</p>
<p><em>Grazie Zii per quel tower con un Intel Pentium ®️ qualcosa.</em></p>
<p>Se non mi ricordo male nel 2001.</p>
<p>Ma che figata di nome e tempistica era scusa?</p>
<p><img src="https://cri.dev/assets/images/posts/windows-millenium-edition.png" alt="windows millenium edition" loading="lazy" /></p>
<p><a href="https://seeklogo.com/vector-logo/92326/microsoft-windows-millenium-edition">seeklogo.com</a></p>
About 1:1s2023-06-09T00:00:00Zhttps://cri.dev/posts/2023-06-09-about-one-on-one-meetings/<p>Let’s talk about weekly 1:1s. Some people swear by them, and others think they’re a waste of time. But what makes them effective?</p>
<p>Context: Discussion on HackerNews about <a href="https://news.ycombinator.com/item?id=36254217">“What’s your opinion on weekly 1:1s?”</a></p>
<p>One thing that many people agree on is the importance of <strong>consistency.</strong><br />
When everyone shows up for these meetings week after week, it builds <strong>trust and reliability</strong>.<br />
Workers feel they can <strong>discuss ideas</strong>, <strong>concerns</strong>, and <strong>issues</strong> without fear because these meetings happen regularly.</p>
<p><strong>Trust</strong> is also a key factor.<br />
When you have a good working relationship with your colleague, weekly 1:1s become a valuable tool for <strong>open communication</strong> and <strong>problem-solving</strong>.<br />
The atmosphere of trust can facilitate healthy conversations and <strong>transparency</strong>.</p>
<p>When done right, these meetings can also benefit <strong>career development.</strong><br />
For example, they offer a platform to discuss <strong>professional opportunities</strong>, ask for <strong>feedback on performance</strong>, and identify <strong>areas for growth</strong>.<br />
It can demonstrate a <strong>commitment</strong> to an employee’s <strong>personal development</strong>, making them feel invested in their <strong>career</strong>.</p>
<p>On the other hand, some may feel like these meetings <strong>don’t add value</strong> or are merely an <strong>unnecessary distraction.</strong><br />
They can be <strong>distracting</strong> to the workflow when having too many people involved or scheduling several meetings without an adequate structure.<br />
It’s essential to assess the individual, <strong>team dynamics</strong>, and <strong>frequency</strong> of the meetings and adjust as needed.</p>
<p>Another potential pitfall is when the <strong>conversation doesn’t flow naturally.</strong><br />
Some people may feel *<em>uncomfortable+</em> discussing certain topics during a one-on-one discussion, making it challenging to cultivate transparency.<br />
It’s the manager’s responsibility to create a <strong>safe space</strong> and guidelines to support <strong>constructive</strong> conversations, transparent communication, and professional development.</p>
<p>In conclusion, whether weekly 1:1s are helpful or not <strong>depends on the individuals</strong> involved and their <strong>communication</strong> style.</p>
<p>It is not a one-size-fits-all approach. <strong>What works for one group may not work for another</strong>, and it is up to the management to create an environment that can encourage the type of communication and <strong>feedback</strong> needed for each team’s success.</p>
<p>Weekly 1:1s can be incredibly valuable when there’s consistency, <strong>trust</strong>, and clear <strong>structure</strong>, creating a <strong>supportive space</strong> for open communication, problem-solving, and career development.</p>
Modern frontend apps with ES modules and import maps2023-05-03T00:00:00Zhttps://cri.dev/posts/2023-05-03-Modern-frontend-apps-using-ES-modules-and-import-maps/<p>With import maps you can map external module names to their relative URLs, on a CDN for example, which makes it super easy to use external modules, natively in 2023.</p>
<p>By defining a <code><script type="importmap"></code> tag you can map the various modules you like to include in your web application, leveraging ES modules.</p>
<p>Put the mapping the in the <code><head></code> tag of the document and can import modules using the import tag inside a <code><script type=“module”></code> tag.</p>
<p>Additionally, if you want to gracefully degrade the application somehow, or progressively enhance if you prefer, you can use <code>HTMLScriptElement.supports('importmap')</code> to detect if the browser supports import maps.</p>
<h2 id="example" tabindex="-1">Example</h2>
<p>Define a <code><script type=“importmap”></code> in the <code><head></code> section with the following content:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"imports"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"app"</span><span class="token operator">:</span> <span class="token string">"/modules/app.js"</span><span class="token punctuation">,</span>
…
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Now inside a <code><script type=“module”></code> tag you can import the app module, e.g.:</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span>mount<span class="token punctuation">,</span> render<span class="token punctuation">,</span> …<span class="token punctuation">}</span> from “app”
</code></pre>
<p>For more info check out <a href="https://web.dev/import-maps-in-all-modern-browsers/">web.dev</a> with their great article on the subject.</p>
<p>PS: I use unpkg as a CDN in my hobby projects and I’m super happy with it</p>
MiCA pizza e fichi2023-07-26T00:00:00Zhttps://cri.dev/posts/2023-04-21-MiCA-pizza-e-fichi/<p>Alcuni lo chiamano MiCA, altri MiCAR.</p>
<p>Markets in Crypto Assets (Regulation): una prima regolamentazione per garantire l’accessibilità dei servizi bancari alle imprese che hanno a che fare con criptovalute.</p>
<p>MiCA si applica a persone o entità coinvolte nell’emissione, offerta al pubblico e negoziazione di cripto-attività o che offrono servizi correlati in Europa.</p>
<p>Le cripto-attività sono rappresentazioni digitali di valore o diritti trasferibili e conservabili elettronicamente su tecnologie come i registri distribuiti.</p>
<p>Esistono tre categorie di cripto-attività:</p>
<ol>
<li>i token di moneta elettronica (EMT), che utilizzano una singola valuta per stabilizzare il loro valore</li>
<li>i token legati ad asset (ART), che ancorano il loro valore a qualcos’altro o a un paniere di valute e asset, come il progetto Diem (precedentemente Libra)</li>
<li>gli altri tipi di cripto-attività non rientranti nelle prime due categorie.</li>
</ol>
<p>Il MiCA essenzialmente comprende (<a href="https://www.europarl.europa.eu/news/en/press-room/20230414IPR80133/crypto-assets-green-light-to-new-rules-for-tracing-transfers-in-the-eu">link al testo</a>):</p>
<ul>
<li>
<p>Un quadro normativo uniforme per i mercati delle cripto-attività all’interno dell’Unione Europea</p>
</li>
<li>
<p>tracciabilità delle operazioni effettuate con cripto-attività, alla pari dei tradizionali trasferimenti di denaro</p>
</li>
<li>
<p>misure volte a migliorare la protezione dei consumatori, la prevenzione della manipolazione del mercato e del crimine finanziario</p>
</li>
</ul>
<p>Tuttavia, rimangono ancora incertezze riguardanti le regole che riguardano gli NFT e i servizi DeFi.</p>
<p>Inoltre, è ancora poco chiaro come la nuova normativa si applicherà alle aziende che operano in Europa ma hanno sede negli Stati Uniti.</p>
<p>Comunque sia, il regolamento rappresenta un’opportunità per migliorare la trasparenza e la sicurezza nell’ambito delle criptovalute.</p>
<p>Fonti</p>
<p><a href="https://www.europarl.europa.eu/news/en/press-room/20230414IPR80133/crypto-assets-green-light-to-new-rules-for-tracing-transfers-in-the-eu">Crypto-assets: green light to new rules for tracing transfers in the EU</a></p>
<p><a href="https://blockworks.co/news/mica-could-get-crypto-banked">EU’s MiCA Framework Could Help Crypto Firms Get Banked</a></p>
<p><a href="https://www.coindesk.com/policy/2023/04/20/eu-parliament-approves-crypto-licensing-funds-transfer-rules/">The European Parliament Has Voted for the EU’s Landmark MiCA Regulation and Anti-Money Laundering Transfer of Funds Rules</a></p>
<p><a href="https://www.agendadigitale.eu/documenti/cripto-attivita-ecco-il-regolamento-mica-le-nuove-regole-da-conoscere/">Cripto-attività, ecco il regolamento Mica: le nuove regole da conoscere</a></p>
<p><a href="https://decrypt.co/138713/what-is-mica-eu-crypto-regulation-explained">What is MiCA? The European Union’s Landmark Crypto Regulation Explained</a></p>
My impractical RSS reader in Google Sheets2023-04-20T00:00:00Zhttps://cri.dev/posts/2023-04-20-My-impractical-RSS-reader-in-Google-Sheets/<p>While exploring how to build an RSS reader, I stumbled across the idea of using Google Sheets to display RSS feed content.</p>
<p>Just for fun, I decided to give it a shot and see how far I could get.</p>
<p>I am using <a href="https://cri.dev/posts/2023-03-25-ansible-miniflux-nginx-provisioning-self-hosting/">Miniflux</a> daily, but I am also having some fun coding up a stupid RSS reader on my own.</p>
<p>I found the idea of creating an ‘impractical’ RSS reader in Google Sheets intriguing, so I decided to give it a shot.</p>
<p>You know, using “No-code” aka Spreadsheets.</p>
<p>Keep reading to find out how I did it, and what I learnt along the way.</p>
<p><img src="https://cri.dev/assets/images/posts/sheets-rss-feed.jpeg" alt="sheets rss feed" loading="lazy" /></p>
<h2 id="creating-your-impractical-rss-reader" tabindex="-1">Creating Your Impractical RSS Reader</h2>
<p>Create a new Google Sheet.</p>
<p>In the <code>A1</code> cell enter the following:</p>
<pre><code>=QUERY({QUERY(IMPORTFEED("https://cri.dev/rss.xml"), "select Col1, Col3, Col4")}, "select *")
</code></pre>
<p>You’ll be presented with the latest blog posts from my RSS feed, with the title, the URL and content of the post.</p>
<p>I think it’s super cool.</p>
<p>PS: To add multiple feeds and combine them, you can use the following:</p>
<pre><code>=QUERY({QUERY(IMPORTFEED("https://cri.dev/rss.xml"), "select Col1, Col3, Col4"); QUERY(IMPORTFEED("https://hnrss.org/frontpage"), "select Col1, Col3, Col5")}, "select *")
</code></pre>
<h2 id="challenges-and-solutions" tabindex="-1">Challenges and Solutions</h2>
<p>Learnt along the way, you can combine multiple “data ranges” using the <code>{}</code> union operator.</p>
<p>That’s essentially the trick to combine multiple ranges of data.</p>
<p>Tried to mess around with filters, but they don’t seem to play weel alongside a <code>QUERY</code> function.</p>
<p>Don’t know exactly how to work that issue out.</p>
<p>Also I tried to implement the concept of “article read” using checkboxes.</p>
<p>But of course, once the feed updates and new content updates/overwrites the view, the data is all out of order.</p>
<p>So I guess that’s a no-go.</p>
<p>Another idea was to calculate the reading time, and then sort the feed entries by that column.</p>
<p>If you find out how to do it, please do let me know, even though of course I won’t be using this method to read my RSS feeds 😅.</p>
Provisioning a Raspberry Pi with Ansible (and an iPad)2023-04-20T00:00:00Zhttps://cri.dev/posts/2023-04-20-Provisioning-a-Raspberry-Pi-with-Ansible-Playbook-iPad-Docker/<p>The repository accompaining this blog post can be found <a href="https://github.com/christian-fei/ansible-pi">on GitHub</a></p>
<p>It contains the whole code to get your Raspberry Pi up and running with Docker and Docker Compose, set it up for network access through USB-C and configure a few other things.</p>
<p>There is an excellent <a href="https://neoighodaro.com/posts/10-setting-up-raspberry-pi-to-work-with-your-ipad">post about configuring your Pi</a> to connect to your iPad via USB-C.</p>
<p>I automated the process with Ansible and added a few other things.</p>
<h2 id="personal-setup" tabindex="-1">Personal setup</h2>
<p>I have an iPad Pro M1 and a Raspberry Pi 4B 8GB.</p>
<p>Sometimes I connect them via USB-C.</p>
<p>But most of the times the Pi is connected via WiFi to my home network.</p>
<p>When doing 3D printing stuff, I connect through VNC to the Pi.</p>
<p>On the iPad I use <a href="https://blink.sh/">Blink shell</a> (no affiliation) to connect to the Pi over SSH via WiFi.</p>
<p>On the Pi, I provision the Pi itself, by running <code>ansible-playbook playbook.yml</code></p>
<p>Again on the Pi, that’s where I provision my server(s) with Ansible too.</p>
<h2 id="conclusions-(ipad-%2B-pi)" tabindex="-1">Conclusions (iPad + Pi)</h2>
<p>The experience with Blink Shell is surprisingly sleak and quite usable to be honest.</p>
<p>I can even run VSCode inside Blink Shell and edit files on the Pi, use GitHub Codespaces and generally do most of the things I do on my laptop.</p>
<p>Managing Docker containers on the Pi can be done with ease from the iPad.</p>
<p>Various services run on Docker in my home network and can be accessed from e.g. my iPad or phone.</p>
<p>To be clear: currently nothing beats a computer with a keyboard and a mouse, but this is my setup for now and I make it work for me.</p>
Node.js 20 is here2023-04-19T00:00:00Zhttps://cri.dev/posts/2023-04-19-nodejs-20-is-here/<p>Node.js 20 has been released, and it brings several new features and improvements for developers.</p>
<p>Let’s take a closer look at some of them.</p>
<h2 id="stable-test-runner" tabindex="-1">Stable test runner</h2>
<p>Node.js 20 introduces a stable test runner module!</p>
<p>You can use describe, it/test, and hooks to structure test files, as well as watch mode, running multiple test files in parallel, and mocking.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> test<span class="token punctuation">,</span> mock <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:test'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> assert <span class="token keyword">from</span> <span class="token string">'node:assert'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> fs <span class="token keyword">from</span> <span class="token string">'node:fs'</span><span class="token punctuation">;</span>
mock<span class="token punctuation">.</span><span class="token function">method</span><span class="token punctuation">(</span>fs<span class="token punctuation">,</span> <span class="token string">'readFile'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token string">"Hello World"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">test</span><span class="token punctuation">(</span><span class="token string">'reads file content'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">t</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
assert<span class="token punctuation">.</span><span class="token function">strictEqual</span><span class="token punctuation">(</span><span class="token keyword">await</span> fs<span class="token punctuation">.</span><span class="token function">readFile</span><span class="token punctuation">(</span><span class="token string">'a.txt'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">"Hello World"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Note that some features of the test runner are not yet stable.</p>
<h2 id="permission-model" tabindex="-1">Permission Model</h2>
<p>The experimental Permission Model allows developers to restrict access to specific resources during execution.</p>
<p>For example, you can manage file system access, <code>child_process</code>, <code>worker_threads</code>, and native addons.</p>
<p>Use the <code>--experimental-permission</code> flag.</p>
<p>You can also use the <code>--allow-*</code> flags to grant specific permissions.</p>
<p>Use specific paths for file system access by passing comma-separated values to the flags.</p>
<p>For example, this command allows write access to the <code>/tmp/</code> folder:</p>
<pre class="language-bash"><code class="language-bash">$ <span class="token function">node</span> --experimental-permission --allow-fs-write<span class="token operator">=</span>/tmp/ --allow-fs-read<span class="token operator">=</span>/home/index.js index.js</code></pre>
<p>Use <code>process.permission.has()</code> method to check if a certain permission has been granted at runtime. For example:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">if</span> <span class="token punctuation">(</span>process<span class="token punctuation">.</span>permission<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span><span class="token string">'fs.write'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// ...</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>process<span class="token punctuation">.</span>permission<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span><span class="token string">'fs.write'</span><span class="token punctuation">,</span> <span class="token string">'/etc/hosts'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// ...</span>
<span class="token punctuation">}</span></code></pre>
<p>It’s still experimental and may change in future releases of Node.js.</p>
<h2 id="performance-improvements" tabindex="-1">Performance improvements</h2>
<p>E.g., the cost of initializing EventTarget has been cut by half, while V8 Fast API calls have been leveraged to improve performance in APIs like URL.canParse() and timers.</p>
<h2 id="eol-for-node.js-14" tabindex="-1">EOL for Node.js 14</h2>
<p>Node.js version 14 is set to reach End-of-Life in April 2023. Therefore, upgrading to either Node.js 18 (LTS) or Node.js 20 (soon to be LTS) is recommended to ensure that applications remain secure and up to date.</p>
<p>View the full changelog for more details:</p>
<p><a href="https://nodejs.org/en/blog/announcements/v20-release-announce">https://nodejs.org/en/blog/announcements/v20-release-announce</a></p>
ChatGPT practical use cases and examples2023-03-25T00:00:00Zhttps://cri.dev/posts/2023-03-25-ChatGPT-practical-use-cases-examples/<p>Found <a href="https://news.ycombinator.com/item?id=35299071">this discussion</a> on HackerNews about “How are you using GPT to be productive?” and thought it was interesting.</p>
<p>I’ve been using ChatGPT for a while now and have found it to be a great tool for getting things done.</p>
<p>ChatGPT has become an indispensable tool for many professionals in various fields. Its ability to understand natural language and provide accurate responses has made it a go-to resource for many tasks.</p>
<p>Below are some ways in which people are using ChatGPT to be productive.</p>
<p><strong>Coding</strong></p>
<p>ChatGPT is being used as a substitute for Stack Overflow in some cases. It can understand the context of the question and provide helpful responses. For example, someone can ask a question about a problem with Pandas, and ChatGPT can recognize the topic and provide relevant answers. It can also help with creative tasks such as coming up with ideas for lesson plans or overcoming writer’s block. ChatGPT has lowered the emotional-resistance barrier for doing creative tasks and improved the quality of output by providing creative ideas.</p>
<p><strong>Reviewing Legal Documents</strong></p>
<p>ChatGPT has also been used to review contracts and explain hard to parse legalese. It can quickly provide an overview of a given topic and guide people on where to delve deeper.</p>
<p><strong>Accounting and Tax Advice</strong></p>
<p>For billing international clients, ChatGPT has been helpful in providing accounting and tax advice. It has also been used to assist with visa applications.</p>
<p><strong>Rapid Prototyping</strong></p>
<p>ChatGPT has been used as a rapid prototyping tool for building prototypes for public APIs. Someone can ask for endpoints, find the endpoint they want, and then ask ChatGPT to code a request to the endpoint in a specific language. This saves time and reduces the setup required.</p>
<p><strong>Thesaurus</strong></p>
<p>ChatGPT has been used as a thesaurus, providing options for words that mean a certain thing. It has also been used to brainstorm ideas, generate OpenAPI schemas, and explain code that is not understood.</p>
<p><strong>Proofreading</strong></p>
<p>ChatGPT can also be used to proofread emails and make suggestions based on the stated goal. It can also settle language/choice of words discussions by asking ChatGPT to reverse pitch understanding and then choosing the one that’s most aligned with the point being made.</p>
<p><strong>Linux Commands</strong></p>
<p>For Linux-y commands and explanations, ChatGPT has been helpful in finding the best way to remap keys in i3 or finding a file with specific content faster than using the ‘find’ command.</p>
<p>Overall, ChatGPT has become a second brain for many people, providing quick and accurate responses to a wide range of tasks. Its ability to understand natural language has made it a valuable tool for professionals in various fields, from coding and legal documents to accounting and tax advice. ChatGPT has drastically reduced the time and effort required to complete many tasks and has improved the quality of output. Its benefits are undeniable, and it is likely that more people will continue to adopt it as an essential tool in their work.</p>
How to add Node.js & npm to jekyll in GitHub Codespaces2023-03-25T00:00:00Zhttps://cri.dev/posts/2023-03-25-jekyll-github-codespaces-node-js-npm/<p>You can use GitHub CodeSpaces to develop your Jekyll site. But you need to install Node.js and npm to use e.g. TailwindCSS.</p>
<p>So, if you haven’t already, create a file called <code>devcontainer/devcontainer.json</code> in your repository, with the following content:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Jekyll"</span><span class="token punctuation">,</span>
<span class="token property">"image"</span><span class="token operator">:</span> <span class="token string">"mcr.microsoft.com/devcontainers/jekyll:0-bullseye"</span><span class="token punctuation">,</span>
<span class="token property">"features"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"ghcr.io/devcontainers/features/node:1"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"latest"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>This will start from the image “<a href="http://mcr.microsoft.com/devcontainers/jekyll:0-bullseye">mcr.microsoft.com/devcontainers/jekyll:0-bullseye</a>” and also add Node.js and npm.</p>
<p>Now, when you open your repository in GitHub Codespaces, you can use Node.js and npm.</p>
<p>For more information about the “features” property, see <a href="https://containers.dev/features">devcontainer.json reference</a>.</p>
Self-hosting miniflux with docker and provisioning with Ansible2023-03-25T00:00:00Zhttps://cri.dev/posts/2023-03-25-ansible-miniflux-nginx-provisioning-self-hosting/<p>I have been using miniflux for a while now, and I really like it. It’s a great RSS reader.</p>
<p>Here I want to share my experience with self-hosting it with docker and nginx.</p>
<p>The provisioning is done with Ansible.</p>
<p>Pretty simple, and I think it’s a good starting point for anyone who wants to self-host miniflux.</p>
<h2 id="tldr%3B" tabindex="-1">TLDR;</h2>
<ul>
<li>Provisioning with Ansible</li>
<li>Running miniflux with docker</li>
<li>Reverse proxy with nginx</li>
</ul>
<h2 id="provisioning-with-ansible" tabindex="-1">Provisioning with Ansible</h2>
<p>The basic idea is to spin up the docker container with miniflux, providing all the necessary env variables to configure it correctly.</p>
<p>Then you <em>can</em> configure nginx to reverse proxy the miniflux container.</p>
<p>E.g. you could host it on <code>https://rss.example.com</code> and then let nginx route the traffic to the container running miniflux.</p>
<h2 id="running-miniflux-with-docker" tabindex="-1">Running miniflux with docker</h2>
<p>You’ll need to install the <code>community.docker</code> ansible galaxy plugin:</p>
<pre class="language-bash"><code class="language-bash">ansible-galaxy collection <span class="token function">install</span> community.docker</code></pre>
<p>The docker compose file (translated in an Ansible task) looks like this, and is located in <code>roles/miniflux/tasks/main.yml</code>:</p>
<pre class="language-yaml"><code class="language-yaml">
<span class="token punctuation">-</span> <span class="token key atrule">community.docker.docker_compose</span><span class="token punctuation">:</span>
<span class="token key atrule">project_name</span><span class="token punctuation">:</span> miniflux
<span class="token key atrule">definition</span><span class="token punctuation">:</span>
<span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">'2'</span>
<span class="token key atrule">services</span><span class="token punctuation">:</span>
<span class="token key atrule">db</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> postgres
<span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token key atrule">POSTGRES_PASSWORD</span><span class="token punctuation">:</span> <span class="token string">"{{ miniflux_password }}"</span>
<span class="token key atrule">POSTGRES_USER</span><span class="token punctuation">:</span> <span class="token string">"{{ miniflux_username }}"</span>
<span class="token key atrule">volumes</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> miniflux<span class="token punctuation">-</span>db<span class="token punctuation">:</span>/var/lib/postgresql/data
<span class="token key atrule">miniflux</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> miniflux/miniflux
<span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token key atrule">DATABASE_URL</span><span class="token punctuation">:</span> <span class="token string">"postgres://{{ miniflux_username }}:{{ miniflux_password }}@db/miniflux?sslmode=disable"</span>
<span class="token key atrule">RUN_MIGRATIONS</span><span class="token punctuation">:</span> <span class="token number">1</span>
<span class="token key atrule">CREATE_ADMIN</span><span class="token punctuation">:</span> <span class="token number">1</span>
<span class="token key atrule">ADMIN_USERNAME</span><span class="token punctuation">:</span> <span class="token string">"{{ miniflux_admin_username }}"</span>
<span class="token key atrule">ADMIN_PASSWORD</span><span class="token punctuation">:</span> <span class="token string">"{{ miniflux_admin_password }}"</span>
<span class="token key atrule">POLLING_FREQUENCY</span><span class="token punctuation">:</span> <span class="token number">10</span>
<span class="token key atrule">BASE_URL</span><span class="token punctuation">:</span> <span class="token string">"https://{{ miniflux_domain }}"</span>
<span class="token key atrule">HTTPS</span><span class="token punctuation">:</span> on"
<span class="token key atrule">ports</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token string">"127.0.0.1:3000:8080"</span>
<span class="token key atrule">depends_on</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> db
<span class="token key atrule">volumes</span><span class="token punctuation">:</span>
<span class="token key atrule">miniflux-db</span><span class="token punctuation">:</span>
<span class="token key atrule">driver</span><span class="token punctuation">:</span> local
<span class="token key atrule">register</span><span class="token punctuation">:</span> output
<span class="token punctuation">-</span> <span class="token key atrule">ansible.builtin.assert</span><span class="token punctuation">:</span>
<span class="token key atrule">that</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token string">"output.services.miniflux.miniflux_miniflux_1.state.running"</span>
<span class="token punctuation">-</span> <span class="token string">"output.services.db.miniflux_db_1.state.running"</span>
</code></pre>
<p>As you can see, we’re running a postres container alongside miniflux, exposing the miniflux container port <code>8080</code> to the host port <code>3000</code>. This will later be used by nginx to reverse proxy the traffic.</p>
<p>We’re also setting various environment variables to configure miniflux, and then asserting the services are running.</p>
<p>This means you’ll also need to configure a file containing the variables / secrets.</p>
<p>You could do this in a <code>group_vars</code> file, or in a <code>secrets</code> file.</p>
<p>I’ll use <code>ansible-vault</code> to decrypt when editing and then encrypt the secrets file, like this</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment">#create a new secrets file</span>
<span class="token function">touch</span> secrets
<span class="token comment">#encrypt and decrypt it providing a vault password</span>
ansible-vault encrypt secrets
ansible-vault decrypt secrets</code></pre>
<p>The secrets file looks like this:</p>
<pre><code>miniflux_username: miniflux
miniflux_password: <your password>
miniflux_admin_username: <your username>
miniflux_admin_password: <your password>
miniflux_domain: <your domain>
</code></pre>
<h2 id="reverse-proxy-with-nginx" tabindex="-1">Reverse proxy with nginx</h2>
<p>The nginx configuration looks like this, put it in <code>roles/miniflux/templates/miniflux.conf.j2</code>:</p>
<pre class="language-nginx"><code class="language-nginx">
<span class="token directive"><span class="token keyword">upstream</span> miniflux</span> <span class="token punctuation">{</span>
<span class="token directive"><span class="token keyword">server</span> 127.0.0.1:3000 max_fails=5 fail_timeout=60s</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span>
<span class="token directive"><span class="token keyword">server_name</span></span> <span class="token punctuation">{</span><span class="token punctuation">{</span> miniflux_domain <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">listen</span> <span class="token number">80</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">listen</span> [::]:80</span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">gzip_vary</span> <span class="token boolean">on</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">gzip_proxied</span> any</span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">gzip_comp_level</span> <span class="token number">6</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">gzip_buffers</span> <span class="token number">16</span> <span class="token number">8k</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">gzip_http_version</span> 1.1</span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">gzip_types</span> text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml</span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">client_max_body_size</span> <span class="token number">16m</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">ignore_invalid_headers</span> <span class="token boolean">off</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">proxy_http_version</span> 1.1</span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">proxy_set_header</span> Upgrade <span class="token variable">$http_upgrade</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">proxy_set_header</span> Connection <span class="token string">"upgrade"</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">proxy_set_header</span> Host <span class="token variable">$http_host</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">proxy_set_header</span> X-Forwarded-For <span class="token variable">$proxy_add_x_forwarded_for</span></span><span class="token punctuation">;</span>
<span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span>
<span class="token directive"><span class="token keyword">proxy_pass</span> http://miniflux</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>In the playbook, we copy the template to the nginx sites-available directory, and then symlink it to the sites-enabled directory.</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> install nginx
<span class="token key atrule">become</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">apt</span><span class="token punctuation">:</span>
<span class="token key atrule">name</span><span class="token punctuation">:</span> nginx
<span class="token key atrule">state</span><span class="token punctuation">:</span> present
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> rm /etc/nginx/sites<span class="token punctuation">-</span>enabled/default
<span class="token key atrule">file</span><span class="token punctuation">:</span>
<span class="token key atrule">path</span><span class="token punctuation">:</span> /etc/nginx/sites<span class="token punctuation">-</span>enabled/default
<span class="token key atrule">state</span><span class="token punctuation">:</span> absent
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> setup nginx vhost
<span class="token key atrule">template</span><span class="token punctuation">:</span>
src=miniflux.conf.j2
dest=/etc/nginx/sites<span class="token punctuation">-</span>available/miniflux.conf
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> symlink nginx vhost
<span class="token key atrule">file</span><span class="token punctuation">:</span>
src=/etc/nginx/sites<span class="token punctuation">-</span>available/miniflux.conf
dest=/etc/nginx/sites<span class="token punctuation">-</span>enabled/miniflux.conf
state=link
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> reload nginx
<span class="token key atrule">service</span><span class="token punctuation">:</span>
name=nginx
state=reloaded</code></pre>
<p>PS: you could use handlers here, instead of reloading the service directly, I took it out for simplicity.</p>
<h2 id="the-ansible-project" tabindex="-1">The Ansible project</h2>
<p>Before running the playbook you need to configure the hosts file.</p>
<p>Create a new file called <code>hosts</code> and add the following:</p>
<pre><code><your server name> ansible_host=<your ip> ansible_user=<user> ansible_connection=ssh ansible_ssh_private_key_file=<ssh key> ansible_ssh_port=22
</code></pre>
<p>Also, add a <code>requirements.yml</code> file to install some dependencies with <code>ansible-galaxy install -r requirements.yml</code>:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">---</span>
<span class="token key atrule">roles</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> geerlingguy.pip
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> geerlingguy.docker</code></pre>
<p>Your project structure should look like this:</p>
<pre><code>.
├── hosts
├── playbook.yml
├── requirements.yml
├── roles
│ ├── app-miniflux
│ │ ├── tasks
│ │ │ └── main.yml
│ │ └── templates
│ │ └── miniflux.conf.j2
└── secrets
</code></pre>
<h3 id="running-the-playbook" tabindex="-1">Running the playbook</h3>
<p>Now you can run the playbook:</p>
<pre class="language-bash"><code class="language-bash">ansible-playbook <span class="token parameter variable">-i</span> hosts miniflux.yml --ask-vault-pass</code></pre>
<p>You’ll be prompted for the vault password you set when encrypting the secrets file.</p>
Programming on an iPad2024-02-18T00:00:00Zhttps://cri.dev/posts/2023-03-22-ipad-programming-github-codespaces-raspberry-pi-vscode/<p>In this blog post I wanted to share my experience of programming on an iPad Pro (no PC at the moment).</p>
<p>With the right software tools (and optional hardware accessories), an iPad can serve as a powerful mobile development environment.</p>
<p>I’ll get into using</p>
<ul>
<li>Visual Studio Code on the web</li>
<li>mosh, ssh and bashy stuff with Blink Shell</li>
<li>GitHub Codespaces for casual pull-requests and development</li>
<li>a Raspberry Pi
<ul>
<li>ansible provisioning of servers</li>
<li>self-hosting apps on local network</li>
<li>vscode server/tunnels for local development</li>
</ul>
</li>
<li>and much more! (even <a href="https://cri.dev/posts/2023-03-17-3d-printing-ipad-raspberry-octoprint-guide-tutorial/">3d-printing</a>)</li>
</ul>
<p><img src="https://cri.dev/assets/images/posts/ipad-programming/vscode.jpeg" alt="ipad programming vscode.jpeg" loading="lazy" /></p>
<h2 id="what-do-you-need%3F" tabindex="-1">What do you need?</h2>
<p>Occasional programming on the go, or a full-fledged development environment?</p>
<h3 id="for-occasional-commits" tabindex="-1">For occasional commits</h3>
<p>Go with Github Codespaces, it’s free and you can use it on the go.</p>
<p>Visit <a href="https://vscode.dev/">vscode.dev</a> and sign in with your GitHub account.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-programming/codespaces.jpeg" alt="ipad programming codespaces.jpeg" loading="lazy" /></p>
<p>Create a new Codespace starting with one of your repositories on GitHub, you can customize the environment by defining a <code>devcontainer.json</code> (more info <a href="https://code.visualstudio.com/docs/devcontainers/create-dev-container">here</a>) file in the root of your repository.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-programming/codespaces-quota.jpeg" alt="ipad programming codespaces-quota.jpeg" loading="lazy" /></p>
<p>Eventually set a spending limit, and you are good to go.</p>
<p>For ease of use you can bookmark the URL of your Codespace, so you can access it quickly, also from your homescreen.</p>
<p><strong>update: haha not so fast, this isn’t a thing anymore apparently, since <a href="https://mashable.com/article/apple-kills-home-screen-web-apps-pwas-in-eu-dma">Apple didn’t like DMA so much</a></strong></p>
<h3 id="full-fledged-development-environment" tabindex="-1">Full-fledged development environment</h3>
<p>You could also try out Codesandbox, but I haven’t tried it yet.</p>
<p>You’ll need a Raspberry Pi, or a VPS somewhere.</p>
<p>You can configure a VSCode server instance and connect to it from your iPad, using <a href="https://code.visualstudio.com/docs/remote/tunnels">Remote Tunnels</a>.</p>
<p>Here you can find <a href="https://code.visualstudio.com/docs/remote/tunnels">the official guide</a> to get started.</p>
<p>It takes 5 minutes.</p>
<p>Once you have a VSCode server instance running, you can connect to it from your iPad using Blink Shell or directly from vscode.dev on the web.</p>
<p>From the Blink Shell app, just type <code>code</code> and you’ll be presented with a VSCode instance.</p>
<p>Find the <code>Remote Explorer</code> extension in the sidebar, and select the <code>Remote</code> option.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-programming/remotes.jpeg" alt="ipad programming remotes.jpeg" loading="lazy" /></p>
<p>Connect to your workstation, you also have SSH access to the underlying system directly from the terminal in VSCode!</p>
<h2 id="what-about-the-raspberry-pi%3F" tabindex="-1">What about the Raspberry Pi?</h2>
<p>A really nice thing about the Raspberry Pi is that it’s a very versatile ARM computer, compact and with a lot of potential.</p>
<p>So how do I use it?</p>
<p>Some examples</p>
<h3 id="old-printer-on-airprint-using-cups" tabindex="-1">Old printer on AirPrint using CUPS</h3>
<p>I recently used it to hook up a really old printer on the Raspberry Pi, using CUPS and a USB-to-Serial adapter.</p>
<p>Then I could send documents to print using AirPrint from my iPad.</p>
<h3 id="arduino-and-esp32" tabindex="-1">Arduino and ESP32</h3>
<p>By connecting to the Pi via VNC, I can program my Arduino and ESP devices from the iPad, by viewing and interacting with the Pi desktop environment.</p>
<h3 id="3d-printing" tabindex="-1">3D printing</h3>
<p>I design the parts on the iPad, and then I can slice my models using Ultimaker Cura and 3D print them using OctoPrint running on the Pi.</p>
<h3 id="ansible-hetzner-server-provisioning" tabindex="-1">Ansible Hetzner server provisioning</h3>
<p>Provisioning of my Hetzner server using Ansible, using VSCode tunnels to my Pi.</p>
<h3 id="self-hosting-apps-on-local-network" tabindex="-1">Self-hosting apps on local network</h3>
<p>Self-hosting apps on my local network, like a Nextcloud, Syncthing or a GitLab instance.</p>
<h3 id="vscode-server%2Ftunnels-for-local-development" tabindex="-1">VSCode server/tunnels for local development</h3>
<p>I can also use the Pi to run a VSCode server instance, and connect to it from my iPad using Remote Tunnels.</p>
Minimal Ansible Playbook to provision a server2023-03-21T00:00:00Zhttps://cri.dev/posts/2023-03-21-minimal-ansible-playbook-provision-server/<p>In this blog post, we’ll create a minimal Ansible playbook that will allow you to quickly and easily provision a new server.</p>
<p>With just a few simple steps, you’ll be able to set up a server with all the necessary software and configurations in no time.</p>
<h2 id="install-ansible" tabindex="-1">Install Ansible</h2>
<p>You can do this by following the instructions on the <a href="https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html">Ansible installation page</a>.</p>
<p>But generally a quick <code>python3 -m pip install --user ansible</code> should do the trick.</p>
<h2 id="create-an-inventory-file" tabindex="-1">Create an inventory file</h2>
<p>This is a simple text file that contains the IP addresses of the servers that you want to provision, plus some connection settings.</p>
<p>To create a new inventory file, create a new file called <code>production</code> and add the IP address of your server to it:</p>
<pre class="language-bash"><code class="language-bash">server1 <span class="token assign-left variable">ansible_host</span><span class="token operator">=</span><span class="token operator"><</span>ENTER_YOUR_SERVER_IP_ADDRESS<span class="token operator">></span> <span class="token assign-left variable">ansible_user</span><span class="token operator">=</span>root <span class="token assign-left variable">ansible_connection</span><span class="token operator">=</span>ssh <span class="token assign-left variable">ansible_ssh_private_key_file</span><span class="token operator">=</span><span class="token operator"><</span>PATH_TO_YOUR_SSH_PRIVATE_KEY<span class="token operator">></span> <span class="token assign-left variable">ansible_ssh_port</span><span class="token operator">=</span><span class="token number">22</span></code></pre>
<p>In this file we defined a server creatively named <code>server1</code> and set some connection settings for it, like the user, ssh private key path etc.</p>
<h2 id="create-a-new-ansible-playbook" tabindex="-1">Create a new Ansible playbook</h2>
<p>Next, we’ll create a new Ansible playbook. This is a simple YAML file that contains all the instructions for provisioning the server.</p>
<p>To create a new playbook, create a new file called <code>playbook.yml</code> and add the following content:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">---</span>
<span class="token punctuation">-</span> <span class="token key atrule">hosts</span><span class="token punctuation">:</span> all
<span class="token key atrule">become</span><span class="token punctuation">:</span> yes
<span class="token key atrule">tasks</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> install nginx
<span class="token key atrule">apt</span><span class="token punctuation">:</span>
<span class="token key atrule">name</span><span class="token punctuation">:</span> nginx
<span class="token key atrule">state</span><span class="token punctuation">:</span> present</code></pre>
<h2 id="run-the-playbook" tabindex="-1">Run the playbook</h2>
<p>Now that we have our playbook, we can run it to provision our server.</p>
<p>To run the playbook, run the following command:</p>
<pre class="language-bash"><code class="language-bash">ansible-playbook <span class="token parameter variable">-i</span> production playbook.yml</code></pre>
<hr />
<p>And that’s it! You now know how to create a minimal Ansible playbook to provision a server.</p>