Christian Fei's Blog - cri.devA blog about Programming, DIY and personal ramblings2024-02-13T00:00:00Zhttps://cri.devChristian FeiGoogle Sheets got fixed on iPad2024-02-13T00:00:00Zhttps://cri.dev/posts/2024-02-13-google-sheets-ipad-working/<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" /></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' 💩2023-11-04T00:00:00Zhttps://cri.dev/posts/2023-11-04-google-sheets-ipad-slow/<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" /></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>
Apple finally added iPad audio control 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>
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" /></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" /></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" /></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" /></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" /></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" /></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" /></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>
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 it2023-09-03T00: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>
Scrum, the hated one2023-08-30T00:00:00Zhttps://cri.dev/posts/2023-08-30-Scrum-the-hated-one/<p>In today’s fast-paced software development industry, Scrum has emerged as a popular framework for agile project management. However, opinions about Scrum vary greatly, with some praising its effectiveness while others criticizing its limitations. In this blog post, we will delve into these diverse perspectives to extract the most valuable insights about Scrum.</p>
<h1 id="an-agile-duplo-block" tabindex="-1">An agile duplo block</h1>
<p>Scrum is often described as the “duplo block of agile,” highlighting its simplicity and adaptability. It doesn’t have to be heavy, and it can be tailored to fit the specific needs of each team. One of the key takeaways here is that Scrum ceremonies, such as retrospectives, sprint reviews, and standups, can be done in a lightweight manner, reducing the meeting time to a half-day block every other week and 15 minutes a day for standups.</p>
<h1 id="focus-on-collaboration-and-prioritization" tabindex="-1">Focus on collaboration and prioritization</h1>
<p>The core idea of Scrum revolves around collaboration and prioritization to ensure efficient project delivery. Daily standup meetings, weekly plannings, and retrospectives play a crucial role in keeping everyone on the same page, adapting to real-world changes, and addressing potential bottlenecks. By constantly adjusting and refining the process, teams can build the right thing and ensure continuous improvement.</p>
<h1 id="implementation%3A-a-double-edged-sword" tabindex="-1">Implementation: a double-edged sword</h1>
<p>While Scrum presents a sound framework when implemented sensibly, it can also be susceptible to misuse and arbitrary estimates. Some teams may add unnecessary add-ons or become too rigid, losing sight of the true purpose. This is why it is crucial to empower teams with good leadership and principles, ensuring they have the freedom to adapt and make the most of the Scrum framework.</p>
<h1 id="scrum-vs.-kanban%3A-work-priority-and-flexibility" tabindex="-1">Scrum vs. Kanban: Work Priority and Flexibility</h1>
<p>Scrum and Kanban differ in terms of work priority, with Scrum relying on forecasting based on past velocity and Kanban focusing on taking the next item off the top of the backlog. However, both approaches share the need for flexible adaptation to accommodate changes in priorities. It is essential to find a balance between strict adherence to process and the ability to respond effectively to evolving requirements.</p>
<h1 id="scrum%E2%80%99s-value-lies-in-collaboration%2C-not-just-ceremonies" tabindex="-1">Scrum’s Value Lies in Collaboration, Not Just Ceremonies</h1>
<p>The true value of Scrum lies beyond its ceremonies and processes. It lies in fostering collaboration, communication, and continuous improvement. The daily standups, demos, and retrospectives serve as enablers for valuable collaboration, provided the engagement is meaningful and effective. Merely going through the motions of Scrum without genuine collaboration may not yield the desired results.</p>
<h1 id="scrum%E2%80%99s-achilles%E2%80%99-heel%3A-political-hijacking" tabindex="-1">Scrum’s Achilles’ Heel: Political Hijacking</h1>
<p>One recurring concern with Scrum is its vulnerability to political influences and misuse. When Scrum is hijacked or used for ulterior motives, it loses its effectiveness and becomes a liability. To achieve success with Scrum, it is crucial to have the right people involved, maintain a balance between formality and flexibility, and focus on the core value of getting things done efficiently.</p>
<h1 id="scrum%E2%80%99s-applicability%3A-not-one-size-fits-all" tabindex="-1">Scrum’s Applicability: Not One-Size-Fits-All</h1>
<p>Scrum’s suitability depends on various factors, including project size, complexity, organizational culture, flexibility needs, team size, and project stability. It may not be the best fit for every situation, and alternative approaches, such as Kanban, should be considered. Understanding the unique needs of your project and organization is key to choosing the right framework.</p>
<h1 id="conclusion" tabindex="-1">Conclusion</h1>
<p>Scrum, as a project management framework, has its strengths and weaknesses. Success with Scrum lies in striking a balance between formality and flexibility, fostering collaboration, and focusing on delivering results. By understanding the core principles and adapting them to specific contexts, teams can harness the true value of Scrum and achieve better outcomes in their software development projects.</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" /></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" /></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" /></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>
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>
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>
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>
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" /></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" /></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" /></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" /></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>
3D printing with an iPad, Raspberry Pi and OctoPrint2023-03-17T00:00:00Zhttps://cri.dev/posts/2023-03-17-3d-printing-ipad-raspberry-octoprint-guide-tutorial/<p>In this article, we will explore the different ways to use an iPad for 3D printing, from designing to slicing and printing.</p>
<p>What you’ll need:</p>
<ul>
<li>a Raspberry Pi (yes, calm down and stay with me)</li>
<li>basic design experience with tools like Fusion360, or alternatives like Tinkercad, OnShape and Shapr3d</li>
<li>basic setup for slicing the STL in Ultimaker Cura</li>
</ul>
<p>This is definitely not the best setup, but the best one that suits my needs.</p>
<h2 id="designing-the-model" tabindex="-1">Designing the model</h2>
<p>After a rough initial idea in your mind and perhaps on paper, designing the model is the next step in the 3D printing process.<br />
To do that on an iPad you have various options: I am personally trying out Shapr3d, OnShape and Tinkercad to find what like most.<br />
I’ll leave it up to you to decide which one you prefer.</p>
<p>You know the drill: chamfer, extrude and subtract your way to your own STL</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-raspberry-3d/designing-shapr3d.png" alt="designing shapr3d" /></p>
<h2 id="slicing-the-model" tabindex="-1">Slicing the model</h2>
<p>To do that I’m using my Raspberry Pi.<br />
Namely by connecting to it via VNC and using Ultimaker Cura.<br />
Check out <a href="https://raspberrytips.com/ipad-as-raspberry-pi-monitor/">this link</a> to set it up on your Pi.</p>
<p>Is it awesome? Oh hell no, it’s a nightmare at first, but after a few minutes you’ll have your G-code ready for printing.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-raspberry-3d/slicing-vnc.jpeg" alt="slicing vnc" /></p>
<h2 id="printing-the-model" tabindex="-1">Printing the model</h2>
<p>Here you have two options:</p>
<ul>
<li>using OctoPrint to control your 3d printer connected to the Raspberry Pi</li>
<li>export the G-code to an USB stick</li>
</ul>
<p>Depending on what you prefer, you can finally print your model with the help of a Raspberry Pi and the iPad.</p>
<p>If you want to try out OctoPrint, you’ll need to install OctoPrint manually on your Raspberry Pi, without the help of the OctoPi image.</p>
<p>It’s quite simple though: just follow the instructions on the <a href="https://octoprint.org/download/#installing-manually">OctoPrint website</a> and you’ll be good to go.</p>
<h2 id="personal-setup" tabindex="-1">Personal Setup</h2>
<p>Here a quick overview of my iPad and Raspberry Pi setup:</p>
<div class="md:flex">
<div class="md:flex-1"><a class="no border-b-0 hover:border-none" href="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-1.jpeg"><img src="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-1.jpeg" alt="ipad-pi-view-1" /></a></div>
<div class="md:flex-1"><a class="no border-b-0 hover:border-none" href="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-3.jpeg"><img src="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-3.jpeg" alt="ipad-pi-view-3" /></a></div>
</div>
<div class="md:flex">
<div class="md:flex-1"><a class="no border-b-0 hover:border-none" href="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-2.jpeg"><img src="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-2.jpeg" alt="ipad-pi-view-2" /></a></div>
<div class="md:flex-1"><a class="no border-b-0 hover:border-none" href="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-4.jpeg"><img src="https://cri.dev/assets/images/posts/ipad-raspberry-3d/ipad-pi-view-4.jpeg" alt="ipad-pi-view-4" /></a></div>
</div>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>This is definitely not the best setup, but the best one I found to get the job done.</p>
<p>I hope this article was helpful and you can now use your iPad and Raspberry Pi to 3D print your models.</p>
<h2 id="references" tabindex="-1">References</h2>
<ul>
<li><a href="https://octoprint.org/">OctoPrint</a></li>
<li><a href="https://shapr3d.com/">Shapr3d</a></li>
<li><a href="https://www.onshape.com/">OnShape</a></li>
<li><a href="https://www.tinkercad.com/">Tinkercad</a></li>
<li><a href="https://ultimaker.com/software/ultimaker-cura">Ultimaker Cura</a></li>
<li><a href="https://www.raspberrypi.org/">Raspberry Pi</a></li>
<li><a href="https://www.realvnc.com/en/connect/download/viewer/">VNC</a></li>
</ul>
Update Node.js version on CloudFlare Pages2023-03-12T00:00:00Zhttps://cri.dev/posts/2023-03-12-update-node-npm-js-version-cloudflare-pages/<p>Recently I upgraded to 11ty 2.0.</p>
<p>Noticed a failing build on CloudFlare Pages, and found out I was using the default Node.js version, namely v14! 😅</p>
<p>So I updated the <code>package.json</code> to use the latest LTS version of Node.js available on CloudFlare (node 17):</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"engines"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"node"</span><span class="token operator">:</span> <span class="token string">">= 16"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>and in the CloudFlare Pages dashboard, I updated the Node.js version to 16 in the environment variables tab.</p>
<p>Alternatively, with a file called <code>.node-version</code> with the following content:</p>
<pre><code>16.16
</code></pre>
<p>Here you can find more information about the Node.js version on CloudFlare Pages.</p>
<p><a href="https://developers.cloudflare.com/pages/platform/build-configuration/#language-support-and-tools">https://developers.cloudflare.com/pages/platform/build-configuration/#language-support-and-tools</a></p>
yubikey spare keys2023-02-22T00:00:00Zhttps://cri.dev/posts/2023-02-22-yubikey-spare-keys/<blockquote>
<p>Think of your YubiKey as you would any other key</p>
</blockquote>
<p>So having a backup key is the way to go</p>
<p><a href="https://www.yubico.com/spare/">https://www.yubico.com/spare/</a></p>
malicious chrome extension2023-02-22T00:00:00Zhttps://cri.dev/posts/2023-02-22-malicious-chrome-extension/<p>In <a href="https://mattfrisbie.substack.com/p/spy-chrome-extension">this excellent article</a> you can find the repository that contains probably the most malicious chrome extension ever created.</p>
<p>It’s <a href="https://github.com/msfrisbie/spy-extension">spy-extension</a>, and if you’re interested in the capabilities, security, permissions and various APIs, that seems like a fun place to start.</p>
'Being agile' vs 'Doing Agile'2023-02-21T00:00:00Zhttps://cri.dev/posts/2023-02-21-being-agile-vs-doing-agile/<p>Remember:</p>
<p>Individuals and interactions over processes and tools<br />
Working software over comprehensive documentation<br />
Customer collaboration over contract negotiation<br />
Responding to change over following a plan</p>
<p>———</p>
<p><em>Beware: random rant ahead</em></p>
<p>Agile is about planning, but also and especially about continously verifying what changed and how to adapt.</p>
<p>When you “are agile” (not “doing Agile”) you try to do as little up-front planning as possible, knowing things will change, with the latest most accurate insights, just in time.</p>
<p>You will need to regularly adjust your direction and check if you and your team are on the right track.</p>
<p>Your team is ideally small, and it’s empowered, trusted and responsible for the outcomes and works on the problem with courage.</p>
<p>You also want lots of feedback for adjusting accordingly.</p>
<p>And for that you want also a reliable CICD release cycle, with early and frequent releases that just work.</p>
<p>Having a smooth, sustainable and well-organized release cycle and feedback loop is vital I think.</p>
<p>Ideally the team works together to find their best process for solving the problem at hand.</p>
<p>I think a process shouldn’t be handed from the top down, the people working as a team should be the ones deciding on the working process, evaluating and improving it.</p>
<p>Just hire good people and let them work how they feel works best for them.</p>
<p>Yes, “Agile” often comes with a lot of hot air. That’s where “Doing Agile” starts to sound like a religion and you start to see a lot of dogma and bullshit.</p>
<p>Just don’t “Do Agile”.</p>
<p>“Be agile” instead.</p>
<p>Just solve the problem at hand, with whatever feels right and doesn’t slow you down, ok?</p>
<p>That’s about it.</p>
How I edit this site on an iPad2023-02-21T00:00:00Zhttps://cri.dev/posts/2023-02-21-github-codespaces-ipad-programming-development-vscode/<h2 id="tldr" tabindex="-1">TLDR</h2>
<p>This blog post was written on an iPad using GitHub Codespaces and Visual Studio Code.</p>
<p>Also the pictures were taken with the iPad and uploaded to GitHub Codespaces.</p>
<ul>
<li>Open <a href="http://github.com/">github.com</a></li>
<li>Use GitHub Codespaces</li>
<li>Spin up dev-server, edit, commit and push with Visual Studio Code</li>
<li>(Profit)</li>
</ul>
<p>Is it the perfect iPad development experience?</p>
<p>Absolutely not, But it’s definitely a good start!</p>
<p>—</p>
<h2 id="setting-up-codespaces" tabindex="-1">Setting up Codespaces</h2>
<p>Codespaces can be configured using “Development Containers”, which resemble a full-featured development environment through Docker containers (see containers.dev for more info)</p>
<p>Pretty neat.</p>
<p>To get started, I created the following .devcontainer.json in my blog repository:</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">"Node.js"</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/javascript-node:0-18-bullseye"</span>
<span class="token punctuation">}</span></code></pre>
<h2 id="accessing-the-dev-container" tabindex="-1">Accessing the dev container</h2>
<p>I go to my blog repo on GitHub and open the previously created Codespace.</p>
<p>I will have the latest changes that I left unstaged, with the previously opened file tabs too.</p>
<p>If you linked your Visual Studio Code with your GitHub account, you’ll also get Settings sync and the extensions are already installed and available as on your PC.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-github-codespaces/vscode.jpeg" alt="" /></p>
<h2 id="spinning-up-the-dev-server" tabindex="-1">Spinning up the dev server</h2>
<p>I just open a terminal, run <code>npm start</code> and once the server is up and running, a popup with the following shows up.</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-github-codespaces/port-forwarding.jpeg" alt="port fowarding codespace" /></p>
<p>I can just click on the link and it will open in a new tab.</p>
<h2 id="caveats" tabindex="-1">Caveats</h2>
<ul>
<li>Don’t ever use CMD+W to close a VSCode tab, else it will close your browser tab! You’ll need to reload the Codespace too…</li>
<li>Scrolling doesn’t work suuper well in VSCode online I find, e.g. scrolling up and down in the code or the file tree is not working with my trackpad every time, it’s very finicky.</li>
<li>The ergonomics and overall experience of a PC are unbeatable, but it’s more than fine for small changes and quick fixes on the go</li>
<li>It’s useful to remap the WORLD key to Escape, e.g. when casually usually vim etc</li>
</ul>
<p>Depending on your internet quality, the experience can be a bit laggy, but it’s still pretty usable.</p>
<p>Although one day the connection was quite unstable and I had to reload the codespace multiple times:</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-github-codespaces/error-reload.jpeg" alt="error reload codespace" /></p>
<h2 id="so%3F-is-it-worth-it%3F" tabindex="-1">So? Is it worth it?</h2>
<p>Is it the perfect iPad development experience?</p>
<p>Absolutely not, But it’s definitely a good start!</p>
<p>I’m looking forward to see what GitHub/Microsoft will come up with in the future to suit my needs even better.</p>
<p>There is also CodeSandbox that looks very promising, I haven’t played around with it a whole lot.</p>
<p>PS: you can also set a spending limit to avoid unexpected bills</p>
<p><img src="https://cri.dev/assets/images/posts/ipad-github-codespaces/quota.jpeg" alt="quota" /></p>
I cannot wait for the passwordless authentication future of the web2023-02-25T00:00:00Zhttps://cri.dev/posts/2023-02-10-passwordless-auth-webauthn-biometric-security-key/<p>Passwords, usernames, breaches, accounts are a mess to maintain and especially to keep secure and to remember.</p>
<p>I cannot wait for the passwordless authentication future of the web, and I hope it will be adopted widely and soon.</p>
<p>I spend roughly an hour every six months to a year managing my almost 200 accounts saved in my password manager. I cancel accounts I no longer need and update the passwords of others when I feel like it.</p>
<p>WebAuthn is be a great step forward in this wild jungle of passwords / sensitive information and related breaches.</p>
<p>A standard API that websites can adopt to interact with authentication devices, such as security keys or biometric sensors.</p>
<p>This eliminates the need for users to enter their password on the website, and especially remember, managing and keeping them secure.</p>
<p>Encryption is at the heart of this biometric authentication standard, and the user is authenticated directly by the authentication device.</p>
<p>The website is only given a cryptographic proof that the user is who they claim to be.</p>
<p><img src="https://cri.dev/assets/images/posts/webauthn-options.jpeg" alt="WebAuthn demo" /></p>
<p>Below you can find some links with helpful resources, I highly suggest to check out the demo on <a href="https://webauthn.io/">https://webauthn.io</a></p>
<p>Awesome to see the different biometric and security key options that are available.</p>
<ul>
<li>
<p><a href="https://webauthn.io/">https://webauthn.io</a></p>
</li>
<li>
<p><a href="https://webauthn.guide/">https://webauthn.guide</a></p>
</li>
<li>
<p><a href="https://github.com/duo-labs/webauthn.io">https://github.com/duo-labs/webauthn.io</a></p>
</li>
<li>
<p><a href="https://duo.com/solutions/passwordless/the-life-and-death-of-passwords">https://duo.com/solutions/passwordless/the-life-and-death-of-passwords</a></p>
</li>
<li>
<p><a href="https://bitwarden.com/blog/living-the-passwordless-life/">https://bitwarden.com/blog/living-the-passwordless-life/</a></p>
</li>
</ul>
<p><img src="https://cri.dev/assets/images/posts/webauthn-success-login.jpeg" alt="WebAuthn successful login" /></p>
How to become a better Software Developer2023-02-02T00:00:00Zhttps://cri.dev/posts/2023-02-02-How-to-become-a-better-Software-Engineer/<p>Troll ahead 👹</p>
<h1 style="font-size: 2em">
<marquee>
study,
</marquee>
</h1>
<h1 style="font-size: 2em">
<marquee>
study even more,
</marquee>
</h1>
<h1 style="font-size: 2em">
<marquee>
be humble
</marquee>
</h1>
<h1 style="font-size: 2em">
<marquee>
and
</marquee>
</h1>
<h1 style="font-size: 2em">
<marquee>
read the fucking docs
</marquee>
</h1>
OpenAI iOS Shortcut: Siri-like ChatGPT2023-01-22T00:00:00Zhttps://cri.dev/posts/2023-01-22-artificial-intelligence-openai-ios-shortcut-siri-chatgpt/<p>Recently stumbled upon <a href="https://matemarschalko.medium.com/chatgpt-in-an-ios-shortcut-worlds-smartest-homekit-voice-assistant-9a33b780007a">this article</a> about using OpenAI’s API in an iOS Shortcut, to act as a smart HomeKit Voice Assistant.</p>
<p>Grabbed the Shortcut from the article, and modified it to create two shortcuts that speak OpenAI’s API response:</p>
<ul>
<li>a “textual” shortcut, that prompts you a text input</li>
<li>a “voice” shortcut, that uses Speech-to-Text to read your voice input and convert it to text</li>
</ul>
<p>The nice part about the textual version is that it can be used also from iOS’s Sharesheet based on text you selected in another app.</p>
<p>Additionally, this can be improved further to simulate a ChatGPT-like experience, by keeping conversational context.</p>
<p><strong>Shortcuts linked at the end of the post.</strong></p>
<h1 id="textual-shortcut" tabindex="-1">Textual Shortcut</h1>
<p>The shortcut is pretty straightforward, it just prompts you for a text input (or reads the Shortcut Input value if called via Share Sheet), and then calls the OpenAI API with the text you entered.</p>
<p>Then it extracts the value from the <code>choices</code> array, the API’s response for the prompt “How are you?” has the following form:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"id"</span><span class="token operator">:</span><span class="token string">"cmpl-6bSFgj3DGgqMri31EJjE3xOy4fiko"</span><span class="token punctuation">,</span>
<span class="token property">"object"</span><span class="token operator">:</span><span class="token string">"text_completion"</span><span class="token punctuation">,</span>
<span class="token property">"created"</span><span class="token operator">:</span><span class="token number">1674384756</span><span class="token punctuation">,</span>
<span class="token property">"model"</span><span class="token operator">:</span><span class="token string">"text-davinci-003"</span><span class="token punctuation">,</span>
<span class="token property">"choices"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">{</span>
<span class="token property">"text"</span><span class="token operator">:</span> <span class="token string">"\n\nI'm doing great, thanks for asking!"</span><span class="token punctuation">,</span>
<span class="token property">"index"</span><span class="token operator">:</span><span class="token number">0</span><span class="token punctuation">,</span>
<span class="token property">"logprobs"</span><span class="token operator">:</span><span class="token null keyword">null</span><span class="token punctuation">,</span>
<span class="token property">"finish_reason"</span><span class="token operator">:</span><span class="token string">"stop"</span>
<span class="token punctuation">}</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token property">"usage"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"total_tokens"</span><span class="token operator">:</span><span class="token number">19</span><span class="token punctuation">,</span>
<span class="token property">"completion_tokens"</span><span class="token operator">:</span><span class="token number">11</span><span class="token punctuation">,</span>
<span class="token property">"prompt_tokens"</span><span class="token operator">:</span><span class="token number">8</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>In summary, the textual version of the iOS Shortcut looks like this:</p>
<p><img src="https://cri.dev/assets/images/posts/openai-ios-shortcut-text.jpeg" alt="" /></p>
<h1 id="voice-shortcut" tabindex="-1">Voice Shortcut</h1>
<p>The concept is essentially the same as the textual version, but it uses the <code>Dictate Text</code> action to convert your voice input to text.</p>
<p><img src="https://cri.dev/assets/images/posts/openai-ios-shortcut-voice.jpeg" alt="" /></p>
<h1 id="shortcut-download" tabindex="-1">Shortcut download</h1>
<p>Grab the desired shortcut from the links below, and run it from the Shortcuts app.</p>
<p><a href="https://www.icloud.com/shortcuts/6787d7a43e594b0d8c5b70f3409e6460">Textual version</a><br />
<a href="https://www.icloud.com/shortcuts/a66248d8370c48db8612dd38a2bb26b3">Voice version</a></p>
<p>You can add them to your Home Screen for quick access too.</p>
<h1 id="improvements" tabindex="-1">Improvements</h1>
<p>As mentioned above, the current version of the Shortcut doesn’t keep conversational context, so it’s not really a ChatGPT-like experience.</p>
<p>But I think it can be done if we use the <code>Get Value</code> action to store the previous prompt and response in a variable, and then use the <code>Set Value</code> action to update the variable with the new prompt and response.</p>
Artificial "intelligence"2023-01-13T00:00:00Zhttps://cri.dev/posts/2023-01-13-ai-openai-chatgpt-notes/<p>Personal notes about the article <a href="https://spectrum.ieee.org/stop-calling-everything-ai-machinelearning-pioneer-says">Stop calling it artificial intelligence</a></p>
<p><strong>Language models</strong> like ChatGPT, GPT-3 seem to pop-up in the news and in my YouTube feed quite a lot.</p>
<p>So I tried to better understand what these tools are and what they are not, what they can do and cannot do:</p>
<ul>
<li>no real knowledge of reasoning, the real-world, and social interaction</li>
<li>show human-level competence in low-level pattern recognition skills</li>
<li>at the cognitive level they are merely imitating human intelligence, not engaging deeply and creatively</li>
<li>machine learning can serve to augment human intelligence, via painstaking analysis of large data sets</li>
<li>can provide new services to humans in domains such as health care, commerce, and transportation</li>
<li>Good at finding patterns</li>
<li>There is no intelligent thought in computers that is responsible for the progress and which is competing with humans</li>
<li>do not form the kinds of semantic representations and inferences that humans are capable of</li>
<li>For the foreseeable future, computers will not be able to match humans in their ability to reason abstractly about real-world situations</li>
</ul>
Voracious RSS2022-12-27T00:00:00Zhttps://cri.dev/posts/2022-12-27-Voracious-RSS/<p>A while ago I wrote about my <a href="https://cri.dev/posts/2021-02-09-my-reading-stack-miniflux-rss-pocket">RSS setup</a> using Miniflux, and how I was using it to read the news from various sources.</p>
<p>Mostly subscribed to news sites (that have an RSS feed), some more niche blogs (finance etc.) and the HackerNews RSS feed.</p>
<p>But sometimes, depending on the week, I was not really reading the news, I was just collecting it and let it pile up.</p>
<p>I needed to focus on what is really important to me while reading the news and be more diligent about it.</p>
<p>Sometimes found myself with a pile of unread articles in my RSS reader which felt like an unsurmountable task.</p>
<p>I would then end up not reading anything at all and clearing the queue with my eyes closed.</p>
<p>This is a perfect example of <em>collectors fallacy</em>, collecting and collecting, for the sake of collecting and not for the sake of actually reading and acquiring knowledge.</p>
<p>I am done with that approach.</p>
<p>Essentially my approach was to take 30 minutes to an hour a day to read the news, and not let it pile up.</p>
<p>That’s the basic trick. Simple.</p>
<p>If I ever encountered an interesting article and saw it for the second day in a row, I would simply discard it.</p>
<p>As much as it “hurts” to discard an article, it is better to discard it and read something else, than to let it pile up and never read it.</p>
Escaping the bullshit web2022-10-22T00:00:00Zhttps://cri.dev/posts/2022-10-22-escaping-the-bullshit-web/<p>The web is bloated. It’s bloated with <a href="https://pxlnv.com/blog/bullshit-web/">bullshit</a>.</p>
<p>And I don’t mean with bullshit content, that’s fine; you should be able to freely express your opinion.</p>
<p>With “bullshit-bytes” sent down the wire, so to speak.</p>
<p>In the past I <a href="https://cri.dev/posts/2021-06-03-navigating-the-web-with-javascript-disabled/">wrote about an experiment</a> I was conducting (that sounds so scientific and proper), namely browsing the web with JavaScript disabled.</p>
<p>Hold your horses, I hear you.</p>
<blockquote>
<p>“Have you heard about Nickleback’s new song?”</p>
</blockquote>
<p>But that’s quite superficial, without <em>trying</em> to understand the real reasons and context.</p>
<p>So here we go, one year later, and many bytes saved and flashy ads avoided.</p>
<p>Read on before coming to conclusions, and who knows, perhaps you might want to escape the bullshit web too.</p>
<h1 id="tldr%3B" tabindex="-1">TLDR;</h1>
<p>Disable JavaScript. Using <a href="https://chrome.google.com/webstore/detail/noscript/doojmbjmlfjjnbmnoijecmcbfeoakpjm">Noscript</a>.</p>
<p>You can then white-list certain domains and individual scripts, e.g. allow all scripts from a web application you trust (e.g. <a href="https://pr.tn/ref/7ZSJWDRJATHG">ProtonMail’s web application</a>)</p>
<p>Will it work 100% of the time, without any issues ever?</p>
<p>100% not. But most of the time, yes, it works without major drawbacks.</p>
<p>The idea is essentially: Disable JavaScript globally, and enable it where you <em>need</em> it.</p>
<p>The <strong>need</strong> comes down to a few factors:</p>
<ul>
<li>
<p>You need it to read a article or access information on a <em>webpage</em> that won’t render without JavaScript</p>
</li>
<li>
<p>You want to use the interactivity that JavaScript provides on a certain web <em>app</em> (read “JavaScript application”)</p>
</li>
</ul>
<h1 id="more-in-depth" tabindex="-1">More in depth</h1>
<p>Use <a href="https://chrome.google.com/webstore/detail/noscript/doojmbjmlfjjnbmnoijecmcbfeoakpjm">Noscript</a>.</p>
<p>Or <a href="https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm">uBlock origin</a>.</p>
<p>Or through Brave, if you’re brave enough.</p>
<p>Use <a href="https://chrome.google.com/webstore/detail/cookie-autodelete/fhcgjolkccmbidfldomjliifgaodjagh/">Cookie AutoDelete</a>.</p>
<p>Pair that with a Pi-hole/Adguard installation on your network/Rpi and smooth sailing awaits.</p>
<p>In both extensions, you can whitelist certain urls or cookies you “trust”/need.</p>
<h2 id="pros" tabindex="-1">Pros</h2>
<h3 id="save-a-lot-of-bandwidth" tabindex="-1">Save a lot of bandwidth</h3>
<p>It saves <em>a lot</em> of bandwidth. With a lot I mean around 90%.</p>
<p>For me it’s a game changer, since I live in a small rural town browsing the web with a 4G modem and have limited bandwidth.</p>
<p>Simply put: Most of the time, you don’t need JavaScript at all.</p>
<p><em>To read articles and browse the web.</em></p>
<p>If you’re using a <em>JavaScript web app</em>, you’ll most likely encounter a blank page, or the dreaded <code><noscript></code> “You need JavaScript to run this app” will appear.</p>
<p>In that case, you can <em>decide</em> if it’s worth enabling JS or bail out.</p>
<h3 id="faster-browsing" tabindex="-1">Faster browsing</h3>
<p>I don’t have numbers for the amount of time I saved/lost browsing the web with JS disabled.</p>
<p>I might be biased, but I feel that whitelisting some common JS (e.g. a CDN) works most of the time and enables to render substantial portions of most webpages.</p>
<h3 id="less-cookie-banners" tabindex="-1">Less cookie banners</h3>
<p>Awww yessss!</p>
<p>That’s one the most satisfying achievements of this experiment.</p>
<p>No JS? No cookie banners.</p>
<h3 id="less-googly%2Fcreepy-trackers-(perceived-privacy)" tabindex="-1">Less googly/creepy trackers (<em>perceived</em> privacy)</h3>
<p>Essentially the above. No JS? <em>Less</em> tracking.</p>
<h3 id="unexpected-pros-%23-1-(paywall)" tabindex="-1">Unexpected pros # 1 (paywall)</h3>
<p>I was reading an article on a popular Italian news magazine, without issues.</p>
<p>Shared it on Twitter, and some people complained about the nasty paywall, not being able to read it fully.</p>
<p>They were using JavaScript to <em>remove</em> content, not to <em>add</em> it for paid users.</p>
<h3 id="unexpected-pros-%23-2-(degradation)" tabindex="-1">Unexpected pros # 2 (degradation)</h3>
<p>Most of the time, browsing with JavaScript disabled, it just works.</p>
<p>Quality content is served with <em>HTML fallback behavior</em>/<em>Graceful degradation</em> in mind.</p>
<h2 id="cons" tabindex="-1">Cons</h2>
<p>You’ll need to manually whitelist domains you trust and perform some one-time manual actions.</p>
<p>It’s a small price to pay if you want to save bandwidth and browse the web faster.</p>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>I hear someone shouting from afar:</p>
<blockquote>
<p>“Why don’t you use Lynx at this point”<br />
“Or cURL while you’re at it?! hahha”</p>
</blockquote>
<p>I get it. It’s not perfect and it can be cumbersome from time to time.</p>
minimal-analytics, one year later2022-07-18T00:00:00Zhttps://cri.dev/posts/2022-07-18-One-year-minimal-analytics/<p>Started tracking pageviews on <a href="https://s.cri.dev/">s.cri.dev</a> with <a href="https://github.com/christian-fei/minimal-analytics">minimal-analytics</a> at the beginning of May 2021.</p>
<p>Here I wanted to share a story about a simple self-hosting solution for tracking website users in a <strong>privacy-friendly, cookie-less, gdpr-compliant</strong> way.</p>
<p>Oh well, and happy birthday <a href="https://github.com/christian-fei/minimal-analytics">minimal-analytics</a> 🎂</p>
<h2 id="110k-pageviews" tabindex="-1">110k pageviews</h2>
<p>After 1 year of tracking, minimal-analytics tracked 110k pageviews to this very blog.</p>
<p><a href="https://s.cri.dev/?resolution=monthly&timeframe=past-year">Check it out at s.cri.dev</a></p>
<p>Out of ~105k visitors and ~115k pageviews, 75k+ come from google.</p>
<p>That’s a solid piece of traffic from google.</p>
<p>A small 7k come from duckduckgo.</p>
<p>The bounce rate is around 91%.</p>
<p>The traffic is increasing, month to month, starting with ~7k visitors a month to ~10k in July 2022.</p>
<p>A rough 40% increase.</p>
<p><img src="https://cri.dev/assets/images/posts/minimal-analytics-one-year.png" alt="minimal analytics data one year" /></p>
<h2 id="hosting" tabindex="-1">Hosting</h2>
<p>My minimal-analytics instance, hosted at <a href="https://s.cri.dev/">s.cri.dev</a>, is running on a <a href="https://www.linode.com/lp/refer/?r=a248b42e63f90809eedddafd6bb07f1522e09993">small Linode VPS</a>, 1GB RAM, 2 vCPUs, 25GB SSD.</p>
<p>For <a href="https://www.linode.com/lp/refer/?r=a248b42e63f90809eedddafd6bb07f1522e09993">5$ a month</a>, it’s been a good deal and experience for me.</p>
<p>I am using <code>docker-compose</code> and <code>nginx</code>, cloudflare as cache, dns & domain.</p>
<p>Here the instructions for <a href="https://github.com/christian-fei/minimal-analytics#how-to-self-host">self-hosting minimal-analytics</a></p>
<h2 id="the-experiment-goes-on" tabindex="-1">The experiment goes on</h2>
<p>A JSONL file as a DB to track visitors.</p>
<p>Hash the ip, url and a seed to compute a quite unique visitor hash.</p>
<p>All on the server, without cookies, with minimal JavaScript involved.</p>
<p>That is the idea behind <a href="https://github.com/christian-fei/minimal-analytics">minimal-analytics</a>, and I am planning to extend it further:</p>
<ul>
<li>more granular referrer breakdowns (e.g. aggregate all google.* sites)</li>
<li>allow embedding into iframes/images</li>
<li>improve chart visualization</li>
</ul>
<p>And if you want to contribute to the project, file an issue or pull-request <a href="https://github.com/christian-fei/minimal-analytics">here on github</a></p>
Did an ES module migration, and it was okay2022-07-14T00:00:00Zhttps://cri.dev/posts/2022-07-14-Did-an-ES-module-migration-and-it-was-okay/<p>Just recently I moved <a href="https://github.com/christian-fei/minimal-analytics/">minimal-analytics</a>, a Node.js and Preact project to ES modules.</p>
<p>And it was quite pleasant and straightforward.</p>
<p>It didn’t <code>require</code> much work to migrate the project to ES modules 🤭.</p>
<h2 id="why%3F" tabindex="-1">Why?</h2>
<ul>
<li>wanted to learn more about ES modules</li>
<li>it’s the (foreseeable) future/standard of module resolution in Node.js and the browser</li>
<li>more and more people and dependencies are using ES modules</li>
<li>perfect excuse to revisit some parts of the project</li>
<li>why not?</li>
</ul>
<h2 id="how-did-it-go%3F" tabindex="-1">How did it go?</h2>
<p>I honestly had some troubles with two things:</p>
<p>Namely:</p>
<ul>
<li><code>path.resolve</code> occurrences with <code>__dirname</code> caused me some headaches (<a href="https://cri.dev/rss.xml#pathresolve-with-__dirname">see dedicated section</a>)</li>
<li>I had to dig deeper to simulate the <code>require.main === module</code> check (<a href="https://cri.dev/rss.xml#requiremain--module">see dedicated section</a>)</li>
</ul>
<p>Ah, and before I forget, use node 16.16.0 (lts) for the best experience.</p>
<p>And also, set <code>"type": "module"</code> in your project’s <code>package.json</code> to make it work.</p>
<p>Check out the <a href="https://github.com/christian-fei/minimal-analytics/commit/990ba5708fd8c438c0119609ddd5cded5562d982">commit for the migration</a> if you want to see the code.</p>
<h3 id="path.resolve-with-__dirname" tabindex="-1"><code>path.resolve</code> with <code>__dirname</code></h3>
<p>I had some trouble with this, since in a few places I needed to resolve for relative filepaths.</p>
<p>The CommonJS variables <code>__dirname</code> and <code>__filename</code> are not available in ES modules.</p>
<p>But the usage can be replicated with <code>import.meta.url</code>, the native <code>URL</code> module and <code>pathname</code> property.</p>
<p>E.g.</p>
<p><code>new URL(import.meta.url).pathname</code> is essentially the same as <code>__filename</code> in the CommonJS environment.</p>
<p>And similarly, <code>new URL('.', import.meta.url).pathname</code> can be compared to <code>__dirname</code> in CommonJS.</p>
<p><em>Keep in mind, this is not the only way to simulate __dirname and __filename</em></p>
<p>Under an experimental flag, you can resolve modules relatively with <code>import.meta.resolve</code> (<a href="https://nodejs.org/api/esm.html#importmetaresolvespecifier-parent">as far as I understood</a>)</p>
<p>See <a href="https://nodejs.org/api/esm.html#no-__filename-or-__dirname">this</a> for more information.</p>
<h3 id="require.main-%3D%3D%3D-module" tabindex="-1"><code>require.main === module</code></h3>
<p>This is oftentimes used to determine if the main script was run directly from the shell, or required as a module from another script.</p>
<p>I am using this technique to start the server when the script was invoked from the shell.</p>
<p>e.g.</p>
<pre><code>#!/usr/bin/env node
...
if (require.main === module) {
startServer()
}
...
</code></pre>
<p>To replicate this behaviour, I found multiple ways to do it:</p>
<pre><code># option 1
if (import.meta.url === `file://${process.argv[1]}`) { ... }
# option 2
if (new URL(import.meta.url).pathname === process.argv[1]) { ... }
# option 3
import url from 'url'
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { ... }
# option 4
// TC39 proposal for import.meta.main
# option 5
# See https://github.com/tschaub/es-main
import esMain from 'es-main';
if (esMain(import.meta)) { ... }
</code></pre>
<p>Well, as always, in JavaScript, there are multiple ways to achieve the same thing. Some cleaner, some hackier.</p>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>Apart from the usual ramblings about JavaScript, Node.js, standards and headaches, it was a fun experience.</p>
<p>Learned a ton about ES modules, and how to use them in Node.js.</p>
<h2 id="more-reading-about-esm" tabindex="-1">More reading about ESM</h2>
<p>Here some of the best resources I found on this topic, I highly suggest to read them</p>
<ul>
<li><a href="https://nodejs.org/api/esm.html">Node.js ESM documentation</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">A guide to ES modules - mozilla</a></li>
<li><a href="https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/">ES modules:§ a cartoon deep-dive</a></li>
</ul>
Self-hosting nitter with docker-compose and nginx2022-07-12T00:00:00Zhttps://cri.dev/posts/2022-07-12-Self-hosting-nitter-with-docker-compose-and-nginx/<p>In this short guide, I wanted to document the few steps needed to have nitter running with docker-compose behind nginx.</p>
<h2 id="1.-clone-the-repo" tabindex="-1">1. Clone the repo</h2>
<p><code>git clone --depth 1 https://github.com/zedeus/nitter.git</code></p>
<h2 id="2.-configure-nitter" tabindex="-1">2. Configure nitter</h2>
<p>Create a <code>nitter.conf</code> file in the root of the repo, by copying the example conf: <code>cp nitter.example.conf nitter.conf</code></p>
<p>In <code>nitter.conf</code> file, change the <code>redisHost</code> to <code>nitter-redis</code></p>
<p>Optionally, adapt the other settings, but for a bare setup, that’s the only change needed.</p>
<h2 id="3.-docker-compose-setup" tabindex="-1">3. docker-compose setup</h2>
<p>Since nitter is running behind nginx, thus a proxy, you can leave the exposed ports in <code>docker-compose.yml</code> as they are.</p>
<p>Alternatively, if you want to expose nitter under a different port than 8080, you can do so by changing the exposed ports like this: <code>127.0.0.1:YOUR_PORT</code></p>
<p>Now run <code>docker-compose up -d</code> from the project directory to start nitter.</p>
<h2 id="4.-nginx-setup" tabindex="-1">4. nginx setup</h2>
<p><code>sudo vim /etc/nginx/sites-available/nitter</code></p>
<p>Add the following content to that file, just replacing the <code>YOUR_DOMAIN</code> placeholder.</p>
<pre><code>upstream nitter {
server 127.0.0.1:8080 max_fails=5 fail_timeout=60s;
}
server {
server_name YOUR_DOMAIN; # e.g. nitter.example.com
listen 80;
listen [::]:80;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml;
client_max_body_size 16m;
ignore_invalid_headers off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://nitter;
}
}
</code></pre>
<p>Now create a symbolic link to make the nginx configuration available:</p>
<pre><code>sudo ln -s /etc/nginx/sites-available/nitter /etc/nginx/sites-enabled/nitter
</code></pre>
<p>Test the nginx setup with <code>sudo nginx -t</code></p>
<p>And reload the nginx service with <code>sudo service reload nginx</code></p>
<h2 id="5.-configure-your-dns" tabindex="-1">5. Configure your DNS</h2>
<p>Update your DNS records to point with an <code>A</code> record to your server’s IP mapping it to the configured domain.</p>
Fix node command not found using nvm2022-06-05T00:00:00Zhttps://cri.dev/posts/2022-06-05-Fix-node-command-not-found-using-nvm/<p>Had an issue with <code>nvm</code> not loading <code>node</code> and <code>npm</code>.</p>
<p>Rightfully in my session I was getting <code>command not found: node</code> when trying to run <code>node</code> or <code>npm</code>.</p>
<p>Most likely it’s an issue related to my previous installation with <code>brew</code>…</p>
<p>To solve this issue, I simply ran <code>nvm alias default 16.0.0</code> after installing the specific node version with <code>nvm install 16.0.0</code></p>