<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
        <title>Renan  LE CARO</title>
        <link>https://lecaro.me</link>
        <description>I try to include the post content directly in the feed</description>
        <lastBuildDate>Wed, 14 Oct 2026 18:07:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Giving Publii a try ]]></title>
            <link>https://lecaro.me/20200527-giving-publii-a-try.html</link>
            <guid>https://lecaro.me/20200527-giving-publii-a-try.md</guid>
            <pubDate>Tue, 26 May 2020 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Giving Publii a try</h1>
<p>As a web developer, I&#39;ve created some websites for family and friends. My default tool for that was WordPress, because you could easily set them up, pick a theme, mess with the CSS a bit and let them write the content.</p>
<p>But hosting a wordpress website is always a trade-off between reliability and costs. On one side you have free hosts giving you a terrible VM that can barely handle the administration side, on the other overpriced plans for what&#39;s really a tiny website with usually almost no traffic. </p>
<pre><code>I&#39;ve always thought that WordPress was using a tank to kill a fly.
</code></pre>
<p>So until now, for non technical business owners, I&#39;ve set up a free host and the administration of the website was usually quite slow and unreliable. If the client wanted something more reliable, I&#39;d get a paid hosting plan, but that would include some overhead for me. </p>
<p>For my personal projects I personally used custom made static websites, with or without automatic generation. But I didn&#39;t see myself explaining the details of git push, markdown and CLI use to my parents. I thought I should work on an ideal, user friendly static website editor one of those days.  </p>
<p>Today i discovered Publii and thought i&#39;d give it a try for my blog. If it&#39;s satisfactory, I might recommend it to my less tech-savvy friends in the future. Until now I&#39;ve been pleasantly surprised. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Documenting systems]]></title>
            <link>https://lecaro.me/20200530-documenting-systems.html</link>
            <guid>https://lecaro.me/20200530-documenting-systems.md</guid>
            <pubDate>Fri, 29 May 2020 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Documenting systems</h1>
<p>Back when I was working at virtualQ, I had to do some documentation. At first I didn&#39;t quite enjoy it, feeling like this was a chore. But documenting a system can be a very interesting task. I also had to explain our systems to new hires after we raised some funds and quickly grew the team. </p>
<p>We had a documentation website for our API, based on markdown. It was very technical, with manually copy pasted sample API responses. It wasn&#39;t very visual. It had endpoints documented publicly while they were only for internal use. It had a lot of outdated content, and felt more like a patchwork of data bits than a comprehensible thing.</p>
<p>My fist step was to remove the documentation off irrelevant endpoints. The docs were kind of an hybrid between a public api documentation and some internal guide for new hires. I made dedicated to customers trying to use our api only.</p>
<p>It still had a &quot;wall of text&quot; feeling though. To make it more palatable, I tried to add visuals. Integrating mermaid to the docs was quite easy, I styled it roughly with some css rules. Sequence diagrams are awesome. I used them to communicate the different workflow we had and some typical scenarios the clients would like to use our API for. </p>
<p>We had a complex state machine that tracks the state of a caller through our system. I tried to represent it as a graph, but that was a bit too cluttered, so i split it in two graphs and removed some of the transitions that were not normally happening. </p>
<p>Finally, I wanted to fix our outdated sample API requests and responses. Our API was evolving rapidly and having outdated samples was more confusing than helpful. I created a tiny script that curl commands and update the API answer samples with them. I had to do some dirty grepping on the answers to replace dates, ids, etc .. to avoid polluting the git log with meaningless edits every time we&#39;d refresh the samples.</p>
<p>We deployed the new version of the documentation. Some of our new hires still had some use for the old version (that documented more endpoints) so we added a link to it. We don&#39;t have any tracking on the docs website, so I have no idea what the readership is, but it was great as a learning experience.</p>
<p>It made me love mermaidJS so much, that I eventually ended up building a google docs addon for it. </p>
<p>I like the idea of writing tutorials as markdown, with E2E test instructions mixed in, and outputting the result (text, screenshots, response samples) as a document. It would guarantee that the tutorials stay in sync with the product and don&#39;t become outdated, with always fresh screenshots. It would make the task of writing E2E tests for some web app a dual purpose task, where you&#39;d generate both a helpful document and some smoke tests in one go. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Automating local backups of DO MUP on Ubuntu]]></title>
            <link>https://lecaro.me/20200804-automating-backups-of-digital-ocean-meteor-up.html</link>
            <guid>https://lecaro.me/20200804-automating-backups-of-digital-ocean-meteor-up.md</guid>
            <pubDate>Mon, 03 Aug 2020 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Automating local backups of DO MUP on Ubuntu</h1>
<h2>DO + MUP = 💙</h2>
<p>I&#39;ve migrated my main project,  from Heroku to Digital Ocean, with the help of the awesome meteor up tool.</p>
<p>I wanted a way to automate the backups of production to my own computer. In meteor, the only thing really worth backing up is the content of the mongoDB.</p>
<p><strong>We need two things : a script</strong> that does the backup, <strong>and a cron job</strong> to run regularly</p>
<h2>Backup script</h2>
<p>You just need to configure the two environment variables at the top (in bold)</p>
<p><strong>daily_backup.sh</strong></p>
<pre><code>#!/bin/sh  
  
\# folder where to store the last 7 daily backups, without trailing slash  
DUMPFOLDER=&quot;**/home/renan/cl/app/dump**&quot;  
  
\# IP of your digital ocean droplet  
DO\_INSTANCE\_IP=&quot;**46.101.232.182**&quot;  
  
\# so that notifications know on which display to show when called from the cron tab  
export DISPLAY=:0  
  
\# Let it quit when it crash  
set -e  
  
DUMPFILE=&quot;$DUMPFOLDER/daily\_$(date +&quot;%y-%m-%d&quot;).backup.gzip&quot;  
  
if \[ ! -f $DUMPFILE \];  
then  
  continue  
else  
  echo &quot;Already done today&quot; &amp;&amp; exit 0  
fi  
  
notify-send &quot;Backing up ...&quot;  
  
\# ssh to the instance, then ask the mongodb container to output an archive of all dbs, then stream that back to your machine.   
ssh &quot;root@$DO\_INSTANCE\_IP&quot; &quot;docker exec mongodb mongodump --archive --gzip&quot; &gt; &quot;${DUMPFILE}&quot;  
  
\# Only keep the last 7 dumps   
ls -tp $DUMPFOLDER/ | grep -v &#39;/$&#39; | tail -n +8 | xargs -I {} rm -- $DUMPFOLDER/{}  
  
notify-send &quot;... backup done.&quot;
</code></pre>
<p>Make sure the script is executable with <code>chmod +x daily_backup.sh</code></p>
<h2>CRON setup</h2>
<p>We will run this script every hour. The first time it runs it will do the backup, and the next time it will see the file exists already and just exit. The next day the filename will be different and therefore a new backup will be run. </p>
<p>Open the cron editor : <code>crontab -e</code> </p>
<p>Add the following line, modifying the path in bold to point to your script.  </p>
<pre><code>1 * * * * /**home/renan/cl/app/.production/daily_backup.sh** &gt;/dev/null 2&gt;&amp;1
</code></pre>
<p>This will run the script every hour when minutes are equal to 1, so if you start your computer at 8:30 it will first run at 9:01</p>
<p>The redirect after the command are to avoid having errors show up in the logs. Ubuntu is a bit confused when cron jobs generate outputs.  </p>
<h2>How to restore such backup</h2>
<p>Having backup is nice, but knowing out to restore them is better. </p>
<pre><code>#!/bin/sh  
  
\# folder with the dumps  
DUMPFOLDER=&quot;**/home/renan/cl/app/dump**&quot;  
\# IP of your digital ocean droplet  
DO\_INSTANCE\_IP=&quot;**46.101.232.182**&quot;  
  
\# Let it quit when it crash  
set -e  
  
\# find the latest backup file  
LATEST\_FILE=$(ls -t1 &quot;$DUMPFOLDER/&quot; |  head -n 1)  
LATEST\_DUMP=&quot;$DUMPFOLDER/$LATEST\_FILE&quot;  
echo &quot;Restoring to production from $LATEST\_DUMP&quot;  
read -p &quot;Press enter to continue ..&quot;  
  
\# pipe file to the mongorestore inside the mongodb docker container inside the droplet   
cat &quot;${LATEST\_DUMP}&quot; | ssh &quot;root@$DO\_INSTANCE\_IP&quot; &quot;cat | docker exec -i mongodb mongorestore --archive --gzip --drop&quot;
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Customizing the look of Publii]]></title>
            <link>https://lecaro.me/20200805-customizing-the-look-of-publii.html</link>
            <guid>https://lecaro.me/20200805-customizing-the-look-of-publii.md</guid>
            <pubDate>Tue, 04 Aug 2020 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Customizing the look of Publii</h1>
<p>I really like the look of the default Publii theme, however some modifications weren&#39;t quite working using the in-app options. I wanted this website to have a very low level of interactivity, i don&#39;t need a mobile hamburger menu for 2 short entries (code &amp; life). </p>
<p>So I started looking for a different theme but I couldn&#39;t find what I was looking for, that is, a minimalist theme. I resolved to just strip things away from the default theme to make it lighter and JS free. </p>
<p>Opening the default theme gave me the same feeling that I got when trying to customize the code of a 40$ admin panel. Lots of code, neatly organized, most of it unused.</p>
<pre><code>What is all this code, what is it for ? Oh, i can get rid of it.  
</code></pre>
<p>I managed to strip down the source by removing the parts I wasn&#39;t interested in. I think now the design looks clean, and I got rid of most useless css and js I could find. I&#39;m not sure that this is the right approach, as those changes definitely won&#39;t survive a theme update (but why would I want to update it ?) and are a bit hacky. I&#39;m pretty sure those changes are only stored on my machine, so if I try to edit the website from somewhere else it will revert back to it&#39;s default look. </p>
<p>However, i&#39;m not too worried about this. This personal website is a playground for experimentation after all.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Be careful what you ping for]]></title>
            <link>https://lecaro.me/20210324-be-careful-what-you-ping-for.html</link>
            <guid>https://lecaro.me/20210324-be-careful-what-you-ping-for.md</guid>
            <pubDate>Tue, 23 Mar 2021 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Be careful what you ping for</h1>
<p>I have a setup to know when my main app, ciboulette.net, is down.</p>
<p>It&#39;s based on <a href="https://www.statuscake.com/">statuscake</a>, which pings my marketing site every 5 minutes, and also my main app. </p>
<p>It used to ping a third endpoint, which was an order form deep inside my app. </p>
<p>The idea was to check the the clients of my clients should be able to order veggies, so pinging a specific, real page would make sense.</p>
<p>I also used to log every action that would happen on such an order, directly in an array within the &quot;order&quot; document.</p>
<p>It means that every 5 minute, the document was getting a tiny bit bigger. </p>
<p>Today, i tried to mongorestore a production dump on my local dev environment to get fresh data. It&#39;s very useful to see how the real data would look like in the new version, and lets me see missuses and bugs that i would miss otherwise. </p>
<p>So i tried to import my production dump (the db needs renaming from &quot;ciboulette&quot; to &quot;meteor&quot; to work locally) </p>
<pre><code>renan@renan-hp:~/cl/app$ cat dump/daily\_21-03-24.backup.gzip | mongorestore --archive --gzip  --nsFrom=&#39;ciboulette.\*&#39; --nsTo=&#39;meteor.\*&#39; --uri=mongodb://127.0.0.1:3001/  
2021-03-24T15:31:06.076+0100 preparing collections to restore from  
2021-03-24T15:31:06.083+0100 reading metadata for meteor.shortUrls from archive on stdin  
2021-03-24T15:31:06.102+0100 restoring meteor.shortUrls from archive on stdin  
2021-03-24T15:31:06.263+0100 reading metadata for meteor.opens from archive on stdin  
2021-03-24T15:31:06.278+0100 restoring meteor.opens from archive on stdin  
2021-03-24T15:31:06.753+0100 reading metadata for meteor.Orders from archive on stdin  
2021-03-24T15:31:06.767+0100 restoring meteor.Orders from archive on stdin  
2021-03-24T15:31:07.308+0100 reading metadata for meteor.logs from archive on stdin  
2021-03-24T15:31:07.318+0100 restoring meteor.logs from archive on stdin  
2021-03-24T15:31:07.827+0100 no indexes to restore  
2021-03-24T15:31:07.827+0100 finished restoring meteor.shortUrls (8224 documents, 0 failures)  
2021-03-24T15:31:08.269+0100 no indexes to restore  
2021-03-24T15:31:08.269+0100 finished restoring meteor.opens (21263 documents, 0 failures)  
2021-03-24T15:31:08.503+0100 finished restoring meteor.Orders (32999 documents, 1 failure)  
2021-03-24T15:31:08.503+0100 Failed: **meteor.Orders: error restoring from archive** on stdin: bulk write error: \[{\[{**write to oplog failed: BadValue: object to insert exceeds cappedMaxSize**}\]}, {&lt;nil&gt;}\]  
2021-03-24T15:31:08.503+0100 32999 document(s) restored successfully. 1 document(s) failed to restore.
</code></pre>
<p>It took me quite a while to pinpoint the problem. </p>
<p>Luckily, the same dump would import fine in a real mongod instance, the problem seemed to be only with the meteor provided development mongo instance. </p>
<p>The problem seemed to be be with some order documents. </p>
<p>So, first i wiped the local &quot;meteor&quot; db in mongod</p>
<pre><code>mongo mongodb://127.0.0.1:27017/meteor --eval &quot;db.dropDatabase()&quot;
</code></pre>
<p>Then i imported the dump there</p>
<pre><code>cat dump/daily\_21-03-24.backup.gzip | mongorestore --archive --gzip  --nsFrom=&#39;ciboulette.\*&#39; --nsTo=&#39;meteor.\*&#39;
</code></pre>
<p>I tried removing all documents from db.Orders, dumping, and then importing that to meteor, it worked. But i was missing my main collection.</p>
<p>I then used this script to find the biggest document and its _id within my mongo collection</p>
<pre><code> mongo  mongodb://127.0.0.1:27017/meteor --eval &quot;(()=&gt;{var max=0,maxId=null; **db.Orders**.find().forEach(o=&gt;{var s=**Object.bsonsize**(o);if(s&gt;max){max=s;maxId=o.\_id}}); return {max,maxId}})()&quot;  
      
MongoDB server version: 4.4.4  
{ **&quot;max&quot; : 14199831**, &quot;maxId&quot; : &quot;GBeerkZ46dcjAgLtu&quot; }
</code></pre>
<p>14 199 831 bytes, holly molly ! About 14 megabytes of text in one document.</p>
<p>A findOne query allowed me to see that it was the &quot;Order.logs&quot; field that was so full.</p>
<pre><code>So i cleaned up this field in  all documents :

renan@renan-hp:~/cl/app$ mongo mongodb://127.0.0.1:27017/meteor --eval &#39;db.Orders.update({logs:{ $exists:true}},**{$unset:{logs:&quot;&quot;}},{multi:true}**);&#39;  
MongoDB shell version v4.4.4  
connecting to: mongodb://127.0.0.1:27017/meteor?compressors=disabled&amp;gssapiServiceName=mongodb  
Implicit session: session { &quot;id&quot; : UUID(&quot;bb02fe49-5241-4beb-82ea-fbdc4c325998&quot;) }  
MongoDB server version: 4.4.4  
WriteResult({ &quot;nMatched&quot; : 180383, &quot;nUpserted&quot; : 0, **&quot;nModified&quot; : 180383** })
</code></pre>
<p> And re-run the maximum size check </p>
<pre><code>mongo mongodb://127.0.0.1:27017/meteor --eval &quot;(()=&gt;{var max=0,maxId=null; db.Orders.find().forEach(o=&gt;{var s=Object.bsonsize(o);if(s&gt;max){max=s;maxId=o.\_id}}); return {max,maxId}})()&quot;  
{ &quot;max&quot; : **7578**, &quot;maxId&quot; : &quot;eYdGxbBKDNfKZYZw8&quot; }
</code></pre>
<p>Much, much better :)</p>
<p>So, all that was left was to dump that data, load the dump in meteor and all was fine.</p>
<p>I added a migration to clean that field in production too.  It will run on the next deploy.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Javascript minification with sed]]></title>
            <link>https://lecaro.me/20210324-javascript-minification-with-sed.html</link>
            <guid>https://lecaro.me/20210324-javascript-minification-with-sed.md</guid>
            <pubDate>Tue, 23 Mar 2021 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Javascript minification with sed</h1>
<p>I added a few of my projects to <a href="https://1mb.club/">https://1mb.club/</a>. I&#39;m not sure what for, but eh, it&#39;s free link estate !
I stumbled upon a touch type trainer in 2 KB of js. The best thing about it is not really the tool (it&#39;s limited,
obviously) but the absurd way the author got down to that 2kb size, in particular this little gem.</p>
<p><a href="https://github.com/KaarelP2rtel/keyboard-tutor/blob/master/minify.sh">https://github.com/KaarelP2rtel/keyboard-tutor/blob/master/minify.sh</a></p>
<pre><code>sed -i &#39;s/tutor-window/tw/g&#39; &quot;$target&quot;
sed -i &#39;s/tutor-container/tc/g&#39; &quot;$target&quot;
sed -i &#39;s/tutor-completed/tm/g&#39; &quot;$target&quot;
sed -i &#39;s/tutor-cursor/tr/g&#39; &quot;$target&quot;
sed -i &#39;s/tutor-uncompleted/tu/g&#39; &quot;$target&quot;
sed -i &#39;s/tutor-result/te/g&#39; &quot;$target&quot;
sed -i &#39;s/tutor-status/ts/g&#39; &quot;$target&quot;
sed -i &#39;s/current-layout/cl/g&#39; &quot;$target&quot;
</code></pre>
<p>I have no words, it&#39;s beautiful :)</p>
<p>I remember going to similar extremes on importabular.</p>
<p>All the private methods had names prefixed by an underscore, and then i had some instructions for the minifying tool to
not keep names with that prefix.</p>
<p>It shaved a few kilobytes from the already tiny lib, but i reverted it after to make it easier to subclass the
Importabular class.  </p>
<p>Anyway, gzipped code golf is always a lot of fun. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Meteor lags ? Aggregates to the rescue !]]></title>
            <link>https://lecaro.me/20210326-meteor-lags-aggregates-to-the-rescue.html</link>
            <guid>https://lecaro.me/20210326-meteor-lags-aggregates-to-the-rescue.md</guid>
            <pubDate>Thu, 25 Mar 2021 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Meteor lags ? Aggregates to the rescue !</h1>
<p>My app has 4 main collections in mongo : </p>
<ul>
<li><p>A user has many weeks and clients</p>
</li>
<li><p>Each week has many orders, one per client, where the order content is stored (can be empty if the client didn&#39;t order that week).</p>
</li>
</ul>
<p>Initially, my documents were quite dry, I was doing the &quot;joins&quot; work on the client. It got laggy though, so I moved the joins logic to the meteor server, with hooks (matb33:collection-hooks) in every directions to keep denormalized fields in sync.</p>
<p>It was a pain though, really hard to follow what was going on and to debug. </p>
<p>There&#39;s also potential for chain reactions and never ending hooks loops. </p>
<p>But mostly, it was buggy in edge cases. </p>
<p>So I&#39;m migrating away from those hooks, and creating an aggregation pipeline per collection (week, client, order) that computes all the dynamic fields for some given ids, and returns the documents where the value is different, plus some additional ids to check in the next pass. </p>
<p>It&#39;s not yet in production, but after this rewrite, my e2e tests are passing. </p>
<p>Meteor uses mongodb 4.2 in development server. I didn&#39;t feel like forcing an update to 4.4, which brings in two very useful features for my use case (the $function operator and the ability to $merge to the collection that you are aggregating on).</p>
<p>So I&#39;ve been $writing:{ $some: {$nested: {$queries:&#39;It&#39;s a pain&#39;&#39;}}} . I&#39;m back to junior level of mastery in this new paradigm, where &quot;It works&quot; comes as a surprise. I&#39;m sure i&#39;ll rewrite those ugly aggregations at some point, but for now they seem to do the job. </p>
<p>One nice approach to using aggregation pipelines in meteor is to declare them in a separate js file, and then have a simple, non-meteor script that runs the aggregation and logs the results. </p>
<p>I use nodemon to watch the files, and get a very fast feedback loop, without the need to wait for the meteor server to recompile. Great when you&#39;re learning as you go. </p>
<p>I start the script like this (-r esm allows me to use modern imports) </p>
<pre><code>nodemon -e js  -r esm tests/scripts/mongofun.js
</code></pre>
<p> And the script itself is very simple. It just runs the query </p>
<p>tests/scripts/mongofun.js :</p>
<pre><code>const mongoUrl=&#39;mongodb://127.0.0.1:3001/meteor&#39;

async function actual_aggregation(db) {
  const result = await db
    .collection(&#39;Orders&#39;)
    .aggregate([
      {
        $match: {_id:&#39;&lt;some test id&gt;&#39;}
      }, 
      ...the_pipeline_imported_from_the_meteor_source
    ])
    .toArray();
  console.log(result);
}

const mongodb = require(&#39;mongodb&#39;);
const client = new mongodb.MongoClient(mongoUrl, {
  useUnifiedTopology: true
});

client.connect().then(
  async () =&gt; {
    const start = Date.now();
    await author_aggregation(client.db());
    console.log(&#39;took &#39; + (Date.now() - start) + &#39; ms&#39;);
  },
  e =&gt; console.error(e)
);
</code></pre>
<p>Anyway, that&#39;s all for today :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Configuring i3 for headful e2e tests]]></title>
            <link>https://lecaro.me/20210327-configuring-i3-for-headful-e2e-tests.html</link>
            <guid>https://lecaro.me/20210327-configuring-i3-for-headful-e2e-tests.md</guid>
            <pubDate>Fri, 26 Mar 2021 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Configuring i3 for headful e2e tests</h1>
<p>i3 is nice. End to end tests are nice. Chromium is nice. But they don&#39;t get along so well by default.</p>
<p>When you run you e2e script with nodemon, each run opens and closes the test browser window. By default, that window appears right by your editor, messing the layout.</p>
<p>It&#39;s easy to fix though : you just need to tell i3 what to do with that test browser window.</p>
<p>To make every new chromium window appear in the workspace $ws5, add this in ~/.config/i3/config</p>
<pre><code>assign [class=&quot;Chromium-browser&quot;] → $ws5
</code></pre>
<p>The workspace name is a variable here, but you can put anything. I assign that window to a rarely used workspace. </p>
<p>Chromium has a different class from Google Chrome, so this has no impact on where the &quot;real&quot; browser is started.</p>
<p>To get the class infos for other browsers, run xprop in a terminal and then click on a window.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github action to check for TODOs and FIXMEs]]></title>
            <link>https://lecaro.me/20210327-github-action-to-check-for-todos-and-fixmes.html</link>
            <guid>https://lecaro.me/20210327-github-action-to-check-for-todos-and-fixmes.md</guid>
            <pubDate>Fri, 26 Mar 2021 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Github action to check for TODOs and FIXMEs</h1>
<p>I had some TODO and FIXME instructions in random locations in my code that served no purpose, as I was never looking for them. I also use &quot;console.log&quot; for temporary debug logs that should be removed in production. </p>
<p>I sometimes forget some console.log statements, and basically always ignore the TODO/FIXME comments. </p>
<p>So i made a very simple script that fails if there are TODOs, FIXMEs or console.log in your code. This way the PR won&#39;t look like it&#39;s passing if it&#39;s missing an important part.</p>
<p>Save it in .github/workflows/check_todos.yml</p>
<pre><code>name: Look for todos and fixmes
on:
  pull_request:
    branches:
      - master
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: |
         if grep --exclude-dir={.git,.github} -rE &quot;TODO|FIXME|console\.log&quot;; then
              exit 1
         else
             echo &quot;All good&quot;
         fi
</code></pre>
<p>I exclude .git because it could contain the text. And I exclude .github so that the regex doesn&#39;t match itself. Package files should not be a problem as the script doesn&#39;t pull any external dependencies (no npm install)  </p>
<p>The search is case sensitive, so it will only trip for clear TODOs and not some random method names.</p>
<p>You get the results really quickly, and because only the source is checked out ,all the generated files/packages should be excluded from the search.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[One liner to watch the current folder and tree its structure]]></title>
            <link>https://lecaro.me/20210605-one-liner-to-watch-the-current-folder-and-tree-its-structure.html</link>
            <guid>https://lecaro.me/20210605-one-liner-to-watch-the-current-folder-and-tree-its-structure.md</guid>
            <pubDate>Fri, 04 Jun 2021 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>One liner to watch the current folder and tree its structure</h1>
<p>I&#39;ve been working with smstools3 which uses a folder and its subfolder as its state store. </p>
<p>Instead of running &quot;tree&quot; manually in the /var/spool/sms folder, i&#39;d rather have it be done automagically whenever the files change.</p>
<p>I&#39;ve used inotifywait for that, which needs to be installed on ubuntu with </p>
<pre><code>sudo apt install inotify-tools .
</code></pre>
<p>Then it&#39;s just a matter of cd-ing to the proper directory and running :</p>
<pre><code>clear &amp;&amp; tree &amp;&amp; inotifywait --monitor --quiet --recursive --event modify . | while read; do clear; tree; done
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Picnic: end-to-end encrypted collaborative text editor]]></title>
            <link>https://lecaro.me/20220110-picnic-end-to-end-encrypted-collaborative-text-editor.html</link>
            <guid>https://lecaro.me/20220110-picnic-end-to-end-encrypted-collaborative-text-editor.md</guid>
            <pubDate>Sun, 09 Jan 2022 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Picnic: end-to-end encrypted collaborative text editor</h1>
<p>I felt like having a small break from my main project this month, so I&#39;m giving myself some weeks to try out other ideas I&#39;ve been putting off. Just happy coding of open source MVPs for various ideas I&#39;ve had for a while. I&#39;m giving myself a max of 7 days per app to make sure the scope stays reasonable.</p>
<p>The first one is pretty close to done now, and it&#39;s called picnic : <a href="https://txt.lecaro.me/">https://txt.lecaro.me/</a></p>
<p>It&#39;s a simple web app that lets you create a plain text document and edit it with friends in real time (on PC and smartphone).</p>
<p>It&#39;s end-to-end encrypted, so the client side does the encryption with window.crypto (despite not really adding much security) and sends that to my node server over web sockets. The server saves that document and forwards it to the other clients that can decode it thanks to the key stored in the URL. &quot;Rolling your own crypto&quot; is generally a bad idea, but I think here i&#39;m &quot;using pre-made crypto&quot; which is alright, despite a very strong feeling on &quot;I have no idea what I&#39;m doing&quot;.</p>
<p>Speaking of haphazardly reaching a decent solution : syncing edits is hard. When you type some words in the text, the full value of that text is sent over to the server, who forwards it to everyone as-is. So each client receives the latest version, but might have done its own edits that are not yet sent for syncing (two people typing at the same time).</p>
<p>I end up doing a three-way merge of full text, using a homemade, quite naive approach. It works  most of the time, though it quite easily gets confused. Initially, just typing alone on the document would get super messy when the updates from the server would conflict with your local edits.</p>
<p>My 3 way merge algorithm performs well when the edits are far from each other, but it performs terribly when two users are adding a thing at the same spot in the doc. Luckily, this only really happen when a user tries to apply his own edits. So I just made the editor ignore any edits that came from itself (using a session ID that only lasts for the duration of the web page). </p>
<p>Furthermore, some edits would arrive in the wrong order, and my algorithm would consider a reversed addition as a deletion. I could solve that quite easily too, by just adding a version number to each document, incrementing it at each edit, and ignoring stale edits. The server keeps the authoritative value for the current version number and blocks any stale edit form propagating. </p>
<p>Finally, the app reconnects with exponential back-off and retries to send user&#39;s edits whenever the sync fails for any reason, until it&#39;s up-to-date with the server. Overall, I&#39;m quite pleased with the result, it&#39;s usable, loads fast and more reliable than my previous attempt at solving the same project. It&#39;s also quite easy to self-host as the DB is simply the local file system.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2023 update]]></title>
            <link>https://lecaro.me/20230120-2023-update.html</link>
            <guid>https://lecaro.me/20230120-2023-update.md</guid>
            <pubDate>Thu, 19 Jan 2023 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>2023 update</h1>
<p>In 2022 in got a new job at Della AI and moved to Nantes. I focused less on ciboulette.net than before, and had less time for side project. </p>
<p>Working at Della has been interesting. I took over a React fronted (with Redux that i&#39;m slowly getting rid of). I had to learn typescript, but in the end I don&#39;t fully exploit it. It helps avoid mistakes, but also forces you to write code that looks more like java than proper Javascript. I had never seen so many enums in a codebase. </p>
<p>Anyway, I&#39;m quite happy with the way things are going, and our company has been bought out by a bigger one, Legisway. I visited their offices and they are neat !</p>
<p>On the hacking side, over the Christmas/New year vacations I started a small game project. It&#39;s a chilled out city builder, made in web tech. You can have a look at it here : <a href="https://drmp.lecaro.me/">https://drmp.lecaro.me/</a> (there&#39;s no good name for it yet). </p>
<p>That&#39;s all for today, I had to dig up this website from an old save to publish something new on my new computer. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Integrating remote services in e2e tests]]></title>
            <link>https://lecaro.me/20230124-integrating-remote-services-in-e2e-tests.html</link>
            <guid>https://lecaro.me/20230124-integrating-remote-services-in-e2e-tests.md</guid>
            <pubDate>Mon, 23 Jan 2023 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Integrating remote services in e2e tests</h1>
<p>Ciboulette.net has a large battery of tests that ensure I don&#39;t break too many things when doing a deployment. The app also connects together many services, like google maps, google custom search, Stripe and Sumup.</p>
<p>I wanted to make sure those integrations would keep working, so i embedded the real services in my test. For example, playwright would attempt to connect to a test account at sumup when running the e2e test.</p>
<p>Recently, sumup changed their login flow, and added a captcha. I don&#39;t see how I can work around this. The tests are a bit flaky for a few other reasons anyway, so i think i&#39;ll just run them by hand and complete the captcha every time.</p>
<p>Still, i&#39;m sure there&#39;s a better way. I tried to just copy the result of the authentication request but this only worked for a single run of the tests. During the next run Sumup would refuse them for some reason.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024 update]]></title>
            <link>https://lecaro.me/20240508-2024-update.html</link>
            <guid>https://lecaro.me/20240508-2024-update.md</guid>
            <pubDate>Tue, 07 May 2024 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>2024 update</h1>
<p>Long time no see ! I haven&#39;t posted much in the last year, mostly because I couldn&#39;t find a way to edit the site. Publii can be confusing at times. </p>
<h2>Summary of 2023</h2>
<p>I spent the first half of 2023 working for Della, and making a small project (<a href="https://drmp.lecaro.me/">https://drmp.lecaro.me/</a>).</p>
<p>I then resigned from Della and went to do the Camino de Santiago with a friend in September. We walked for 30 days in spain, 
it was great, not really challenging. Here are my main tips if you want to do that trip : bring sandals and a hat (the path is easy but it gets hot),
don&#39;t reserve ahead of time (the best places don&#39;t give reservations), take your time (we went too fast), don&#39;t worry too much about bed bugs (we didn&#39;t encounter any).</p>
<p>Then, I moved to Créteil and started programming. I first focused on making <a href="https://sms.lecaro.me/">https://sms.lecaro.me/</a> as a standalone app that lets you program sms sending from your phone. 
It&#39;s much simpler than the first app I made to let ciboulette do it. I recognized that my android programming skills are low, and so i should keep most of the 
logic on the server.</p>
<p>I worked on many other aspects of ciboulette to improve its performance and modernise the codebase.</p>
<h2>Summary of 2024 so far</h2>
<ul>
<li>I spent some time learning rust, and made a project  with it  <a href="https://obm.lecaro.me/">https://obm.lecaro.me/</a>
It kinda works, but i feel like the potential for improvement lies in better algorithms, much more than the use of a fast language.</li>
<li>I spent way too much time playing brigador <a href="https://www.gog.com/en/game/brigador">https://www.gog.com/en/game/brigador</a> </li>
<li>I switched from using one average 15&quot; laptop to having two devices : a powerful desktop and a very light 13&quot; laptop. </li>
<li>I started using syncthing for absolutely everything, this tool rocks</li>
<li>I rooted my old redmi note 7 phone with LineageOS and i love it. It&#39;s so clean, no Xiaomi adware getting in the way. </li>
<li>I learned to cook chinese food, Mapo tofu in particular is amazing</li>
<li>I learned a bit of chinese with Hello Chinese, that app is not bad. </li>
<li>I did some freelancing for <a href="https://heyhosystems.com/">https://heyhosystems.com/</a> and refreshed my rails skills</li>
<li>I just moved this blog off Publii and back to some handcrafted HTLM</li>
</ul>
<h2>Goals for the rest of 2024</h2>
<ul>
<li>make ciboulette.net lower mantenance, to be able to go back to the industry if I want to</li>
<li>get my driver license (I&#39;m 32, it&#39;s about time)</li>
<li>Sell the things I don&#39;t really need</li>
<li>Push one of my previous project further, to hopefully get it to help real people</li>
<li>use more markdown files, less online services</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Introducting NanoSheets, a spreadsheets library in 2.5KB of JS ]]></title>
            <link>https://lecaro.me/20240511-introducting-nanosheets.html</link>
            <guid>https://lecaro.me/20240511-introducting-nanosheets.md</guid>
            <pubDate>Fri, 10 May 2024 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Introducting NanoSheets, a spreadsheets library in 2.5KB of JS</h1>
<p>A while ago I made a small tool called importabular, that would help me import data into ciboulette.net
It&#39;s a spreadsheet like UI with named columns. It was my first attempt at writing a spreadsheet UI and there were some issues. 
It was trying to do too much, and was too specific for one use case, and the lack of virtualization brought some issues. </p>
<p>Today I worked on a new version and I&#39;m quite happy with the result. Given that the project scope and data model are quite different, I picked a new name. 
NanoSheets was free, and puts the emphasis on how light that lib is (2.5Kb minified and gzipped). The other libraries in this domain seem to all be Swiss army behemoth where you first need to turn off 90% of the features. </p>
<p>Anyway, here&#39;s the demo website : <a href="https://nanosheets.lecaro.me/">https://nanosheets.lecaro.me/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Slow deploys of meteor apps are not a fatality]]></title>
            <link>https://lecaro.me/20240517-slow-deploys-not-a-fatality.html</link>
            <guid>https://lecaro.me/20240517-slow-deploys-not-a-fatality.md</guid>
            <pubDate>Thu, 16 May 2024 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Slow deploys of meteor apps are not a fatality</h1>
<p>I&#39;ve been developing a meteor app since 2015 now, and the production deployments have always been quite slow, around 15 minutes. </p>
<p>It used to be because heroku was taking its sweet time. Heroku got too slow and too expensive quite quickly though, and I switched to <a href="https://cloud.digitalocean.com">https://cloud.digitalocean.com</a> + <a href="https://meteor-up.com/">https://meteor-up.com/</a></p>
<p>My upload speed then became the bottleneck, because I lived in the countryside, where DSL is the only option. So I made sure that my CI runner (first circle CI, then github CI) would do the upload of the bundled app for me. </p>
<p>However, the CI workers you get for free aren&#39;t the fastest, they just can&#39;t compete with my desktop workstation on any criteria. I now also have fiber, and so the upload speed is no longer a problem. I&#39;ve switched to running <code>meteor up</code> locally, with great success. </p>
<p>I can now deploy my app in 2 minutes, and by setting the correct meteor up flags, I barely get 10 seconds of downtime after each deploy. It saves me all the trouble that comes with load balancing and rolling deploys when they go wrong. </p>
<p>As a bonus, I can also tag the commit I just deployed with the current date, to be able to easily rollback to a previous version. </p>
<p>I&#39;ve noticed that deploying to my staging instance is faster than the production one. It&#39;s not a big difference, something like two minutes vs two minutes twenty. My staging server is on Hetzner, so I guess my production will go there soon too. </p>
<p>Here&#39;s the production mup.js config, by the way</p>
<pre><code>module.exports = {
  servers: {
    one: {
    	// my server public ip
      host: &#39;46.101.232.182&#39;,
      username: &#39;***&#39;,
      pem: &#39;~/.ssh/***&#39;
    }
  },
  app: {
    name: &#39;ciboulette&#39;,
    path: &#39;../&#39;,
    servers: {
      one: {}
    },
    deployCheckWaitTime: 300,
    buildOptions: {
      serverOnly: true
    },
    env: {
      ROOT_URL: &#39;https://app.ciboulette.net&#39;,
      MONGO_URL: &#39;mongodb://mongodb/meteor&#39;,
      MONGO_OPLOG_URL: &#39;mongodb://mongodb/local&#39;,
      ENV: &#39;PRODUCTION&#39;,
      TZ: &#39;Europe/Paris&#39;
    },
    docker: {
      image: &#39;zodern/meteor:root&#39;,
      prepareBundle: true,
      useBuildKit: true,
      stopAppDuringPrepareBundle: false
    },
    enableUploadProgressBar: true
  },
  mongo: {
    version: &#39;6.0.3&#39;,
    servers: {
      one: {}
    }
  }
  , hooks: {
    &#39;pre.deploy&#39;: {
      localCommand: &#39;npm run build&#39;
    },
    &#39;post.deploy&#39;: {
      localCommand: &#39;git tag -f $(date +%Y-%m-%d)&#39;
    }
  }
};
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[The price of the cloud]]></title>
            <link>https://lecaro.me/20240720-cloud-prices.html</link>
            <guid>https://lecaro.me/20240720-cloud-prices.md</guid>
            <pubDate>Fri, 19 Jul 2024 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>The price of the cloud</h1>
<h2>Hosting my SAAS over 9 years</h2>
<p>I run an app (ciboulette.net) in the cloud. This app generates about 1000 € / month of revenues. I try to run it 
in a way that doesn&#39;t cost too much, while keeping good performance. </p>
<h3>Heroku</h3>
<p>I started by hosting it on Heroku with a paid mLab database. It quickly got expansive, the database in particular. At the time I had barely 6 clients and the hosting was already costing around 40 € / month adn getting laggy.</p>
<h3>Digital ocean</h3>
<p>I switched to Digital Ocean, with a small instance at 5€ per month I could run the app much more smoothly. Hosting the db on the same machine as the app itself got rid of most of the lag. </p>
<p>I couldn&#39;t have made that switch without the help of <a href="https://meteor-up.com/">meteor up</a>, an amazing free tool that helps you deploy meteor apps very easily.</p>
<p>For backups, I just have a cron on my dev machine that ssh to production and pulls a mongo dump every day. They are about 650MB each now, and I&#39;ve upgraded the production instance to &quot;premium intel&quot; : 2 vCPUs, 4GB ram, 80 GB SSD storage. It costed 28$ / month. </p>
<p>I host static files on an s3-like &quot;spaces&quot; bucket that cost 5$ more per month. So in total i got good performance for 30 € / months</p>
<h3>Hetzner</h3>
<p>I heard that <a href="https://www.hetzner.com/cloud/">hetzner</a> could be a bit cheaper than digital ocean, so I opened an account there. I was happily surprised, it is indeed better value. I use two cx32 machines, one for prod and one for staging. They each cost 7.56€/month, with 4 vcpu and 8gb ram, basically double the power of the DigitalOcean machine for less than a third the price.</p>
<h2>Benchmark</h2>
<p>I thought it would be interesting to compare cloud and physical machines, here are the result, and below are the details</p>
<table>
<thead>
<tr>
<th>Machine</th>
<th align="center">CPU</th>
<th>RAM</th>
<th>Sysbench</th>
<th align="center">MD5</th>
<th align="center">Idle</th>
<th align="center">Peak</th>
<th align="center">Initial price</th>
<th>Monthly cost (idle)</th>
</tr>
</thead>
<tbody><tr>
<td>Dell 7010 SFF</td>
<td align="center">i7-13700</td>
<td>15Gi</td>
<td>66315 op/s</td>
<td align="center">2s</td>
<td align="center">30 W</td>
<td align="center">215 W</td>
<td align="center">1600 € new</td>
<td>5.40 €</td>
</tr>
<tr>
<td>Lenovo M700</td>
<td align="center">i5-6400T</td>
<td>15Gi</td>
<td>(absurd)</td>
<td align="center">22s</td>
<td align="center">10 W</td>
<td align="center">30 W</td>
<td align="center">55 € used</td>
<td>1.80 €</td>
</tr>
<tr>
<td>Hetzner cx32</td>
<td align="center">4 x vcpu</td>
<td>7.6Gi</td>
<td>3362 op/s</td>
<td align="center">17s</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td>7.56 €</td>
</tr>
<tr>
<td>Mini forum</td>
<td align="center">N4020</td>
<td>3,6Gi</td>
<td>3172 op/s</td>
<td align="center">38s</td>
<td align="center">6 W</td>
<td align="center"></td>
<td align="center">175 € new</td>
<td>1.08 €</td>
</tr>
<tr>
<td>DigitalOcean</td>
<td align="center">2 x vcpu</td>
<td>3,6Gi</td>
<td>1987 op/s</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td>23.89 €</td>
</tr>
<tr>
<td>Heroku</td>
<td align="center">(8x8375C)</td>
<td>(61Gi)</td>
<td>(7159 op/s)</td>
<td align="center">(11s)</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td>(7 $)</td>
</tr>
</tbody></table>
<p>Note that the heroku machine does not match the specs you pay for at all, it should have 0.5GB of ram not 64GB, as explained below. </p>
<h3>Power consumption</h3>
<p>I would like to compare cloud machines with computers I physically own, so cpu tests on both of them. I measure the electrical consumption of the machine, both while performing the test and idle. Realistically, the machine is mostly idle but needs to be fast enough to work in the day. </p>
<p>The monthly price was computed this way for physical devices : </p>
<pre><code>monthly_cost = power consumed idle (watts) * 24 (hours per day) * 30 (days per month) / 1000 (Kw per w) * 0.25 (€/kwh price in france)
</code></pre>
<p>For cloud devices, the price is simply the one demanded by the hosting service</p>
<h3>Ram and Processor info</h3>
<p>I got the ram with this command</p>
<pre><code>~ $ free -h
               total        used        free      shared  buff/cache   available
Mem:            61Gi        30Gi       2.0Gi        67Mi        30Gi        31Gi
Swap:          121Gi       1.2Gi       120Gi
</code></pre>
<p>I then took the total Mem size</p>
<p>For cpus, I ran </p>
<pre><code>cat /proc/cpuinfo | grep &quot;model name&quot;
</code></pre>
<h3>sysbench benchmark</h3>
<p>I ran two tests on each machine, the first one is sysbench</p>
<pre><code>sudo apt install sysbench
sysbench cpu run --threads=16
</code></pre>
<p>The result would look like the code below, &quot;events per second&quot; is the result, higher is better</p>
<pre><code>sysbench cpu run --threads=16
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)

Running the test with following options:
Number of threads: 16
Initializing random number generator from current time


Prime numbers limit: 10000

Initializing worker threads...

Threads started!

CPU speed:
    events per second: 11162.86

General statistics:
    total time:                          10.0009s
    total number of events:              111655

Latency (ms):
         min:                                    0.33
         avg:                                    1.43
         max:                                  160.65
         95th percentile:                        4.49
         sum:                               159767.60

Threads fairness:
    events (avg/stddev):           6978.4375/1210.28
    execution time (avg/stddev):   9.9855/0.01
</code></pre>
<p>To test most devices, I just logged in by ssh and ran the commands. </p>
<h3>MD5 benchmark</h3>
<p>My M700 gave an absurd result for sysbench because I ran freebsd on it, and the compiler for that target optimized away most of the benchmark code. That&#39;s what pushed me to add a second benchmark test that&#39;s easier to run on any pc (the MD5 hashing).</p>
<pre><code># MD5 benchmark
time (for i in {1..16}; do (dd if=/dev/zero bs=1M count=1512 2&gt;/dev/null | md5sum &gt; /dev/null) &amp; done; wait)
</code></pre>
<p> You should check the &quot;real&quot; line in the output, lower is better</p>
<pre><code>real	0m2.121s
user	0m28.930s
sys	0m11.597s
</code></pre>
<h3>Heroku testing</h3>
<p>For heroku, I used the &quot;heroku run&quot; command, but the results are suspiciously good. 
I suspectrf this kind of command gets a beefier dyno assigned that any regular process. </p>
<pre><code>mkdir benchmark
cd benchmark
git init
heroku create benchmark
heroku git:remote -a benchmark
echo &quot;web: bash&quot; &gt; Procfile
echo &quot;python-3.11&quot; &gt; runtime.txt
echo &quot;sysbench&quot; &gt; Aptfile
heroku buildpacks:add --index 1 https://github.com/heroku/heroku-buildpack-aptck-apt
git add .
git commit -m &quot;Initial commit&quot;
git push
heroku run bash
sysbench cpu run --threads=16
</code></pre>
<p>So I then changed my approach and used the procfile directly.</p>
<p>Procfile </p>
<pre><code>web: bash bench.sh
</code></pre>
<p>bench.sh</p>
<pre><code>free -h
cat /proc/cpuinfo | grep &quot;model name&quot;
time (for i in {1..16}; do (dd if=/dev/zero bs=1M count=1512 2&gt;/dev/null | md5sum &gt; /dev/null) &amp; done; wait)
sysbench cpu run --threads=16
</code></pre>
<p>This still showed very hight ram and CPU</p>
<p>heroku logs -t</p>
<pre><code>[web.1]: total        used        free      shared  buff/cache   available
[web.1]: Mem:            61Gi        26Gi       4.5Gi       111Mi        31Gi        35Gi
[web.1]: Swap:          121Gi       175Mi       121Gi
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: model name	: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
[web.1]: real	0m8.683s
[web.1]: user	0m38.177s
[web.1]: sys	0m12.991s
[web.1]: sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
[web.1]: 
[web.1]: Running the test with following options:
[web.1]: Number of threads: 16
[web.1]: Initializing random number generator from current time
[web.1]: 
[web.1]: 
[web.1]: Prime numbers limit: 10000
[web.1]: 
[web.1]: Initializing worker threads...
[web.1]: 
[web.1]: Threads started!
[web.1]: 
[web.1]: CPU speed:
[web.1]: events per second: 12035.49
[web.1]: 
[web.1]: General statistics:
[web.1]: total time:                          10.0012s
[web.1]: total number of events:              120387
[web.1]: 
[web.1]: Latency (ms):
[web.1]: min:                                    0.33
[web.1]: avg:                                    1.33
[web.1]: max:                                  168.64
[web.1]: 95th percentile:                        2.39
[web.1]: sum:                               159648.02
[web.1]: 
[web.1]: Threads fairness:
[web.1]: events (avg/stddev):           7524.1875/1082.56
[web.1]: execution time (avg/stddev):   9.9780/0.02
</code></pre>
<p>The specs of the web &quot;basic dyno&quot; is 500MB ram one &quot;VCPU&quot;, but it seems like we are getting way more than that here. There&#39;s something wrong with my method for Heroku. Let me know (<a href="mailto:renan@lecaro.me">renan@lecaro.me</a>) if you have an idea why that is so. </p>
<h2>Taking a look at meteor cloud</h2>
<p>My favorite framework has a <a href="https://www.meteor.com/cloud">cloud hosting offering</a>, this is how they finance the development of the open source framework, and it shows. Once you pass the free usage threshold, the <a href="https://www.meteor.com/cloud/pricing">pricing</a> looks scary  : a &quot;professional&quot; container with 2 GB of ram and 2 &quot;ECU&quot;, whatever that is, costs 158 $ / months. Instead of 5$. </p>
<h2>Summary</h2>
<p>1€ / month of electricity can run a production app on premise, with an initial investment for the server.</p>
<p>10€ / month lets you rent a server from Hetzner with similar performance and greater stability. </p>
<p>100€ / month lets you pay for other people&#39;s free tier and &quot;not worry about scaling&quot; with things like meteor cloud / heroku / aws</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Improving my i3 keyboard shortcuts for daily notes]]></title>
            <link>https://lecaro.me/20240821-improving-my-writing-aliases.html</link>
            <guid>https://lecaro.me/20240821-improving-my-writing-aliases.md</guid>
            <pubDate>Tue, 20 Aug 2024 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Improving my i3 keyboard shortcuts for daily notes</h1>
<p>For a while now, i&#39;ve had those two lines in my i3 config</p>
<pre><code>bindsym $mod+J exec subl  ~/Documents/Journal/$(date +%Y%m%d).md
bindsym $mod+B exec subl  ~/projects/lecaro.me/src/$(date +%Y%m%d).md
</code></pre>
<p>They let me quickly start a note or blog post with a simple keyboard shortcut.</p>
<p>However, I&#39;ve found that renaming the file to add a title would make my life easier when scrolling through blog and notes. </p>
<p>So, i&#39;ve replaced it with these lines </p>
<pre><code>bindsym $mod+J exec ~/projects/bins/today.sh ~/Documents/Journal/
bindsym $mod+B exec ~/projects/bins/today.sh ~/projects/lecaro.me/src/
</code></pre>
<p>And added these lines of bash (thanks ChatGPT) in ~/projects/bins/today.sh</p>
<pre><code>#!/bin/bash
# Check if the directory parameter is provided
if [ -z &quot;$1&quot; ]; then
    echo &quot;Usage: $0 /path/to/journal/directory/&quot;
    exit 1
fi
# Set the directory where the journal files are stored
JOURNAL_DIR=&quot;$1&quot;
# Ensure the directory path ends with a slash
JOURNAL_DIR=&quot;${JOURNAL_DIR%/}/&quot;
# Get today&#39;s date in the format YYYYMMDD
TODAYS_DATE=$(date +%Y%m%d)
# Search for a file in the journal directory that starts with today&#39;s date
MATCHING_FILE=$(ls $JOURNAL_DIR$TODAYS_DATE*.md 2&gt;/dev/null | head -n 1)
# Check if a matching file was found
if [[ -n &quot;$MATCHING_FILE&quot; ]]; then
    # Open the matching file with Sublime Text
    subl &quot;$MATCHING_FILE&quot;
else
    # If no matching file is found, open the standard file
    subl &quot;${JOURNAL_DIR}${TODAYS_DATE}.md&quot;
fi
</code></pre>
<p>Now, if I rename the file (but keep the prefix), it will still open when pressing window+B or window+J later that day.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[No spyware, no ads, no bloat]]></title>
            <link>https://lecaro.me/20241027-spyware-bload-and-ads.html</link>
            <guid>https://lecaro.me/20241027-spyware-bload-and-ads.md</guid>
            <pubDate>Sat, 26 Oct 2024 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>No spyware, no ads, no bloat</h1>
<p>When shopping, the products pushed forward are usually bad value. You have to dig a little to find the good stuff. In software, even more so. Here&#39;s a summary of what works for me.</p>
<h2>Operating system</h2>
<p>I started on OG Windows 98. I discovered Fedora in my first internship at university, in the Windows 7 era. I adopted Ubuntu on my dual boot laptop, then only used ubuntu, then got tired of the snap apps taking forever to start.</p>
<p>I&#39;m currently using <a href="https://www.debian.org/">Debian</a>. Always using old-ish sofware can be a bit annoying but I enjoy the stability and quick apps startup. I use <a href="https://i3wm.org/">i3</a> as a window manager.</p>
<p>I&#39;ve tried <a href="https://www.freebsd.org/">FreeBSD</a> for Desktop computing and I&#39;m considering doing the switch eventually, to have a simpler OS that I better understand. The documentation is great and it&#39;s a chance to finally learn standard Unix tools from scratch. </p>
<h2>Browser</h2>
<p>I used to be a big fan of Chrome. I now avoid it like the plague. </p>
<p>I switched fully to <a href="https://www.mozilla.org/en-US/firefox/new/">firefox</a> on both desktop and mobile when I realized that firefox mobile supported <a href="https://ublockorigin.com/">ublock</a>. My ad blocker works well, firefox syncs my passwords and the web is fast. I use <a href="https://duckduckgo.com/">duck duck go</a> for search.  </p>
<p>Firefox has been going in the <a href="https://www.mozilla.org/en-US/advertising/">wrong direction</a> though. I&#39;m looking at ways to extract the password management with <a href="https://www.passwordstore.org/">pass</a>, to be able to use a browser that dislikes ads as much as I do, something like <a href="https://librewolf.net/">LibreWolf</a>. </p>
<h2>Code Editor / Git Hosting / Project tracking</h2>
<p>I&#39;ve started with Dreamweaver, notepad and notepad++. I relied on github to host my code.</p>
<p>Nowadays, I&#39;m a happy paying user of <a href="https://www.sublimetext.com/">sublime text</a> and <a href="https://www.jetbrains.com/">jetbrain</a> products. I host my git repos on my VPS (hetzner). I edit my translation files with <a href="https://www.codeandweb.com/babeledit">BabelEdit</a>. I&#39;m still using trello for some project tracking, but more and more I use the Readme of the project as a todo list instead. </p>
<p>I&#39;d like to use <a href="https://fossil-scm.org/">Fossil</a> for versionning and project tracking but I haven&#39;t made the jump yet. </p>
<h2>Web Hosting / App hosting</h2>
<p>I used to recommend wordpress for websites, then moved to heroku for web apps, then discovered that my apps ran much faster on DigitalOcean. I used to host my websites on github pages, then cloudflare pages. </p>
<p>All my project are now hosted on <a href="https://www.hetzner.com/">hezner</a> heroku instance. I have set up <a href="https://nginx.org/en/">nginx</a> to automatically serve folders in my www directory to [folder_name].lecaro.me. Some <a href="https://www.docker.com/">Docker</a> containers are managed by <a href="https://meteor-up.com/">mup</a> and run node and <a href="https://www.meteor.com/">meteor</a> apps. SSL certificates are provided by <a href="https://letsencrypt.org/">LE</a>, wich has been a bit troublesome as they broke compatibility with devices running android 7 and older in the default browser. </p>
<p>I&#39;m very happy with that setup, then only thing I might change would be to switch to FreeBSD for the OS and use a VPN to connect to the machine&#39;s private web services instead of exposing their interface publicly. </p>
<h2>Email / Notes / Photos / Backups</h2>
<p>I used to rely on google mail, google keeps, google photos and google drive. </p>
<p>I switched my email to hostinger and <a href="https://syncthing.net/">Syncthing</a> takes care of the rest : notes are now a bunch of md files edited with <a href="https://play.google.com/store/apps/details?id=com.rhmsoft.edit.pro">QuickEdit+</a>, photos are synced between my computer and phones whenever both are on wifi, and my &quot;travel laptop&quot; gets synced to my main workstation before every trip, which also serves as backup. </p>
<p>I will self host my email server because the service i currently pay for is disappointing, constantly interrupting me to push more features I don&#39;t need. For backups, I&#39;d like to setup a server offsite with encrypted backups.</p>
<h2>Games</h2>
<p>I used to dual boot for games, then game on linux, but now I have a dedicated windows 10 mini pc just for gaming.</p>
<p>I get my games from <a href="https://www.gog.com/en/">GOG</a> and <a href="https://itch.io/">itch.io</a>. I mostly play old offline games, but a few of them are only avaliable through steam, so I have installed that bloatware, I just try to start it as rarely as I can. On mobile,</p>
<p>In the future, once windows becomes too pushy, I&#39;ll probably disconnect that pc from the net, load DRM free games using a usb key, and remove steam entirely.  </p>
<h2>Messaging, social networks, news</h2>
<p>Whatsapp is sadly still needed, but I try to use signal whenever possible. I only use facebook to have a presence there for ciboulette.net, same thing for instagram. I&#39;ve got a <a href="https://f-droid.org/en/packages/org.joinmastodon.android/">mastodon</a> account and I read my news using <a href="https://f-droid.org/packages/com.nononsenseapps.feeder/">Feeder</a>. </p>
<p>I&#39;m considering building an E2E encrypted social network at some point, as a toy project, and I also have a syncthing / markdown based news reader project that I would like to improve. </p>
<h2>Android, maps, calendar, AI</h2>
<p>I&#39;ve flashed a <a href="https://lineageos.org/">LineageOS</a> rom on my Redmi Note 7, and enjoy a very clean and responsive system. I look for apps on <a href="https://f-droid.org">F-Droid</a> when I can and my  <a href="https://f-droid.org/en/packages/ademar.textlauncher/">Text Launcher</a> is super nice. </p>
<p>I&#39;m still using some google products though. I pay for youtube premium because <a href="https://nebula.tv/">Nebula</a> doesn&#39;t have enough content. I&#39;m still using google maps and calendar. I moved from ChatGPT to <a href="https://chat.mistral.ai">Mistral</a>. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[How to sharpen a knife]]></title>
            <link>https://lecaro.me/20241217-how-to-sharpen-a-knife.html</link>
            <guid>https://lecaro.me/20241217-how-to-sharpen-a-knife.md</guid>
            <pubDate>Mon, 16 Dec 2024 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>How to sharpen a knife</h1>
<p>I love to cook, and I dislike dull knives. I used to like pull sharpener, but the knives don&#39;t stay sharp for long with them. </p>
<p><a href="https://www.youtube.com/@OUTDOORS55">OUTDOORS55</a> opened my eyes in his very <a href="https://www.youtube.com/watch?v=pagPuiuA9cY">basic video</a> </p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/pagPuiuA9cY?si=D4K8g_axqPrXkFDX" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


<p>I went on to buy a very cheap <a href="https://fr.aliexpress.com/item/1005005411383541.html">400 grit diamond stone</a> for 3.87€ and a <a href="https://fr.aliexpress.com/item/1005002947558808.html">polishing leather and paste</a> for 4.64€. </p>
<p>It took me a bit of getting used to, but I now have very sharp knives and they stay sharp. You don&#39;t need to add water, the diamon stone should stay dry.</p>
<p>What&#39;s a bit sad is that an expert like that guy really just needs to make one video, clearly explaining how to get a normal knife sharp. If he wants to make a living though, he&#39;ll have to constantly push new videos that aren&#39;t really needed, presenting products that we clearly could do without. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[breakout clone and small apks]]></title>
            <link>https://lecaro.me/20250106-a-breakout-game-apk-in-38kb.html</link>
            <guid>https://lecaro.me/20250106-a-breakout-game-apk-in-38kb.md</guid>
            <pubDate>Sun, 05 Jan 2025 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>breakout clone and small apks</h1>
<h2>Breakout v1 : strategic but complicated</h2>
<p>I find the concept of the breakout game fascinating. It&#39;s so simple and open, and can be extended in all sorts of directions. </p>
<p>It also helps that it&#39;s visually intersting, non violent and doesn&#39;t make my girlfriend sick as 3D games do. We spent a lot of time playing with the excellent <a href="https://lgames.sourceforge.io/LBreakoutHD/">LBreakoutHD</a> this summer and I really wanted to make a version of my own.</p>
<p><img src="breakout_v1_screenshot.webp" alt="Breakout v1 screenshot"></p>
<p>My <a href="https://breakout-v1.lecaro.me/">first attempt</a> sticked to the classic formular, where the ball bounces around and drops fall slowly to be picked up by the player. I tried to make most drop a real choice : each negative status effect would simultaneously boost the score multiplier.</p>
<p>I added many different drop types, increasing the density of drops over time as the user progresses past many levels. I decided to also reveal drops from the start, instead of randomly spawning some when the brick broke, to encourage strategic play. </p>
<p>I found this a bit too complex and not very approachable for beginners, requiring a lot of explanation. </p>
<h2>Breakout v2 : you phone is the controller</h2>
<p><img src="breakout_v2_screenshot.webp" alt="Breakout v2 screenshot"></p>
<p>My <a href="https://breakout-v2.lecaro.me/">second attempt</a> simplified the game mechanic (no more status effects, only simple coin drops) but enabled split screen play. You could play 2 players with a keyboard, or remote control the puck using your phone as a game pad, <a href="https://www.airconsole.com/play/">AirConsole</a>-style.  </p>
<h2>Breakout v3 : coins explosions</h2>
<p><img src="breakout_v3_screenshot.webp" alt="Breakout v3 screenshot"></p>
<p>The <a href="https://breakout.lecaro.me/">third version</a> focuses on game feel and &quot;juice&quot;. I added more brick colors to enable more interesting levels. Each color makes a different note when hit, so the ball bouncing around plays a sort of melody. I made sure that each level would spawn plenty of points (500 coins per level). </p>
<h2>Wrapping a web app in a very small APK</h2>
<p>I found that the browser experience was a bit inconsistent, particularily around fullscreen behaviour and sounds. Therefore, I wanted to wrap this very small game (66kb, 22kb compressed) into an equally small APK file. </p>
<p><a href="https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md">This tutorial</a> was very helpful, but a bit out of date, as I also needed to comment out the <code>compose = true</code> bit in <code>build.gradle.kts</code>. </p>
<p>I made an vector icon out of a small svg add added a full screen webview that displays the content of <code>app/src/main/assets</code></p>
<pre><code>package me.lecaro.breakout
import android.os.Bundle
import android.util.Log
import android.view.Window
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
import android.webkit.WebView
class MainActivity : android.app.Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        window.setFlags(
            WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN
        );
        val webView = WebView(this)
        webView.settings.javaScriptEnabled = true
        webView.settings.domStorageEnabled = true
        webView.loadUrl(&quot;file:///android_asset/index.html&quot;)
        webView.webChromeClient = object : WebChromeClient() {
            override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
                Log.d(
                    &quot;WebView&quot;, &quot;${consoleMessage.message()} -- From line &quot; +
                            &quot;${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}&quot;
                )
                return true
            }
        }
        setContentView(webView)
    }
}
</code></pre>
<p>All in all, the file size is now really reasonable, adding 10kb to the web version : </p>
<p><img src="breakout_v3_apk_size.webp" alt="Size analysis"></p>
<p>You can find the apk file here : <a href="https://renanlecaro.itch.io/breakout71">https://renanlecaro.itch.io/breakout71</a></p>
<h2>Adding some bells and wistles</h2>
<p>After 3 months of iterating over the app, the kotlin code is still just one tiny file : <a href="https://gitlab.com/lecarore/breakout71/-/blob/main/app/src/main/java/me/lecaro/breakout/MainActivity.kt?ref_type=heads">https://gitlab.com/lecarore/breakout71/-/blob/main/app/src/main/java/me/lecaro/breakout/MainActivity.kt?ref_type=heads</a></p>
<p>I added support for downloading and uploading save files, and gameplay videos.</p>
<p>The game itself has grown significantly. Because it bundles translations for multiple languages, and that each translation takes a bit of space, we&#39;ve reached an index.html file of 500kb, compressed to 200kb in an apk. </p>
<p>This is still tiny by apks standard, but not as small as I wish it was.</p>
<p>I chose to bundle everything in one html file to allow players to open that file offline on their desktop computer, and to make it easy to share. It does mean thaugh that every language file needs to be bundled and parsed before the game can run, which is a bit suboptimal. </p>
<p>Still, the game is very fun, very light, runs on tons of devices, and weights almost nothing. Posting it on F-droid was a good idea, this is where I got most of my users and even a bit of funding. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Sublime plugins are surprisingly easy to write]]></title>
            <link>https://lecaro.me/20250106-sublime-plugins-are-surpisingly-simple-to-write.html</link>
            <guid>https://lecaro.me/20250106-sublime-plugins-are-surpisingly-simple-to-write.md</guid>
            <pubDate>Sun, 05 Jan 2025 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Sublime plugins are surprisingly easy to write</h1>
<p>I have i3 keyboard shortcuts to write a new blog entry or note</p>
<p><code>~/.config/i3/config</code></p>
<pre><code>bindsym $mod+J exec ~/projects/bins/today.sh ~/Documents/Journal/
bindsym $mod+B exec ~/projects/bins/today.sh ~/Documents/lecaro.me/src/
</code></pre>
<p><code>Mod + J</code> opens today&#39;s journal entry, and <code>Mod + B</code> opens a blog post. I find it very comvenient, and even use it to open sublime and then look at some other files.</p>
<p>The issue with that is that the buffer is kept as an empty tab, and sublime asks me if I want to save it when I try to close it. I created a small plugin to stop asking to save empty buffers for files that don&#39;t exist on disk. All I needed to do was to extend <a href="https://www.sublimetext.com/docs/api_reference.html#sublime_plugin.EventListener">sublime_plugin.EventListener</a> in a python file placed in the <code>~/.config/sublime-text/Packages/User/</code> directory. Mistral AI struggled to follow my prompt so I ended up writing it by hand.</p>
<p><code>~/.config/sublime-text/Packages/User/dont_prompt_save_empty.py</code></p>
<pre><code class="language-python">
import sublime
import sublime_plugin
import os.path

class NoSaveEmptyTabsListener(sublime_plugin.EventListener):
    def on_load_async(self, view):
       if view.file_name() and not os.path.isfile(view.file_name()): 
          view.set_scratch(True)

    def on_init(self, views):
      for view in views:
         if view.file_name() and not os.path.isfile(view.file_name()): 
             view.set_scratch(True)

    def on_modified_async(self, view):
       if view.is_scratch() and view.size() and view.file_name():
          view.set_scratch(False) 
</code></pre>
<p>Edit on 2025-01-14 : added the on_init part to also mark empty documents as scratch when sublime starts up.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Never buy Xiaomi phones]]></title>
            <link>https://lecaro.me/20250429-never-buy-xiaomi.html</link>
            <guid>https://lecaro.me/20250429-never-buy-xiaomi.md</guid>
            <pubDate>Mon, 28 Apr 2025 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Never buy Xiaomi phones</h1>
<p>I&#39;ve bought a Xiaomi phone (Redmi Note 7) that was always a bit annoying (ads, spyware, random installs), until I finally managed to unlock the bootloader and install a pretty clean ROM. </p>
<p>So naturally, when that device had an defective battery, I sent it to the repair shop while buying myself a nicer, more modern Xiaomi Phone, the Redmi note 12 Pro. It even had NFC. </p>
<p>I thought I would just unlock it as the previous one, flash a usable ROM and be happy. The unlock process didn&#39;t really work, I though it was because the phone was brand new, so I tried to wait a few month. My old phone was working fine anyway, and sikly smooth despite being a budget phone from a few years back. It&#39;s funny how little processing power you need to run clean software.</p>
<p>When developing <a href="https://breakout.lecaro.me/">Breakout 71</a>, I made sure to try the game on as many devices as possible, so I got that note 12 out of the box, and it&#39;s indeed a really nice phone and very capable. Sadly, when trying to unlock it again, i&#39;ve been confronted to all the same dark patterns you&#39;d expect from a company that makes ad supported cheap devices : they don&#39;t want you to clean the phone. </p>
<p>You first need to figure out the steps, because nothing is explained. All software (windows unlock tool, mi community app) is out of date and needs to be pulled from unknown sources. </p>
<p>You need to create a xiaomi account, wait 30 days, sign up for &quot;mi comunity&quot;, configure your account to &quot;global&quot;, click a button to &quot;unlock your booloader&quot; in their forum app (wtf) and then be told, every day, that the &quot;quota&quot; of people that got their request accepted has been reached. The company literaly pits its clients against each other for the privilege of truely owning what they bought. </p>
<p>It&#39;s nasty. Chinese users are even forced to &quot;level up&quot; their account by participating in the forums before they can ask to unlock their phones. </p>
<p>Next time, i&#39;ll just buy something useable from the start. Phones are so good those days, you don&#39;t need to spend more than 200€ to get a solid machine, I spent 400€ on that junk note 12 and i&#39;ll not do that mistake twice. </p>
<p>Keep your 120FPS advertising hell xiaomi, i&#39;d rather use a phone that I truely own.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Beautiful software]]></title>
            <link>https://lecaro.me/20250528-beautiful-software.html</link>
            <guid>https://lecaro.me/20250528-beautiful-software.md</guid>
            <pubDate>Tue, 27 May 2025 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Beautiful software</h1>
<p>Beautiful software starts simple and focused on the main use case. Labels are short, screens are structured, important buttons stand out. For command line interfaces, the important options are obvious and there are good defaults.</p>
<p>Behind the surface however, beautiful software is capable and configurable. In the settings you will find the extra features you need, and turn them on in a click. They are not upsells of course.</p>
<p>Beautiful software is polished. What doesn&#39;t work well is refined until it does. Only once the known bugs are fixed and the performance is good will new features be added.</p>
<p>Features will wait for you to need them and not jump in you face as soon as they&#39;ve been rushed to production. User happiness is the metric, not feature adoption.</p>
<p>The team behind good software is small, focused on one product and directly involved with users. Each team member understands every major aspect of the products and can do anything in the stack.</p>
<p>Ugly software wins contracts, beautiful software wins hearts. Because it&#39;s not as focused on extracting money from users as the competition, is is less profitable, and therefore looses auctions for ad clicks.</p>
<p>Beautiful software doesn&#39;t come to you, you have to look for it. I need more beautiful software in my life.</p>
<p>Written in ghostwriter.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[New project: Kanban md]]></title>
            <link>https://lecaro.me/20251008-kanbanmd.html</link>
            <guid>https://lecaro.me/20251008-kanbanmd.md</guid>
            <pubDate>Tue, 07 Oct 2025 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>New project: Kanban md</h1>
<p>Born from Atlassian&#39;s annoying behavior, it&#39;s a Kanban board desktop app based on local markdown files. The idea is to have a very simple, offline-only app that lets you open any markdown file as if they were a Trello board.</p>
<h2>Minimal UI</h2>
<p>The app is meant to be used daily and therefore puts the data up front. It is made of three parts. </p>
<p><img src="renan-2025-10-08-08-23-46.jpg" alt="Screenshot of the app"></p>
<p>The <strong>tab bar</strong> at the top lists your opened files. They are listed by title, to avoid having three &quot;Readme.md&quot; there. You can double click a tab to locate the file in the files browser. </p>
<p>The + button lets you add a file as a tab. You could also do ctrl+O. The opened files list is saved, and they are reopened when you reopen the app. </p>
<p>The <strong>search</strong> box filters the cards based on the presence of a keyword, and does not reset when going from one tab to the next</p>
<p><img src="renan-2025-10-08-08-24-12.jpg" alt="Search in the app"></p>
<p>I try to support common keyboard shortcuts and Trello shortcuts.</p>
<ul>
<li>Drag and drop cards and lists to move them</li>
<li>Click a card/list title to edit it</li>
<li>Drag the background to scroll horizontally</li>
</ul>
<h2>Data format</h2>
<p>Each board is one markdown file.</p>
<p>Anything before the first h1 is ignored, so markdown headers will be left untouched. </p>
<p>The first top-level title is the board name. If it is not found, the file name is used as the tab name.</p>
<p>Level two titles are column names. </p>
<p>Level three titles are card names. </p>
<p>Anything immediately following a title is considered its description.</p>
<p>Descriptions can be seen and edited in the card view.</p>
<p><img src="renan-2025-10-08-08-24-28.jpg" alt="Editing a card"></p>
<h2>Technologies</h2>
<p>Currently the app is vanilla JS wrapped in Tauri. I might do a web version at some point, but local file editing
is only supported in Chromium based browsers at the moment. </p>
<p>As most of my work, it&#39;s very lightweight. 30 Kb of js source code, but it becomes a 5 MB Tauri app once bundled. Still lighter than Electron. </p>
<h2>Pricing</h2>
<p>I think I&#39;ll make this app paid but DRM-free. </p>
<p>Something like 20€, money-back guarantee for 7 days. </p>
<p>I prefer to publish software as open source, like I did for Breakout 71 and many other projects, but responding to email,
bug reports and inquiries takes a lot of time. Companies usually open source their complement, not their core business, and my core
skill is making front ends that don&#39;t suck too much.</p>
<p>But at the same time, my plan is to make something very simple, with a limited feature set, that doesn&#39;t need a high price or subscription to justify its existence. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Maybe we don't need a server]]></title>
            <link>https://lecaro.me/20251203-maybe-we-do-not-need-a-server.html</link>
            <guid>https://lecaro.me/20251203-maybe-we-do-not-need-a-server.md</guid>
            <pubDate>Tue, 02 Dec 2025 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Maybe we don&#39;t need a server</h1>
<h2>Syncthing for notes and photos</h2>
<p>Have you heard of syncthing ? It syncs a folder between devices, like Dropbox, without the need for someone else&#39;s computer. No cloud, it&#39;s just your pc talking to your phone, directly and automatically, through your home WiFi.</p>
<p>It&#39;s a near perfect solution to sync your phone&#39;s <strong>photo</strong> with your computer, making google photo nearly redundant. It works great for <strong>text notes</strong> synchronization too, as long as you don&#39;t edit the same file on multiple devices while offline.</p>
<p>This got me thinking, how sensible could personal tech be, if it was <strong>truly serverless</strong> ? </p>
<h2>More things could be just files</h2>
<p>Now, this got me wondering : what if my calendar was just a bunch of text files representing <strong>events</strong>, stored on my drive. What about my mailbox, I could use pop to yank mails out of the server, add them to a list of <strong>email</strong> files that would then sync without the need to store them on the server.</p>
<p>What about my browser ? I use Firefox sync to synchronize my <strong>favorites</strong>, <strong>passwords</strong> and <strong>settings</strong>. Maybe those could just be files, maybe the Firefox profile folder already works when shared between android and Linux.</p>
<p>My <strong>contacts</strong> don&#39;t need to be on my google account. The could just be text files. Google map <strong>location history</strong> is creepy, unless it was only for me.
My <strong>marked places</strong> on the map should be plain text files too.
Of course my <strong>SMS</strong> should be file. </p>
<h2>What about self hosting ?</h2>
<p>The main private alternative to this is to self host a &quot;cloud&quot; service on your own hardware, be it a virtual machine on a VPS or a physical device in your home. In both cases, keeping that device <strong>reachable and secure</strong> creates a <strong>non trivial work load</strong>, requires system administration skills and it&#39;s <strong>difficult to migrate data</strong> from one app to another when the data is in db.</p>
<p>I&#39;ve been sticking to google products for so long because I <strong>don&#39;t want to manage my own server for those things</strong>, nor do I want to switch to a different service that could shut down or enshitify tomorrow. </p>
<p>I wish the &quot;data sync&quot; and &quot;application&quot; parts could be separated. This would lower the chance of my data getting stuck in an app that no longer matches my needs. </p>
<h2>Getting there</h2>
<p>I think that for this to work we would need <strong>plugins</strong> that sync app content with a local folder of text files, in a way that doesn&#39;t break when conflict files are generated, and that generates many small text files instead of one sqlite file for example. </p>
<p>Sometimes we&#39;d need <strong>full rewrite and dedicated app</strong>, for a contacts or calendar app on android for example.</p>
<p>Sometimes we&#39;d use <strong>different apps on mobile and desktop</strong> to work with the same files, like I use <a href="https://kanbanmd.lecaro.me/">kanbanmd</a> on desktop and a text editor on mobile for my kanban boards. </p>
<p>In some case, the sync protocol can bring niceties, like <a href="https://github.com/teamtype/teamtype">Teamtype</a> that offers real time collaborative editing.</p>
<p><a href="https://syncthing.net/">https://syncthing.net/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[About desync]]></title>
            <link>https://lecaro.me/20260212-desync.html</link>
            <guid>https://lecaro.me/20260212-desync.md</guid>
            <pubDate>Wed, 11 Feb 2026 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>About desync</h1>
<p>Two months after writing about how we should have peer to peer data sync for contacts and calendars, I stumbled upon a project called DecSync CC on F-Droid. </p>
<p>It maps data (calendar, contacts, rss, map favorites) to a collections of text files 
specifically arranged to avoid conflicts when synchronizing them.</p>
<p>However, the main dev hasn&#39;t pushed any commit since 2022.<br>He must have gotten tired of maintaining <a href="https://github.com/39aldo39?tab=repositories">alternate storage plugins</a> for radicale, evolution, multiple feed readers .. 
It was a beautiful idea.
But I think it&#39;s an incredibly hard task to keep all sorts of external apps synced this way. </p>
<p>Maybe the solution is to build simple, basic apps based on this sync logic, 
that would work both on mobile and desktop.</p>
<p>It might be easier to build a simple app and keep it simple,
than to keep a plugin up to date with an app that&#39;s constantly evolving. </p>
<p>Those apps could actually be paid because of such a unique value proposition, 
even if the kind of people that want a p2p system for their personal life data
are probably not going to be interested in a closed source paid app.</p>
<p>It&#39;s easy to start a cool project. 
It&#39;s hard to keep it running and up to date for years without pay.
It&#39;s even harder to find a way to make it truely sustainable.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[KanbanMD update]]></title>
            <link>https://lecaro.me/20260214-kanbanmd-is-now-free.html</link>
            <guid>https://lecaro.me/20260214-kanbanmd-is-now-free.md</guid>
            <pubDate>Fri, 13 Feb 2026 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>KanbanMD update</h1>
<p>I&#39;ve built a desktop app to use markdown files as trello boards. I thought I&#39;d make it paid to avoid having to handle support for free users (that can get tiring with open source projects), but after seens 0 interest I&#39;m just going to release it for free and see. </p>
<p>I use it daily, it&#39;s usable, even if not perfect. It&#39;s linux only for now.
<a href="https://kanbanmd.lecaro.me/">https://kanbanmd.lecaro.me/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Software I use every day]]></title>
            <link>https://lecaro.me/20260226-software-i-run-every-day.html</link>
            <guid>https://lecaro.me/20260226-software-i-run-every-day.md</guid>
            <pubDate>Wed, 25 Feb 2026 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Software I use every day</h1>
<p>I&#39;ve spent a lot of time exploring various solutions to computing problems and thought I&#39;d share a bit of my findings. You&#39;ll find a heart next to things I&#39;d 100% recommend checking out.</p>
<h2>Desktop</h2>
<p>My pc runs <a href="https://www.debian.org/">❤️ Debian</a> with i3. Much more stable and simple than ubuntu, while still running all the mainstream oss apps. </p>
<ul>
<li><a href="https://i3wm.org/">i3</a> as window manager</li>
<li><a href="https://www.firefox.com/en-US/">Firefox</a> despite some user hostile behavior. As plugins I have <a href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/">❤️ ublock orgin</a> with javascript off by default. <a href="https://einaregilsson.com/redirector/">Redirector</a> helps avoid bad habits. <a href="https://ublacklist.github.io/docs">ublacklist</a> bans spam domains from search results. My search engine is now <a href="https://kagi.com/">kagi</a>, I&#39;ll give it a try with a one year starter plan. <a href="https://duckduckgo.com/">duckduckgo</a> is also great, but I feel bad using it while blocking ads. </li>
<li><a href="https://www.thunderbird.net/en-US/thunderbird/all/">Thunderbird</a> for emails, calendar and contacts. It&#39;s a bit annoying that it can&#39;t archive + mark as read in one keypress, the calendar ui feels a bit rigid, and the &quot;mark as spam&quot; function just keeps the mail there. The plugins look sometimes out of place. It usually looks bad on small windows dimensions. Overally, it&#39;s a great project, I even donated, but I feel like things could be better. I&#39;d like to contribute code there one day.</li>
<li><a href="https://www.jetbrains.com/all/">Jetbrain All Products Pack</a> for real world projects in all sorts of languages (js, php, ruby, rust, python)</li>
<li><a href="https://www.sublimetext.com/">❤️ Sublime Text 4</a> for simpler projects and notes.</li>
<li><a href="https://www.sublimemerge.com/">Sublime Merge</a> when things get too hairy to manage with the git cli</li>
<li><a href="https://kanbanmd.lecaro.me/">KanbanMD</a> for project tracking instead of Trello. I need to fix the ctrl-z and tab behavior.</li>
<li><a href="https://www.codeandweb.com/babeledit">BabelEdit</a> for translations editing, so much better than doing it by hand. I just wish it was OSS to a paid translation service attached.</li>
<li><a href="https://obsproject.com/">OBS</a> for screen recordings</li>
<li><a href="https://flameshot.org/">❤️ flameshot</a> for screenshots, it&#39;s great and works on windows/mac too.</li>
<li><a href="https://www.videolan.org/">❤️ VLC</a> for music and movies</li>
<li><a href="https://syncthing.net/">Syncthing</a> for sync of notes, photos and music mostly.</li>
<li><a href="https://signal.org/download/">Signal desktop</a> but I barely use it</li>
</ul>
<h2>Mobile</h2>
<p>My goal is to eventually transition all mainsteam apps to my iPhone X, and use a degoogled Sony Xperia 10 VI as main phone. 
I&#39;m not there yet thought, so here&#39;s what&#39;s really running on my android phone :</p>
<ul>
<li><a href="https://f-droid.org/packages/ademar.textlauncher/">❤️ Text Launcher</a> is the simplest launcher I could find, and makes you want to debloat your phone.</li>
<li><a href="https://f-droid.org/en/packages/com.beemdevelopment.aegis/">❤️ Aegis Authenticator </a> for 2FA OTP without cloud bs</li>
<li><a href="https://f-droid.org/en/packages/me.lecaro.breakout/">Breakout 71</a> of course, but I don&#39;t play it much.</li>
<li><a href="https://f-droid.org/en/packages/com.best.deskclock/">Clock</a> a simple AOSP based clock.</li>
<li><a href="https://f-droid.org/en/packages/org.fossify.contacts/">Fossify Contacts </a> to display the contacts synced by <a href="https://f-droid.org/en/packages/at.bitfire.davdroid/">DAVx5</a></li>
<li><a href="https://f-droid.org/en/packages/net.osmand.plus/">❤️ OsmAnd~ </a> for offline maps navigation. Great, feature full, but crashes from time to time. It was a real pain to export my thousands of marked places in google maps and import them here, but I managedto do it, I should share the script one day.</li>
<li><a href="https://f-droid.org/en/packages/com.github.catfriend1.syncthingfork/">Syncthing Fork</a> to sync notes, documents, music and photos with my desktop.</li>
<li><a href="https://f-droid.org/packages/org.kde.kdeconnect_tp/">KDE connect</a> to remote control my pc, not required but rather nice</li>
<li><a href="https://f-droid.org/en/packages/org.fossify.filemanager/">Fossify file manager</a></li>
<li><a href="https://f-droid.org/en/">❤️ F-Droid</a> of course</li>
<li><a href="https://f-droid.org/en/packages/com.nononsenseapps.feeder/">❤️ Feeder</a>, and amazing rss client</li>
<li><a href="https://f-droid.org/en/packages/org.fossify.gallery/">Fossify Gallery</a></li>
<li><a href="https://f-droid.org/en/packages/org.onekash.kashcal/">❤️ kashkal</a> to connect to my cardav server</li>
<li><a href="https://f-droid.org/en/packages/net.sourceforge.opencamera/">open camera</a> as a simple camera that can take low res photos (i don&#39;t need 40mpx photos of my groceries receipts)</li>
<li><a href="https://f-droid.org/en/packages/org.fossify.phone">fossify phone</a> for a simple phone dialer</li>
<li><a href="https://f-droid.org/en/packages/dev.octoshrimpy.quik.fdroid/">QUIK SMS</a> for sms/mms</li>
<li><a href="https://sms.lecaro.me/">sms.lecaro.me</a> to send welcome sms to users that signup on <a href="https://ciboulette.net/">https://ciboulette.net/</a></li>
<li><a href="https://f-droid.org/en/packages/com.dessalines.thumbkey/">❤️ thumb key</a> is an amazing keyboard. instead of training a model to recognize your swipes, you build muscle memory to use a keyboard layout, and then you get nearly the same typing speed as gboard without the need for an ai to &quot;read your mind&quot; and guesstimate what you want ot type.</li>
<li><a href="https://f-droid.org/en/packages/net.thunderbird.android/">thunderbird</a> is an allright mail client.</li>
<li><a href="https://f-droid.org/en/packages/org.videolan.vlc/">❤️ vlc</a> for music</li>
<li><a href="https://f-droid.org/en/packages/org.breezyweather/">❤️ Breezy weather</a> as a simple, very reliable weather app</li>
<li><a href="https://download.wireguard.com/android-client/">wireguard</a> to connect to my vpn.</li>
<li><a href="https://f-droid.org/en/packages/org.fossify.math/">fossify calculator</a></li>
<li><a href="https://f-droid.org/en/packages/org.joinmastodon.android/">mastodon</a> but I think I prefer RSS</li>
</ul>
<p>Some of the apps i&#39;d like to remove or move to the iPhone :</p>
<ul>
<li>The play store</li>
<li><a href="https://f-droid.org/en/packages/com.aurora.store/">Aurora Store</a>, but i&#39;m not using it much, multiple apps refused to start after installing from there. Google&#39;s push for install source checks is working.</li>
<li><a href="https://f-droid.org/en/packages/com.rk.xededitor/">Xed-Editor</a> for text editing (notes, todo, recipes, blog posts..). Maybe too featureful for my needs, and a bit buggy, I often get crahses when reopening a file. All I want is a plain text editor with a folder view, some tabs, full text search in folder, and maybe syntax highlighting but that&#39;s not even required. I tried <a href="https://f-droid.org/en/packages/net.gsantner.markor/">markor</a> but it&#39;s opinionated in a different direction from what I want. Basically <strong>I&#39;m looking for sublime text mobile</strong>.</li>
<li><a href="https://f-droid.org/en/packages/org.mozilla.fennec_fdroid/">Fennec</a> as a slightly cleaner firefox, but I no longer trust mozilla</li>
<li>My banking apps, they&#39;ll move to my iPhone soon</li>
<li><a href="https://play.google.com/store/apps/details?id=com.applidium.vianavigo">IDF mobilité</a> from the play store, it uses google maps layers</li>
<li><a href="https://play.google.com/store/apps/details?id=tv.standard.nebula">Nebula</a> as a youtube alternative</li>
<li><a href="https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms">signal</a> from the play store, I want to degoogle fully and I think they rely on google services, also, feature creep.</li>
<li>Whatsapp (shame, shame!). Sadly I need it to talk to customers of <a href="https://ciboulette.net/">https://ciboulette.net/</a>. But maybe I can offer another contact method.</li>
<li>YouTube : I pay (a small price) for premium, because the content available there is just not visible anywhere else.</li>
<li>Unifi endpoint (to open the office door at work, I&#39;d rather not have work software on my personal phone)</li>
</ul>
<h2>Servers</h2>
<p>I rent three servers from <a href="https://www.hetzner.com/cloud/">❤️ Hetzner</a> (about 17€/month total).</p>
<p>The <a href="https://app.ciboulette.net">first CX32</a> hosts </p>
<ul>
<li>ciboulette production</li>
</ul>
<p>The <a href="https://staging.lecaro.me/">second CX32</a> hosts </p>
<ul>
<li>ciboulette staging</li>
<li>my email with dovecot + postfix</li>
<li><a href="https://sms.lecaro.me">A cloud sms service</a> to send SMS from real android phones </li>
<li><a href="https://txt.lecaro.me">An E2E encrypted collaborative text files editing</a></li>
<li><a href="https://breakout.lecaro.me/">breakout 71</a> and its archives</li>
<li>the website you are reading.</li>
</ul>
<p>The third server is a CX23 for private use only. </p>
<ul>
<li><a href="https://radicale.org/v3.html">radicale</a> stores my calendar and contacts.</li>
<li><a href="https://git-scm.com/">git</a> stores my private repos </li>
<li><a href="https://www.wireguard.com/">wireguard</a> connects all my devices in a virtual private network.</li>
</ul>
<p>The only exposed port of the private server is a random port for wireguard.
I&#39;ve had some issues with wireguard, It stopped working randomly for my mobile devices, but I&#39;m not yet sure of the root cause.
It could very well be my home router that&#39;s the issue.</p>
<p>Some of the services I&#39;d really like to self host now are:</p>
<ul>
<li><a href="https://www.discourse.org/">Discourse</a> or another forum  instead of discord for breakout discussions. I find IRC and matrix a bit too involved. I don&#39;t even have time to chat now, so this is not urgent.</li>
<li><a href="https://fossil-scm.org">fossil</a> as an alternative to git for projects where I work alone anyway.</li>
<li>some automatic incremental backup software for my family and myself. The trick is to find something with clients for windows and mac too. Ideally, I&#39;d like the backup to only be readable by their author and not myself as an admin of the server.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[KanbanMD update]]></title>
            <link>https://lecaro.me/20260313-kanban-md-update.html</link>
            <guid>https://lecaro.me/20260313-kanban-md-update.md</guid>
            <pubDate>Thu, 12 Mar 2026 23:00:00 GMT</pubDate>
            <description><![CDATA[<h1>KanbanMD update</h1>
<p>I fixed the undo/redo bahavior, added board backgrounds and dark mode.</p>
<p><img src="renan-2026-03-13_16-45-kanban.jpg" alt="Screenshot of the latest version"></p>
<h2>What is Kanban Md</h2>
<p>It is a desktop app to manage solo projects as kanban boards, with a simple markdown file as a backend.
It is offline first (for the most part, actually the background feature is online only for now).
It only runs on linux for now. It is free to use, but not open source for now. I use it every day.</p>
<h2>Fixed undo/redo</h2>
<p>The logic of undo redo is now a little bit more sensible. If you have a card or section opened, do some edits
and press undo/redo, only edits to the current card will be changed.</p>
<p>If you then close the card and undo, the whole edit is undone.</p>
<p>Before I tried to deal with both in one undo stack and that made things very complicated.</p>
<h2>Background images</h2>
<p>If you add an image before the first h2 of your markdown file, it will be taken as a background image for the board.</p>
<p>Try some of those for example </p>
<pre><code>![Cover image](https://i.imgur.com/PGHn9xr.jpeg)
![Cover image](https://i.imgur.com/cUXkOfc.jpeg)
![Cover image](https://i.imgur.com/v5TrjuQ.jpeg)
</code></pre>
<p>Note that for now the picture must be hosted online, relative paths won&#39;t work.</p>
<h2>Dark mode</h2>
<p>The little button at the top right near the search lets you switch between light and dark mode.</p>
<h2>Memorized scroll position</h2>
<p>The app now remembers what your scroll position was on the board last time you closed it, so you are no longer
sent back to the left side of the board when it refreshes or when switching tabs.</p>
<h2>How to update/try the app</h2>
<p>Head to <a href="https://kanbanmd.lecaro.me/">https://kanbanmd.lecaro.me/</a> and download the linux binary, a deb or rpm. 
If you are still using trello, make a json export of your board and use the tool at the bottom of the page to convert it to markdown.</p>
<p>Happy hacking.</p>
<p>Renan</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[I'm working on a new brick breaker game]]></title>
            <link>https://lecaro.me/20260810-a-new-game-project.html</link>
            <guid>https://lecaro.me/20260810-a-new-game-project.md</guid>
            <pubDate>Sun, 09 Aug 2026 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>I&#39;m working on a new brick breaker game</h1>
<p>It is based on breakout 71 but with a focus on easier learning and less metaprogression. </p>
<p>The name is &quot;Zala&quot;, for &quot;золотой&quot; or &quot;golden&quot; in Russian. As with B71, the gold is to get as many gold coins as possible. I&#39;m trying to get a steam page ready.</p>
<p>It&#39;s very tempting to share the link to get encouragements, but I think I need to focus on building the game a bit more first. So for now, the result is not public. I&#39;ll push a demo when the steam page is ready.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[KanbanMD update]]></title>
            <link>https://lecaro.me/20260810-kanbanmd-right-clicks-and-better-save.html</link>
            <guid>https://lecaro.me/20260810-kanbanmd-right-clicks-and-better-save.md</guid>
            <pubDate>Sun, 09 Aug 2026 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>KanbanMD update</h1>
<p>I&#39;ve updated kanban md with a few fixes to make it nicer to use every day.</p>
<ul>
<li>Right click actions have been added to the tabs, lists and cards</li>
<li>Much nicer behavior on paste, and you can force copied sections to be pasted as cards with ctrl + shift + v</li>
<li>Added the prevent default plugin to disable some shortcuts</li>
<li>Focus the next card after archiving one</li>
<li>Detect and close files that have been deleted or removed</li>
<li>Option to open a file with the default system text editor</li>
<li>More predictable undo/redo behavior, and keeps the scroll postion</li>
<li>[escape] clears the search even if that input is not focused</li>
<li>Remove the &quot;duplicate&quot; shortcut with &quot;d&quot; that was too easy to trigger accidentally, and not super useful</li>
<li>generate a sync conflict file if the local file is not in expected state when trying to overwrite it</li>
<li>check if the file changed after a long time of inactivity</li>
</ul>
<p>This makes the app much nicer to use for me. You can get it here for linux : <a href="https://kanbanmd.lecaro.me/">https://kanbanmd.lecaro.me/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tabby - a text editor for android]]></title>
            <link>https://lecaro.me/20260910-tabby.html</link>
            <guid>https://lecaro.me/20260910-tabby.md</guid>
            <pubDate>Wed, 09 Sep 2026 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Tabby - a text editor for android</h1>
<img style="float:right; width: 200px; padding-left:10px ;"  src="/20260910-tabby.png" alt="Screenshot of tabby"/>

<h2>Just the minimum</h2>
<p>I keep my notes as plain text markdown files.
They sync between my desktop and phones with syncthing.</p>
<p>However, once on device, I didn&#39;t find the perferct editor for 
my needs, something with strictly only what I need.</p>
<p>Tabby is a text editor that lets you open local text files, modify them and close them.</p>
<p>It asks for no permissions and has no settings.
It takes almost no space on your device once installed (500kb).
It has only one mode, plain text editing.
It does not have syntax highlighting.
It will open any text-ish file you throw at it.</p>
<h2>Avoiding conflicts</h2>
<p>There&#39;s only an edit mode, that tries to keep the content of the file and a textarea in sync. </p>
<p>You change are autosaved after 2s without editing.</p>
<p>The content of the editior is refreshed when you leave the app and come back to it. </p>
<p>You can open multiple files as tab, but you&#39;re really just editing one at a time, as your changes are saved if you leave a tab for another.</p>
<h2>Usage</h2>
<p>Click the + button to pick a file to edit, it will open as a tab.
Edit your file, it will be saved automatically.
Long press a tab to close it.</p>
<h2>Download</h2>
<p>You may <a href="/20260910-tabby.apk">download it</a> (25kb) or <a href="https://gitlab.com/lecarore/tabby">check the source</a>. </p>
<p>I&#39;m not very good a android native dev so this is basically <a href="https://gitlab.com/lecarore/tabby/-/blob/main/app/src/main/java/me/lecaro/tabby/MainActivity.kt?ref_type=heads">500 LOC</a> of vibe coded kotlin using google&#39;s free ai agents.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[So long, Google, and thanks for all the nudes]]></title>
            <link>https://lecaro.me/20260921-google-less.html</link>
            <guid>https://lecaro.me/20260921-google-less.md</guid>
            <pubDate>Sun, 20 Sep 2026 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>So long, Google, and thanks for all the nudes</h1>
<p>The Play Store does not run on my old devices; this led me to accidentally experience some joy while using Android.</p>
<p>I&#39;ve recently acquired 10 identical <a href="https://www.devicespecifications.com/en/model/93704df6">Android 6.0.1 phones</a>. The plan is to equip them with some <a href="https://gitlab.com/lecarore/sms-gateway">SMS gateway software</a> and ship them to some customers.</p>
<p>Because those phones are so old, I&#39;ve had to improvise. And because I have 10 of them, I&#39;m not too scared of experimenting.  It gave me a taste of what a degoogled, compile-from-source Android OS is like. I&#39;m starting to think I&#39;d actually enjoy using that.</p>
<p>I&#39;m forced to pick lightweight apps because the OS only has 2 GB of ram. They happen to be my favorite kind of app. I&#39;m making my own (slop) apps when I can&#39;t find exactly what I want.</p>
<p>This joyful fidling comes with a big letdown by Google. I was hoping to publish my new game on the store and charge for it, but their review process is so broken, I think I&#39;ll stick to F-Droid and itch.io.</p>
<p>Google is simultaneously grabbing full power over app developers with their latest <a href="https://developer.android.com/developer-verification">monopoly-consolidating move</a>, and botching the review process to a level I have never witnessed before. </p>
<p>Google never cared about my apps. The review process is humiliating, alien, infuriating. The reasons an app is rejected are never clearly explained. This has always been the case; publishing <a href="https://workspace.google.com/marketplace/app/mermaid_diagramming_and_charting_tool/636321283856">mermaid gdocs</a> years ago led me to the start of my degoogling journey. </p>
<p>I tried to offer my text editor <a href="/20260910-tabby.html">Tabby</a> on the Play Store, but it was rejected. They attached an NSFW screenshot of some unidentified app. <strong>Google is literally sending me nudes</strong> as part of their review process. I appealed, stating that this had nothing to do with my app, and (as expected) the appeal was rejected without explanation. Someone probably looked at the same screenshot and thought, &quot;Yep, that&#39;s still p0rn&quot;</p>
<p>I suspect some of the automatic app listing translations they added contained a link to some dodgy website. I&#39;m blocked from reading the content I submitted anyway, so I can&#39;t tell. Maybe they mixed up apps during the review. Maybe they opened the wrong text file in the editor. I don&#39;t know; I can&#39;t ask anyone, and any repeat offense can lead to a full dev account deletion, so I will not try to &quot;debug&quot; their review process.</p>
<p>I no longer care. Google is never pushing free apps forward. It seems pretty clear to me that <strong>counting on Google is a bad idea</strong>. I&#39;ll keep building tools and games and share them with the people that are brave enough to venture outside the Play Store.</p>
<p>Google, if you want the exclusive power to gatekeep apps, you should do a much better job of reviewing them.</p>
<p>Edit on Oct 13, 2026: The Google Play team saw this post and apologized about the bad experience. They were really nice. They seem to really want to make it right and suddenly care a lot about getting one more text editor on the store. I&#39;m not sure why. I&#39;m good, though. The frog jumped out. It isn&#39;t getting back in. Thanks for kicking the pan.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Treasure hunt on F-droid]]></title>
            <link>https://lecaro.me/20261008-fdroid-finds.html</link>
            <guid>https://lecaro.me/20261008-fdroid-finds.md</guid>
            <pubDate>Wed, 07 Oct 2026 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Treasure hunt on F-droid</h1>
<p>I&#39;ve degoogled my main phone. F-droid is now my only app store. I took the time to do a bit of digging and found some nice things I&#39;d like to share.</p>
<p>Basicsync takes care of syncing notes, photos, music and videos with my computers. davx5 syncs my contacts with radicale, kashcal directly syncs with a calendar hosted on my server with radicale.</p>
<p>For photos, I really like &quot;lomo camera&quot;, with its minimal ui. I review the photos in Iris gallery which is light and snappy, but a bit too featureful for my taste.</p>
<p>My home screen used to be &quot;text launcher&quot; but now it&#39;s also a vibe coded alternative which gets rid of the search, among other things.</p>
<p>Binary eye is fantastic to read qr codes. I&#39;ve found it handy that it can also make a QR code from raw text.</p>
<p>I used to rely on juicessh to connect to my servers, but now ConnectBot does the job just as well. </p>
<p>I tried a few games and liked Edge Roll&#39;s simple inputs, and &quot;find words&quot; as a light brain-teaser. &quot;quiet grid&quot; has tastefully created clones of many board games. I haven&#39;t played much Breakout 71 or Zala recently, too busy with other projects. But my idea list if full of potential games I&#39;d like to build one day.</p>
<p>My email are in thunderbird, VLC reads videos (no more youtube or nebula), breezy weather tells me if it will rain. </p>
<p>My browser is fennec, and I get to mastodon through snowdrop. My keyboard is Thumb-key, I love that keyboard.</p>
<p>I downloaded wireguard and signal as apks manually as they are not on fdroid. Signal works without the google play services, nice. I edit texts with Tabby, a text editor I vibe coded, still a bit rough but already my favourite as it has so few features.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[It's just all so complicated]]></title>
            <link>https://lecaro.me/20261009-its-just-all-too-complicated.html</link>
            <guid>https://lecaro.me/20261009-its-just-all-too-complicated.md</guid>
            <pubDate>Thu, 08 Oct 2026 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>It&#39;s just all so complicated</h1>
<p>I might have coded myself into a corner.</p>
<p>I started ciboulette.net 10 years ago. I have a hundred customers. It&#39;s a lot of Meteorjs code that has to keep running.</p>
<p>I first hosted it on Heroku, but it was expensive and slow. Then I moved to DigitalOcean thanks to <a href="https://meteor-up.com/">meteor up</a>, a CLI tool that takes care of setting up docker, mongo, nginx and let&#39;s encrypt certificates. It was really easy to get up and running. I moved to Hetzner eventually and added more apps on top of the mup setup.</p>
<p>Over time, I&#39;ve understood bit by bit how some of its components work. But it&#39;s difficult to start learning when everything is already deeply integrated.</p>
<p>Meteor up hasn&#39;t been updated in a while. It&#39;s stuck on an old version of nginx and docker. Now, I finally have to understand what&#39;s going on on my servers. Time to learn a lot of things at once.</p>
<p>I feel like this often happens in tech. You find a tool that gives you superpowers. It lets you do things you could not do before. You take advantage of it, count on it, reap the benefits. Then you realize that when serious issues happen, you suddenly need to understand everything your tool did for you.</p>
<p>It wrote code you weren&#39;t capable of writing. Now you need to reverse engineer and debug that code.</p>
<p>I should resist complexity much more and keep things very basic and flat, and use tech I know well. Because building something without deep understanding is just postponing the learning time to the worst possible time.</p>
<p>This applies to AI of course, where you could easily vibe code a mess, then be unable to understand what you built. But also to hosting tool like meteor up or Kamal, that set up production hosting for you. Programming Frameworks often carry the same risk. They let you put all your eggs in one basket that you don&#39;t yet understand.</p>
<p>Those tools are useful. I&#39;m forever thankful to zondern for meteor-up. But it&#39;s a good idea to be curious and slowly learn what&#39;s happening under the hood. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Leave it to know if you should stay]]></title>
            <link>https://lecaro.me/20261013-leave-to-stay.html</link>
            <guid>https://lecaro.me/20261013-leave-to-stay.md</guid>
            <pubDate>Mon, 12 Oct 2026 22:00:00 GMT</pubDate>
            <description><![CDATA[<h1>Leave it to know if you should stay</h1>
<p>I&#39;ve left whatsapp and google behind recently, and the experience has been insightful.</p>
<p>When leaving whatsapp, I tried to make a backup of my conversations, but that required a slow manual manipulation to be performed for each conversation one by one on mobile. It&#39;s like they don&#39;t want you to do that.</p>
<p>When leaving google photo, there was no way to delete my 40k photos in one go, only a laggy &quot;scroll, select, delete, repeat&quot; manual process.</p>
<p>In google maps, my takeout of all the map pins was inexplicably unusable. They gave me a csv full of web links to google map, that even gemini didn&#39;t know what to make out of. Some people made a <a href="https://www.takeout-tools.com/">business</a> out of the shortcomings of that tool.</p>
<p>In tech, I&#39;ve often seen a combination of &quot;are you really sure ?&quot; dark patterns and buggy, low-effort takeout options.</p>
<p>In real life, things get more personal. The gym suddenly treats you like a dog for cancelling your membership. They don&#39;t return your calls. The bank hides behind the fine lines of the contract. Everyone needs an expensive snail mail request for everything they don&#39;t want to do. All the sweet talk evaporates.</p>
<p>For some companies, it&#39;s the opposite, they only start caring about you when you churn. Your telephone rate suddenly goes cheaper. Your natural gas price inexplicably gets slashed in half. They go from treating you like an anonymous excel line to a VIP.</p>
<p>All that to say, if you&#39;re not sure about investing more into a company, try to leave and see how that goes. Often you&#39;ll get to see their true face or get a nice discount. </p>
]]></description>
        </item>
    </channel>
</rss>