<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[SequentialRead]]></title><description><![CDATA[home-made software for autonomous web publishing  ]]></description><link>https://sequentialread.com/</link><image><url>https://sequentialread.com/favicon.png</url><title>SequentialRead</title><link>https://sequentialread.com/</link></image><generator>Ghost 3.41</generator><lastBuildDate>Thu, 26 Feb 2026 21:15:53 GMT</lastBuildDate><atom:link href="https://sequentialread.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[💥PoW! Bot Deterrent, blog comments updates]]></title><description><![CDATA[ I started getting bug  reports about GrapheneOS users not being able to pass the bot deterrent.  As of now all those issues should be fixed.]]></description><link>https://sequentialread.com/pow-bot-deterrent-blog-comments-and-new-capsul-dev-blog/</link><guid isPermaLink="false">6907ba659d040700014931c2</guid><category><![CDATA[browser]]></category><category><![CDATA[FLOSS]]></category><category><![CDATA[for fun]]></category><category><![CDATA[frontend]]></category><category><![CDATA[products]]></category><category><![CDATA[operations]]></category><category><![CDATA[Self-Hosting]]></category><category><![CDATA[ServiceWorker]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Sun, 02 Nov 2025 20:36:15 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2025/11/graphene-vanadium-support.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2025/11/graphene-vanadium-support.png" alt="💥PoW! Bot Deterrent, blog comments updates"><p>It's been a while,  I don't have much to say,  other than I recently updated some of my older projects:</p>
<p><a href="https://git.sequentialread.com/forest/pow-bot-deterrent">💥PoW! Bot Deterrent</a></p>
<ul>
<li>About a year ago, I had hastily modified this to require all the static assets to be served from the same domain, in order to satisfy a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP">Content Security Policy (CSP)</a>.</li>
<li>Unfortunately, this broke 3rd party embedding use cases including my blog comments system.</li>
<li>I also forgot to fully update the docs and example project, so it became a lot less usable. Sorry about that.</li>
<li>I started getting bug  reports about GrapheneOS users not being able to pass the bot deterrent.</li>
</ul>
<p>As of now all those issues should be fixed.</p>
<p><img src="https://sequentialread.com/content/images/2025/11/graphene-vanadium-support.png" alt="💥PoW! Bot Deterrent, blog comments updates"></p>
<p>I changed it so now there are two ways to use it, either with everything on the same domain, <a href="https://git.sequentialread.com/forest/pow-bot-deterrent#data-pow-bot-deterrent-static-assets-path"><code>data-pow-bot-deterrent-static-assets-path</code></a>, or with the static assets downloaded from the separate bot-deterrent domain, <a href="https://git.sequentialread.com/forest/pow-bot-deterrent#data-pow-bot-deterrent-static-assets-cross-origin-url"><code>data-pow-bot-deterrent-static-assets-cross-origin-url</code></a>.</p>
<p>I also updated all of my apps that use the 💥PoW! Bot Deterrent:</p>
<ul>
<li><a href="https://git.sequentialread.com/forest/sequentialread-comments">sequentialread-comments</a> (blog comments system)</li>
<li><a href="https://git.sequentialread.com/forest/picopublish">📤📚 picopublish</a> (minimalist upload-and-share tool)</li>
<li><a href="https://git.sequentialread.com/forest/pow-bot-deterrent-rp">pow-bot-deterrent-rp</a> (AI scraper bot blocker, similar to Anubis. Specifically intended for source code forges like Gitea, Forgejo, etc.)</li>
</ul>
<p>I <a href="https://git.sequentialread.com/forest/pow-bot-deterrent/src/branch/main/build-docker.sh">publish multi-architechture docker images</a> for each of these to docker hub, so if you want to use the binaries that way, you can:</p>
<p><a href="https://hub.docker.com/r/sequentialread/pow-bot-deterrent">https://hub.docker.com/r/sequentialread/pow-bot-deterrent</a></p>
<p><a href="https://hub.docker.com/r/sequentialread/comments">https://hub.docker.com/r/sequentialread/comments</a></p>
<p><a href="https://hub.docker.com/r/sequentialread/picopublish">https://hub.docker.com/r/sequentialread/picopublish</a></p>
<p><a href="https://hub.docker.com/r/sequentialread/pow-bot-deterrent-rp/tags">https://hub.docker.com/r/sequentialread/pow-bot-deterrent-rp/tags</a></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Using v2ray with Caddy to Access the Internet in China]]></title><description><![CDATA[I did witness some of my co-travelers getting blocked at various points, but even after that happened, onboarding them to my system solved these issues and did not precipitate any issues for them or for my services. ]]></description><link>https://sequentialread.com/v2ray-caddy-to-access-the-internet-in-china/</link><guid isPermaLink="false">6789a6bd5e5eb30001141449</guid><category><![CDATA[FLOSS]]></category><category><![CDATA[Self-Hosting]]></category><category><![CDATA[censorship]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Wed, 29 Jan 2025 08:27:17 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2025/01/s-1.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2025/01/s-1.jpg" alt="Using v2ray with Caddy to Access the Internet in China"><p>In fall 2024, I traveled to Japan and China to see my brother and his wife for their wedding. They met in Shanghai, where everyone seems to be impressed by his fluency in spoken and written Chinese, and he was impressed by the tree-climbing skills of a businesswoman he met 💑</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/leif4.jpg" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure><!--kg-card-begin: markdown--><p>I've long been curious about how the internet censorship in China works, what it feels like to interact with it, and was excited about the opportunity to experience it firsthand.</p>
<p>My brother has been working through it for a long time, almost 10 years now I think. He started off using the <a href="https://github.com/StreisandEffect/streisand">StreisandEffect/streisand</a> project as his main solution.  It included some shell scripts and Ansible playbooks to install various VPN servers on an AWS EC2 instance.</p>
<p>For client software back then, he said he used</p>
<blockquote>
<p>shadowrocket on iOS and shadowsocksX-NG on macOS</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>I did a lot of research on how people get around the censorship, the history of anti-censorship technology in china, what matters most when using various VPN/proxy solutions, etc, and came up with a plan.</p>
<ol>
<li>I would use <a href="https://www.v2ray.com/en/">V2Ray</a>
<ul>
<li>Despite what people call it, <code>v2ray</code> is not really a VPN. Technically it's a hightly sophisticated <a href="https://picopublish.sequentialread.com/files/network-protocol-software-terminology2.jpg">&quot;forward tunnel&quot; system</a></li>
<li>This allows it to &quot;blend into the background&quot; more easily, and mask its presence with clever sleight of hand.
<ul>
<li><em>sleight of packets</em>?  <em>sleight of protocols</em>?</li>
</ul>
</li>
<li>As far as I can tell, it's the most mature censorship-resistance solution avaliable</li>
<li>Client software is avaliable for iOS, Android, Mac, Windows, and Linux</li>
</ul>
</li>
<li>I would configure the clients and servers specifically to leverage the sneaky capabilities that <code>v2ray</code>'s various softwares offer.
<ul>
<li>Plausible deniability that &quot;This is just normal internet traffic 😉&quot;</li>
<li><strong>ℹ️ NOTE</strong>: Configuring the client is of utmost importance, that's a large part of why <code>v2ray</code> can work as well as it does.</li>
<li>More information on that later.</li>
</ul>
</li>
<li>I would make several VPN servers ahead of time.
<ul>
<li>Idea was that in case the first one got blocked, I would have a fighting chance to figure out what went wrong and try again with the next one.</li>
</ul>
</li>
</ol>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="flawlessvictory">Flawless Victory</h1>
<p>I won't bury the lede. I had zero problems browsing the web and accessing my cloud accounts while I was in China. Gmail and google maps? No problem. Tank Man? No problem. Airplane tickets, Japanese hotel bookings, bank info, the list goes on.</p>
<p>I did witness some of my co-travelers getting blocked at various points, but even after that happened, onboarding them to my system solved these issues and did not precipitate any issues for them or for my services. I believe it was even used to stream Netflix at one point  🤣</p>
<p>I did have some trouble installing some Chinese apps, but I'm not sure if that was caused by the <code>v2ray</code> or not.  Alipay worked fine, but Dianping (Similar to Yelp) didn't seem to like my phone.</p>
<p>Its true that this was a short duration trip ( about a week ), and I have a few unique advantages that made this easier for me to pull off, but I do believe it's not a complete fluke, and it's worth writing about.</p>
<p>Specifically, there were some unclear and confusing aspects of <code>v2ray</code>'s documentation and configuration. I felt like it would be worthwhile to publish my solutions, I think they are cleaner and easier to understand than others I've seen around the web.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="serverconfigurationwcaddy">Server Configuration w/ Caddy</h1>
<p>This is where I think I have novel stuff to contribute, on top of other guides that already exist.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/Untitled-Diagram.drawio-1-.png" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure><!--kg-card-begin: markdown--><p>I found the v2ray server configuration slightly confusing, and had some issues getting it to work at first, especially the HTTP path. So I decided to just leave it as default as possible and handle most of the HTTP stuff in Caddy, which I'm more familiar with.</p>
<p>First off, the</p>
<h3 id="v2rayconfigurationusingdocker">V2Ray configuration using Docker:</h3>
<p><code>docker-compose.yml</code></p>
<pre><code>services:
  v2ray:
    image: teddysun/v2ray:5.19.0
    ports:
      - 1310:1310
    container_name: v2ray
    environment:
      - TZ=Asia/Shanghai
    restart: always
    command: /usr/bin/v2ray run -config /v2ray/config.json
    volumes:
      - ./v2ray:/v2ray
</code></pre>
<p><code>v2ray/config.json</code></p>
<pre><code>{
  &quot;log&quot;: {
    &quot;access&quot;: &quot;/var/log/v2ray/access.log&quot;,
    &quot;loglevel&quot;: &quot;info&quot;
  },
  &quot;inbounds&quot;: [
    {
      &quot;listen&quot;: &quot;0.0.0.0&quot;,
      &quot;port&quot;: 1310,
      &quot;protocol&quot;: &quot;vmess&quot;,
      &quot;settings&quot;: {
        &quot;clients&quot;: [
          {
            &quot;id&quot;: &quot;82d984d3-1cda-4bc5-bd49-f167e73c2719&quot;,
            &quot;alterId&quot;: 0,
            &quot;security&quot;: &quot;auto&quot;
          }
        ]
      },
      &quot;streamSettings&quot;: {
        &quot;network&quot;: &quot;ws&quot;,
        &quot;wsSettings&quot;: {
          &quot;path&quot;: &quot;/&quot;
        }
      },
      &quot;mux&quot;: {
        &quot;enabled&quot;: true
      }
    }
  ],
  &quot;outbounds&quot;: [
    {
      &quot;protocol&quot;: &quot;freedom&quot;,
      &quot;tag&quot;: &quot;freedom&quot;
    }
  ],
  &quot;dns&quot;: {
    &quot;servers&quot;: [
      &quot;8.8.8.8&quot;,
      &quot;8.8.4.4&quot;
    ]
  }
}

</code></pre>
<p>The <code>inbounds[0].settings.clients[0].id</code> value acts as the <code>v2ray</code> server password, which has to be entered into the client for it to be able to connect.</p>
<p>It must be a GUID, which can be generated with the <code>uuidgen</code> command:</p>
<pre><code>forest@debian:~$ sudo apt-get install uuid-runtime
...
forest@debian:~$ uuidgen
82d984d3-1cda-4bc5-bd49-f167e73c2719
forest@debian:~$ 

</code></pre>
<p>The <code>inbounds[0].streamSettings.network = &quot;ws&quot;</code> value means WebSocket. After an HTTP connection is made, it will upgrade to a WebSocket, and start the <code>VMESS</code> protocol from there.</p>
<p>This may not be the ideal configuration, but it worked for me, and seemed to be the most immediately avaliable and reliable.</p>
<p>Next, we have the</p>
<h3 id="caddyconfigurationnotusingdocker">Caddy configuration (Not using Docker):</h3>
<p>I could have ran Caddy in Docker as well, but the server I was installing it on already had Caddy set up running on the host, with its config file at:</p>
<p><code>/etc/caddy/Caddyfile</code></p>
<pre><code>mail.totally-legit-domain.com {
  root * /usr/share/caddy

  route /ws_io34857 {
    rewrite /ws_io34857* /
    reverse_proxy http://localhost:1310

  }

  route /login {
    respond 401 {
      body &quot;401 Unauthorized&quot;
      close
    }
  }

  route {
    file_server
  }
}
</code></pre>
<p>What all is going on in this <code>Caddyfile</code>?</p>
<p>Lets go thru it...</p>
<ol>
<li>A separate subdomain is used for v2ray.
<ul>
<li>This encapsulates the v2ray config and allows it to coexist with other things on the same server.</li>
</ul>
</li>
<li>The domain name looks like it might be a mailserver. I also used other common, legit sounding subdomains like <code>social.xyz.com</code> and <code>webmail.xyz.com</code> on other servers.
<ul>
<li>Domain names are 100% public.  The domain name is transmitted in plaintext multiple times during each web request, once by the DNS protocol and once in the <code>SNI</code> (server name indication) field of the TLS <code>ClientHello</code> packet.</li>
</ul>
</li>
<li>There are three handlers:
<ul>
<li>Requests to <code>/ws_io34857</code> will have the <code>/ws_io34857</code> path stripped and be reverse-proxied to port <code>1310</code>, where our <code>v2ray</code> server is listening.
<ul>
<li>A non-guessable path is used to make it harder for a prober to analyse what this server is serving.</li>
<li>The path part of an HTTPS request is encrypted with TLS, so it won't leak like the domain name will.</li>
</ul>
</li>
<li>Requests to <code>/login</code> will always return 401</li>
<li>All other requests will serve static files from <code>/usr/share/caddy</code></li>
</ul>
</li>
</ol>
<p>Inside <code>/usr/share/caddy</code>, I put a slightly modified version of the login page from the <a href="https://sr.ht/~migadu/alps/"><code>alps</code> webmail client</a> as <code>index.html</code>:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/Screenshot_20250129_012242.png" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure><!--kg-card-begin: markdown--><p>So from the outside, this looks like a normal login page which matches the domain name. If you try to login, no matter what username and password you chose, you will always get a basic <code>401 Unauthorized</code> response, which would look typical from any sort of basic web application written in Go, or really any web application that returns plain text responses for errors.</p>
<p>Since the <code>v2ray</code> requests to this domain are using TLS, the content of the requests is encrypted. And if I understand correctly, the VMESS protocol that v2ray uses specifically crafts the sizes and timings of the encrypted bytes flowing inside requests and responses to make the connection look like normal web traffic.  (To some extent, it probably helps that real web traffic is being tunneled inside it!)</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h1 id="clientconfiguration">Client Configuration</h1>
<p>I used <a href="https://github.com/2dust/v2rayNG">v2rayNG</a> as my primary client on Android (LineageOS), and <a href="https://github.com/Qv2ray/Qv2ray">QV2ray</a> on Linux desktop.</p>
<p>I downloaded the v2rayNG APK directly from the main project's GitHub releases page:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/3.jpeg" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure><!--kg-card-begin: markdown--><p>The <code>v2rayNG</code> configuration that worked looked like this:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><img src="https://sequentialread.com/content/images/2025/01/7.png" style="max-width:400px;" alt="Using v2ray with Caddy to Access the Internet in China"><br><br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>These client configurations can be shared between apps via text or QR code, however, not all apps / versions are compatible. Worst case, you can always enter it in manually.</p>
<p>This is not the most important part of the configuration though, this just describes how to contact the v2ray server WHEN the client has determined that it needs to.</p>
<p>But the magical part that allows this project to work is all about that WHEN.  The client needs a ton of smart rules and lists to use to determine when it should try to connect via the tunnel vs when it should try to connect directly.</p>
<p>If I remember correctly, v2rayNG comes with all of this setup already, which was a major reason why I chose to use it.  I had to patch together and set it up myself on the desktop app, which took me some time, especially since most of the documentation and community is in Chinese language.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><img src="https://sequentialread.com/content/images/2025/01/20.jpg" style="max-width:400px;" alt="Using v2ray with Caddy to Access the Internet in China"><br><br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>There are a bunch of these rules already setup, but I also created my own based on a list of domains I'm always surfing: various community-hosted chats and resources.  I'm not sure, but I believe this may have helped my <code>v2ray</code> servers to sneak by 100% undetected: <strong>They shared IP addresses with other active &amp; recognizable services that were not blocked.</strong></p>
<p>It might have also helped that lot of these IP addresses aren't on mainstrem American cloud providers:  Its a mix of small scale colo and residential ISP addresses.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/direct.jpeg.jpg" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure><!--kg-card-begin: markdown--><p>I made sure to add the same list and behavior on the desktop app as well.</p>
<p>At any rate, there's more to life than using the internet. So have some pictures of delicious foods I tried while I was in Shanghai.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/s.jpg" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/IMG_20241013_131013358.9oa.jpg" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2025/01/IMG_20241013_131733937.FT2.jpg" class="kg-image" alt="Using v2ray with Caddy to Access the Internet in China"></figure>]]></content:encoded></item><item><title><![CDATA[Bonavita 1L Electric Kettle Repair]]></title><description><![CDATA[it worked for years and years...  started getting harder and harder to power it on; I would have to press the power button down for longer...

Eventually it simply wouldn't turn on anymore at all.]]></description><link>https://sequentialread.com/bonavita-electric-kettle-repair/</link><guid isPermaLink="false">66cbcca4bca75c00013c63f3</guid><category><![CDATA[hardware]]></category><category><![CDATA[soldering]]></category><category><![CDATA[brand shaming]]></category><category><![CDATA[education]]></category><category><![CDATA[for fun]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Tue, 27 Aug 2024 03:48:51 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2024/08/kettle1.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h2 id="toolslist">Tools List</h2>
<ul>
<li>Electronics Screwdriver Set</li>
<li>Multimeter</li>
<li>Soldering Iron</li>
<li>Solder</li>
<li>Solder Flux</li>
<li>Wire</li>
</ul>
<hr>
<img src="https://sequentialread.com/content/images/2024/08/kettle1.jpg" alt="Bonavita 1L Electric Kettle Repair"><p>My friend who was a Barista gave us this nice Bonavita electric Kettle when he moved out:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/kettleclean.jpeg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><p></p><!--kg-card-begin: markdown--><p>And it worked for years and years, even while we had multiple people living in the house and using the kettle every day.</p>
<p>However, it started getting harder and harder to power it on; I would have to press the power button down for longer and longer to get it to start heating.</p>
<p>Eventually it simply wouldn't turn on anymore at all.</p>
<p>Personally, I've never tried to repair electronics like that. But after my initial experience with DC electronics (making a Lead Acid + solar battery for my <a href="https://sequentialread.com/docker-on-odroid-xu4-installation-and-creating-a-base-image-2/">Odroid XU4</a>),</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/pv.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>and also seeing multiple people pull off soldering projects at <a href="https://layerze.ro">Layer  Zero</a>, I was inspired to try my hand.</p>
<p>Unfortunately for me, I was immediately confronted with anti-repair practices from Bonavita:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/trilobe.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>Why the hell do they have tri-lobe screws on the base of this thing? What the actual fuck???</p>
<p>This is especially bizzare once I got the bottom off (I used a flathead small enough to jam into the trilobe, lol).</p>
<p>Then I discovered phillips head screws holding the PCBs in place:</p>
<p><img src="https://sequentialread.com/content/images/2024/08/kettle1.jpg" alt="Bonavita 1L Electric Kettle Repair"></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Observant readers may have already spotted something very suspicious on the bottom right: What's that little electrolytic capacitor shroud doing there!?!?!?</p>
<p>Turning over the Power Electronics board, I found where it came from:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/kettle2.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>Obviously I found the problem, so got to work replacing this capacitor. (Foreshadowing, lol).</p>
<p>Comments I got on the <a href="https://cyberia.club/matrix">Cyberia Matrix chat</a> about the project:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><div class="chat ">
<img src="https://sequentialread.com/content/images/2024/08/ivy.jpg" alt="Bonavita 1L Electric Kettle Repair">
<div>
	<span class="username bluepurple"> 
        ivy (she/her)
    </span>
    <div class="body self"> 
        <div>pretty sure you can just pour some gatorade on that and tape it up</div>
        <div>
        <span style="background:rgba(0,0,0,0.1); border: 1px solid rgba(0,0,0,0.2);  padding: 0 5px;  border-radius: 0.7em; display: inline-flex; flex-direction:row; align-items:center; justify-content:center; position:relative; top:3px;"> 👍 3</span>
<span style="background:rgba(0,0,0,0.1); border: 1px solid rgba(0,0,0,0.2);  padding: 0 5px;  border-radius: 0.7em; display: inline-flex; flex-direction:row; align-items:center; justify-content:center; position:relative; top:3px;"> 😂 1</span>
        </div>
        <br>
    </div>
    
</div>
</div>



<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/08/forest.jpg" alt="Bonavita 1L Electric Kettle Repair">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <div>its what plants crave</div>
        <div>
<span style="background:rgba(0,0,0,0.1); border: 1px solid rgba(0,0,0,0.2);  padding: 0 5px;  border-radius: 0.7em; display: inline-flex; flex-direction:row; align-items:center; justify-content:center; position:relative; top:3px;"> 😂 3</span>
        </div>
        <br>
    </div>

</div>
</div>


<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/08/c.png" alt="Bonavita 1L Electric Kettle Repair">
<div>
	<span class="username bluepurple"> 
        carbide_flux
    </span>
    <div class="body self"> 
<p>
        Naughty manufacturer putting an electrolytic next to a heating element. You should be fine to replace it. I suggest replacing the other one too.</p>
    </div>
</div>
</div>



<hr>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Later that week, I traveled to <a href="https://layerze.ro/">Layer Zero</a> where there's a great soldering station and a large pile of tech waste to pick from.</p>
<p>There, I found a capacitor with the exact same matching specs, <code>16v 470 microfarad</code>, on an old ethernet switch I found in the tech waste bin.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><div class="chat ">
<img src="https://sequentialread.com/content/images/2024/08/forest.jpg" alt="Bonavita 1L Electric Kettle Repair">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <img src="https://sequentialread.com/content/images/2024/08/cap2.jpg" alt="Bonavita 1L Electric Kettle Repair" style="width: 500px; margin-bottom:1em;">

<img src="https://sequentialread.com/content/images/2024/08/cap3.jpg" alt="Bonavita 1L Electric Kettle Repair" style="width: 500px; margin-bottom:1em;">
        <p>From Ethernet switch</p>
    </div>


</div>

</div><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>I had de-soldered the replacement capicitor from its home on the ancient ethernet switch's board. Now I had to figure out how to attach it to the Power Electronics board of the kettle.</p>
<p>But first, I had to de-solder the exploded capacitor from the power electronics board.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><blockquote>
  <b>De-soldering and pulling the broken capacitor off of the board</b>
    <p>Music: <i>Boards of Canada -- poppy seed / everything you do is a balloon</i></p>
  <br>
<em>1 minute 0 seconds</em>
</blockquote>
<br>
<div style="display: flex; justify-content: center; background-color: #aaa;">
  <video autobuffer controls preload="auto" style="max-height: 90vh;" width="100%">
        <source src="https://picopublish.sequentialread.com/files/kettle-repair-1.mp4" type="video/mp4">
  <p>Your browser doesn't support HTML5 video. Here is
     a <a href="https://picopublish.sequentialread.com/files/greenhouse-alpha-demo-windows.mp4">link to the video</a> instead.</p>
  </video>
</div><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Here are the two capacitors next to each-other, the exploded one and the replacement:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/replacement.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>I tried a couple different ways to solder the new capacitor onto the kettle's Power Electronics board.</p>
<p>At first I thought i'd find some thick wire and solder short segments of it onto the holes on the circuitboard, then solder the wires onto the contacts of the capacitor.</p>
<p>However, the only thick wire I could find was braided from a gazzillion smaller strands, and trying to solder it to the PCB was extremely frustrating and taxing.</p>
<p>In the end, I gave up. I couldn't seem to get a clean solder joint with that wire. So next, I thought I'd try a breadboard jumper wire, similar to the one pictured:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/wjw010.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>I was able to get a nice secure joint when soldering the ends of one of those onto the board, but wasn't sure how to connect the capacitor.</p>
<p>At first, I tried stripping the middle wire segment.  It was hard to strip, the wire was multiple tiny copper strands encased in colorful rubber.  It was really hard to remove the rubber without cutting some of the strands.</p>
<p>To make it worse, I wasn't sure if  I would have enough space to solder the capacitor onto those copper wires. The plastic end-housings were taking up a lot of real-estate on the tiny board.</p>
<p>So in the end, I bit the bullet and tried to clip off the plastic end-housings. To my delight, they split off cleanly, leaving just the thick contact wires and little sprigs of copper behind.</p>
<p>After that, I was easily able to bend them into shape, solidify the solder joint to the underlying circuitboard, and finally solder the capacitor onto them.</p>
<blockquote>
<p>ℹ️<strong>NOTE</strong>: I wasn't sure how to tell which way to solder the capacitor -- I'm not sure if they have a &quot;direction&quot; or not like Diodes do. I guessed that they do. But becuase there was another capacitor on the same board, it gave me a clue.  The capacitors all seem to have this directional arrow printed on the outside of the housing -- and the circuitboard underneath showed the outline of the capcitor with one side shaded and the other side clear.  I assumed via  intuition that the shaded side goes with the directional arrow on the outside of the capacitor, because that's what I observed on the one that had not exploded.</p>
</blockquote>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/shaded47.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/joints4.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>Even though this circuitboard has very large features and should be a lot easier to repair compared to a smaller-scale logic board, I still struggled with the solder joints and had to re-do them the first time.</p>
<p>I didn't use any flux at first and quickly regretted it.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/joints.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>Excited, I immediately put the kettle back together, confident it would turn on.</p>
<p>However...</p>
<p>It still seemed very sketchy.  When I pressed the power button, nothing happened.  I had to press it multiple times and hold it down for multiple seconds before the kettle finally sprung to life and started heating.</p>
<h1 id>😰</h1>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/dark1.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: html--><pre style="font-size:  16px; line-height: 15px;">
                                                                                              
                                              ░░                                              
                                                                          ░░                  
              ░░                              ░░                          ░░                  
              ░░        ░░                    ░░                        ░░░░                  
              ░░    ░░  ░░                    ░░      ░░                ░░░░            ░░    
      ░░      ░░    ░░  ░░      ░░            ▒▒      ░░    ░░            ░░            ░░    
░░  ░░░░      ░░    ░░  ░░  ░░  ░░            ▓▓      ░░    ░░░░          ░░            ░░    
░░░░░░░░      ░░    ░░  ░░░░▒▒░░░░        ░░  ▓▓░░    ░░    ░░      ░░  ░░    ░░░░      ░░  ░░
▒▒░░  ░░      ░░  ░░░░  ▒▒░░▓▓░░░░░░      ▒▒  ▓▓▒▒    ░░  ░░░░  ░░  ░░  ░░  ░░░░░░    ░░░░  ░░
▒▒░░▒▒▒▒░░    ▒▒░░░░░░  ▒▒░░▓▓░░░░░░      ▒▒░░██▒▒░░░░░░  ░░▒▒░░░░░░▒▒  ▒▒░░░░░░░░    ░░░░  ▒▒
▒▒░░▒▒▒▒░░  ░░▒▒░░░░░░░░▒▒▒▒▒▒▒▒▒▒░░░░  ░░▒▒░░▓▓▒▒░░░░▒▒  ▒▒▒▒▓▓░░▓▓▒▒▒▒  ░░▒▒▒▒▒▒░░▒▒▒▒░░░░▒▒
▒▒▒▒▓▓▒▒░░░░░░▒▒▒▒░░▒▒░░▒▒▒▒▒▒▒▒▒▒░░▒▒░░▒▒▓▓░░▓▓▒▒▒▒░░▒▒░░▒▒▒▒██  ▓▓▒▒░░▒▒  ▒▒▓▓▒▒░░▒▒▒▒  ▒▒██
▒▒▒▒▓▓▒▒▒▒░░▒▒▒▒▒▒▒▒▓▓░░▒▒▒▒▒▒▓▓▒▒░░▓▓░░▒▒▒▒░░▓▓▒▒▒▒▒▒▓▓▒▒▓▓░░██░░▓▓▒▒▒▒▓▓░░▒▒▓▓▒▒▒▒▒▒▓▓▒▒▒▒██
▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒░░▓▓▓▓▒▒▒▒▒▒▓▓██░░▒▒▒▒▒▒▓▓░░▒▒▒▒▒▒▒▒▓▓██░░██▒▒▓▓░░░░▓▓▒▒▓▓░░▒▒▓▓▓▓▒▒▒▒▓▓▒▒▒▒▓▓
▒▒▓▓▒▒▒▒▓▓██░░▓▓░░▒▒▓▓▒▒▒▒▓▓▓▓██░░▒▒▒▒▓▓▒▒░░▒▒░░▒▒░░▓▓▓▓░░▒▒░░▒▒▒▒░░▓▓▒▒▓▓  ░░▓▓▓▓▓▓▒▒▒▒▒▒░░░░
▒▒▓▓░░▒▒▒▒▒▒░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▓▓░░░░░░▓▓░░░░▒▒░░▒▒░░▓▓▒▒░░░░░░▒▒▒▒░░▓▓▒▒▓▓  ▒▒░░▒▒▒▒▒▒▒▒▓▓▒▒  
░░▒▒  ▒▒░░▒▒  ░░▒▒░░  ░░▒▒▓▓▒▒▓▓░░  ░░▓▓▒▒  ░░░░▓▓  ▒▒░░░░░░  ██▒▒  ▒▒░░▓▓░░▒▒░░▒▒░░▒▒▒▒▓▓▒▒  
░░░░  ▒▒▒▒░░░░  ▒▒  ▒▒  ▒▒░░░░▓▓  ░░░░▒▒░░  ░░░░▒▒  ▒▒░░    ░░░░▒▒  ▒▒░░▓▓▒▒▒▒  ░░  ▒▒░░▒▒▒▒  
      ░░░░░░░░░░    ░░░░▒▒▒▒  ░░      ▒▒░░  ░░░░░░  ▒▒░░      ░░▒▒░░░░░░░░▒▒░░      ▒▒░░░░▒▒  
░░    ░░  ░░        ░░  ▒▒░░  ░░            ░░░░░░  ▒▒░░        ░░  ░░░░░░░░░░      ░░░░░░░░  
      ░░░░  ░░  ░░      ░░    ░░        ░░    ░░    ▒▒░░        ░░  ░░  ░░░░        ▒▒░░░░░░  
      ░░░░                              ░░          ▒▒░░                                  ░░  
      ░░░░                              ░░          ░░                      ░░                
      ░░░░                              ░░      ░░                                            
                                                                             
</pre>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>After a few days off from the project, I came back with a fresh look.</p>
<p>Before I opened it, my intuition had been that the power button was dying.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->
<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/08/forest.jpg" alt="Bonavita 1L Electric Kettle Repair">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <p>Does anyone have tri lobe screw driver(s)?<br>
            We have this nice electric kettle thingy<br>
        It stopped working, my suspicion is that the power button wore out</p>
        <br>
    </div>

</div>
</div>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Upon re-inspection, I was able to tell that the voltage going to the control board seemed to be fine -- I saw a steady <code>5v DC</code> on the header pins labeled <code>VCC</code> / <code>GND</code></p>
<p>When I'd talked to my partner about it, they'd mentioned that they'd observed a difference betwen simply pressing the power button and dragging one's finger across it deliberately perpendicular to the switch's direction of travel -- and that sometimes dragging caused it to work better.</p>
<p>I had never noticed this, and it gave me a suspicion that perhaps my original idea, the power button,  was right all along.</p>
<p>So I opened the stinker up again and started looking at the digital control board and all the switches on it.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><blockquote>
    <p>ℹ️<b>NOTE:</b> I had a humbling experience with this...   When I was a kid, my father gave me a <code>TENMA 72-7720</code> multimeter as a birthday gift. </p>
<p>This thing definitely works,</p>
  <img src="https://sequentialread.com/content/images/2024/08/tenma.JPG" alt="Bonavita 1L Electric Kettle Repair" style="width: 300px; margin-bottom:1em;">
<p>however, it's slow to update its display, and requires manual adjustment to get to the right setting for probing whatever circuit you might be faced with.</p>
    <br>
    <p>At <a href="https://layerze.ro/">Layer Zero</a>, someone donated a modern <code>Fluke 117</code> multimeter. It makes a world of difference. It automatically detects the range of the measurement instead of requiring the user to select it, and it updates its display instantaneously. It also features a log-scale display of the measurement in the form of a segmented bar display-- this ended up being a lifesaver for this project.</p>

  <img src="https://sequentialread.com/content/images/2024/08/fluke.jpg" alt="Bonavita 1L Electric Kettle Repair" style="width: 300px; margin-bottom:1em;">
    <p>I recognized the <i>Fluke</i> brand name as a quality brand from listening to the <a href="https://www.youtube.com/c/MarcoReps">Marco Reps</a> youtube channel, and this thing definitely did not dissapoint. </p>
</blockquote>

<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Using the Fluke at <a href="https://layerze.ro/">Layer Zero</a>, connecting the  probes across the power switch pins, I saw something very interesting which I had missed with the Tenma.</p>
<blockquote>
<p>ℹ️ <strong>INFO</strong>:  In part, this was due to my ignorance combined with the un-repair-ability of the Bonavita kettle.  The kettle's digital control circuitboard's bottom side had been coated with some sort of insulating material -- I had to scrape and scrape and scrape at the solder joints before I could get any reading at all.  To be fair, there's probably a good reason for this coating.. But it's annoying when I'm trying to fix the thing.</p>
<p>The view from the Tenma was just confusing -- I wasn't physically able to hold the probes steady in place for long enough to see a change within whatever narrow sensing range I had selected. Even after I started agressively scraping the coating off.</p>
<p>But once I had it under the Fluke at <a href="https://layerze.ro/">Layer Zero</a>, I started seeing things. The real-time display (less than 10ms update latency) really illuminated the problem.  Basically, the power button switch was toasted.</p>
<p>Instead of showing &quot;infinity&quot; ohms when it was open, it showed about 4 mega-ohms.  When I scraped the coating off of other switches on the control board and tested them, they showed infinity ohms when open.</p>
<p>When the power switch was &quot;closed&quot; (power button pressed), the measurement would vary wildly between mega ohms, kilo ohms, etc, as I wiggled my finger around on the power button. The log scale display bar on the bottom of the Fluke's LCD made this obvious. This explains what my partner had experienced w/ directionality of the way they pressed the button influencing success rate!</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>So, I decided to replace the power button.  Looking around at all the other buttons on the device, I decided I would sacrifice the <code>Centigrade / Farenheit</code> toggle button, and use it to replace the power button.</p>
<p>This went fairly easily, I had to melt and detach one 2-pin side of the 4-pin button at a time, but besides that, I didn't have any issues.</p>
<p>However.....</p>
<p>I put the thing back together again, and excitedly pushed the power button. To my delight, it instantly sprang to life on the first try, unlike before. I thought it was <strong>FIXED!!!!</strong></p>
<p>....</p>
<p>I was wrong. After it turned on, instead of starting to heat, it clicked off and displayed <code>0F</code> on the LCD.</p>
<p>....</p>
<p>Further investigation revealed that the switch I had removed was in fact an important part of the circuit for the temperature sensing feature -- It was the only switch on the board for whoom the horizontal sides of the switch bridged a PCB trace.</p>
<h2 id="pinlayoutofeveryswitchontheboardexceptthecelciusfahrenhietswitch">Pin Layout of Every Switch on the Board Except the Celcius / Fahrenhiet switch:</h2>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/image.png" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><h2 id="pinlayoutofthecelciusfahrenhietswitch">Pin Layout of the Celcius / Fahrenhiet switch:</h2>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/image-2.png" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><p>It turns out that because of the type of switch the manufacturer had used, according to my housemate who's a technician / engineer,</p>
<blockquote>
<p><em>Single pole double throw switch</em></p>
</blockquote>
<p>By removing the Celcius / Fahrenhiet switch I had broken the device. The switch was conducting between those two wires by default until it was pressed, but I had broken that connection by removing it.</p>
<p>So, finally, I got a couple small segements of wire and soldered them long-ways across the place where the switch had originally been:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/08/bridge2.jpg" class="kg-image" alt="Bonavita 1L Electric Kettle Repair"></figure><!--kg-card-begin: markdown--><h3 id="afterthisthekettlewasgoodasnewherestofourmoreyearsofmorningcoffee">After this, the kettle was good as new! Here's to four more years of morning coffee.</h3>
<h1 id="iaintbuyinshit"><em><strong>I ain't buyin' shit!!!</strong></em></h1>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Cross-Platform Statically Compiled Way to Read HEIC Images]]></title><description><![CDATA[There are tons of excellent peices of software out there for handling images -- Krita, ImageMagick, and others. However, as far as I can tell, none of them natively support HEIC images. Frankly, it's a very bad vibes situation.]]></description><link>https://sequentialread.com/cross-platform-statically-compiled-way-to-read-heic-images/</link><guid isPermaLink="false">669cab04d41d850001a29f57</guid><category><![CDATA[brand shaming]]></category><category><![CDATA[desktop applications]]></category><category><![CDATA[for fun]]></category><category><![CDATA[FLOSS]]></category><category><![CDATA[products]]></category><category><![CDATA[tools]]></category><category><![CDATA[windows]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Sun, 21 Jul 2024 07:03:57 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2024/07/convert.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2024/07/convert.jpg" alt="Cross-Platform Statically Compiled Way to Read HEIC Images"><p>Through a cruel coincidence, my mother has an iPhone, but her computer runs Windows.</p>
<p>She has struggled for years with this situation at every stage. Very uncool moves from both Apple and Microsoft have conspired to keep her phone and computer apart, and even to this day, she still couldn't figure out how to open the photos she takes with her phone...</p>
<p>There are tons of excellent peices of software out there for handling images -- Krita, ImageMagick, and others. However, as far as I can tell, none of them natively support HEIC images. Frankly, it's a very bad vibes situation.</p>
<p>So, after coming up emptyhanded, with nothing I could recommend to her that would directly open a HEIC file, I decided to take matters into my own hands and just make one. Little did I know, I was about to learn more than I ever really wanted to about why none of these exist.</p>
<p><strong>EDIT:</strong> It turns out that the Windows Photos app actually was converting the HEIC photos to jpeg, she just wasn't able to find where the photos app was putting the jpeg files.  So this might not be important for windows users, but I'm keeping this post up in case anyone needs to write, for example, a web application that can read HEIC images.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/07/demo.gif" class="kg-image" alt="Cross-Platform Statically Compiled Way to Read HEIC Images"></figure><!--kg-card-begin: markdown--><h2 id="coderepository">Code Repository</h2>
<p><a href="https://git.sequentialread.com/forest/heic-converter-gui">https://git.sequentialread.com/forest/heic-converter-gui</a></p>
<p>This application is written in Go, but the actual image conversion is handled by <code>libde265</code> (C++ code) that was compiled to WebAssembly via Emscripten and runs inside an embedded WebAssembly runtime that was written in Go. (Similar to <a href="https://xeiaso.net/blog/carcinization-golang">The Carcinization of Go Programs 🦀</a>)</p>
<p>This is really good because it should work on any CPU architechture and any OS platform, there are no annoying platform-dependent C/C++ compiler issues to deal with.  It runs about 10 times slower than the native option, but it works, and it's really easy to use, so I think thats what matters most.</p>
<p>🙇 <strong><em>Big thanks to the person who made that happen!</em></strong>  🙇</p>
<h2 id="downloadlinks">Download Links</h2>
<ul>
<li><strong>Windows 64 bit</strong> <a href="https://picopublish.sequentialread.com/files/heic-converter-f451bb.exe">https://picopublish.sequentialread.com/files/heic-converter-f451bb.exe</a></li>
<li><strong>Linux 64 bit</strong> <a href="https://picopublish.sequentialread.com/files/heic-converter-8dd865">https://picopublish.sequentialread.com/files/heic-converter-8dd865</a></li>
</ul>
<h2 id="usagenotes">Usage Notes</h2>
<p>If you want lossless output you can specify <code>png 1</code> (the quality number is ignored for PNG).</p>
<p>Only use png if you want to edit the image and then compress it further with jpeg / avif. The png files will be massive.</p>
<p>This tool strips all EXIF data but it should preserve the rotation that is present in the EXIF <code>Orientation</code> attribute.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Words in a base64-encoded string... Which decodes to the same words in ascii]]></title><description><![CDATA[... so if you imagine f is the computation you want to remain the same regardless of whether it's operating on a base64'd input or not, and g is the base64 function, and x is a not-base64'd input, then f(g(x)) = g(f(x)) 

...  

Oooooohhhhh yes so f = user reads the word 'example', g = base64 ]]></description><link>https://sequentialread.com/words-in-base64-encoded-string-which-decodes-to-same-words-in-ascii/</link><guid isPermaLink="false">666a586deb92490001732279</guid><category><![CDATA[golang]]></category><category><![CDATA[for fun]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Thu, 13 Jun 2024 21:55:17 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2024/06/leet.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2024/06/leet.png" alt="Words in a base64-encoded string... Which decodes to the same words in ascii"><p>After over a year I'm trying to get back to posting here...  A lot has been going on in my life and I haven't had much time or motivation to work on my own personal stuff.</p>
<hr>
<p>I have heard of this concept called a <strong>&quot;polyglot&quot;</strong>, a polyglot is a file that's valid in multiple different formats, or source code which compiles/runs in multiple different languages.</p>
<p>But what I'm after this time is not quite that...  I want...  A chunk of data which displays the same (or similar) string when represented in multiple different encodings (Base64 and ascii). I'm not sure what to call this.</p>
<p>I asked the <a href="https://cyberia.club/matrix">Cyberia Chat</a> and got a couple different interesting ideas:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->

<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/forest-ava3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username self"> 
        forest (he/him)
    </span>
    <div class="body self"> 
        <p>OK so a polyglot is a program that runs OK as two different languages</p>
        <p>What do you call something which functions the same regardless of whether it's encoded or decoded according to some scheme? (Base64 let's say)...??</p>
        <p>And maybe "functions" defined quite loosely, like a "vanity" hash or something, it doesn't have to be exact, just legible</p>
        <p>Idk if there is a name for this and now I wanna try to make one, LOL!</p>
    </div>
</div>
</div>

<hr>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><h2 id="fixedpointvanitypoint">&quot;Fixed Point&quot; -&gt; &quot;Vanity Point&quot;</h2>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->

<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/symys3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username pink"> 
        SYMYƧ
    </span>
    <div class="body self"> 
        <p>I think eigenvalue is the closest since you are essentially saying you put a value through an encoding and the value remains unchanged , or only changed within certain limits</p>
        <p>I think there's a more general term than eigenvalue though, but I'm blanking on it, but also I'm pretty sure most encodings can be phrased as linear algebra anyway, so 🤷‍♂️</p>
        <p>"fixed point", that's the term</p>
    </div>
</div>
</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/forest-ava3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username self"> 
        forest (he/him)
    </span>
    <div class="body self"> 
        <p>How bout a 'vanity point' of a given encoding</p>
    </div>
</div>
</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/symys3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username pink"> 
        SYMYƧ
    </span>
    <div class="body self"> 
        
		<blockquote>   
<p>
        <span style="padding: 0 5px;  display: inline-flex; flex-direction:row; align-items:center; justify-content:center;position:relative; top:3px;"><img style="height:1em; border-radius: 1em;" src="https://sequentialread.com/content/images/2024/06/forest-ava3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii"> <span style="line-height: initial;">&nbsp; forest (he/him)</span></span>
        </p>
<p> How bout a 'vanity point' of a given encoding
            </p>
</blockquote>
        <p>Oh I like that!</p>
    </div>
</div>
</div>
<hr>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><h2 id="homomorphism">Homomorphism</h2>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->

<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/hemant3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username green"> 
        hemant (he/they)
    </span>
    <div class="body self"> 
        <p>yeah the term you're looking for is homomorphism. if <code>f</code> is homomorphic with respect to <code>g</code> then calling <code>f</code> and <code>g</code> is commutative</p>
    </div>
</div>
</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/forest-ava3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username self"> 
        forest (he/him)
    </span>
    <div class="body self"> 
        <p>I'm sorry I would have to go over these math words again</p>
    </div>
</div>

</div>
<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/hemant3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username green"> 
        hemant (he/they)
    </span>
    <div class="body self"> 
        <p>like you can do <code>f</code>  first then <code>g</code> , or <code>g</code>  first then <code>f</code>. and it would be the same</p>
        <p>so if you imagine <code>f</code> is the computation you want to remain the same regardless of whether it's operating on a base64'd input or not, and <code>g</code> is the base64 function, and <code>x</code> is a not-base64'd input, then <code>f(g(x)) = g(f(x))</code></p>
    </div>
</div>
</div>


<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/forest-ava3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username self"> 
        forest (he/him)
    </span>
    <div class="body self"> 
        <blockquote>   
<p>
        <span style="padding: 0 5px;  display: inline-flex; flex-direction:row; align-items:center; justify-content:center;position:relative; top:3px;"><img style="height:1em; border-radius: 1em;" src="https://sequentialread.com/content/images/2024/06/hemant3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii"> <span style="line-height: initial;" class="username green">&nbsp; hemant (he/they)</span></span>
        </p>
           <p>so if you imagine <code>f</code> is the computation you want to remain the same regardless of whether it's operating on a base64'd input or not, and <code>g</code> is the base64 function, and <code>x</code> is a not-base64'd input, then <code>f(g(x)) = g(f(x))</code></p>
         </blockquote>
      <p> Oooooohhhhh yes so <code>f</code> = user reads the word 'example', <code>g</code> = base64
            </p>
    </div>
</div>
</div>




<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/hemant3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username green"> 
        hemant (he/they)
    </span>
    <div class="body self"> 
        
        <p>yep</p>
    </div>
</div>
</div>
    
    
    
<div class="chat ">
<img src="https://sequentialread.com/content/images/2024/06/forest-ava3.jpg" alt="Words in a base64-encoded string... Which decodes to the same words in ascii">
<div>
	<span class="username self"> 
        forest (he/him)
    </span>
    <div class="body self"> 
      <p> Makes sense thx</p>
    </div>
</div>
</div>

<hr>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>I'm not sure which name I like best,</p>
<ul>
<li>A vanity point on the Base64 function</li>
<li>A value whose legibility is homomorphic over the Base64 function</li>
</ul>
<p>But regardless, I had already gotten excited and decided I was going to do this, so now I had to figure out how, and where to start.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2024/06/Screenshot-from-2024-06-12-19-16-26.png" class="kg-image" alt="Words in a base64-encoded string... Which decodes to the same words in ascii"></figure><!--kg-card-begin: markdown--><br>
<p>At first, was completely lost on how to do this. I knew would need to visualize the process a bit, so without knowing what my solution was going to end up being, I decided to simply write a program that displays the process of base64 encoding and decoding down to the individual bits involved.</p>
<p>My code would print out the same data from both the encoding perspective and the decoding perspective: as ascii characters, as decimal, and as binary.</p>
<p>During this process I learned a bit more about Base64, stuff that I already knew, but never really felt so viscerally before:</p>
<ul>
<li>An ascii character is <code>1 byte</code>, <code>8 bits</code></li>
<li>However, only <code>6 bits</code> are represented by a single character in a Base64 string.
<ul>
<li><code>2 ^ 6 = 64</code></li>
</ul>
</li>
<li>There is a 3/4 ratio here -- <code>3 bytes = 4 characters</code> in Base64.
<ul>
<li>This means (in byte-aligned ascii terms) the bit-wise &quot;meaning&quot; of a given Base64 character will change depending on its position</li>
<li>Each subsequent character &quot;shifts&quot; the next one by two bits.</li>
</ul>
</li>
</ul>
<p>Eventually I was able to get my visualization to line up, and I came to a conclusion that seems obvious in retrospect:</p>
<p>The ascii string and Base64 string both represent the exact same binary string, the same bits.</p>
<p>Base64 can represent any bit string... While only some bit strings are valid ascii.</p>
<p>So I needed to try writing out words into a base64 encoded string in different ways, and continually check to make sure that the resulting bits had a valid ascii decoding.</p>
<p>I decided I would:</p>
<ol>
<li>Represent the bits as a <code>string</code> of <code>1</code>s and <code>0</code>s, since it was more familiar to me.</li>
<li>Check each newly-written 8 bits against a list of valid 8-bit strings for ascii characters.</li>
<li>Use a recursive function (depth-first-search) to find solutions</li>
<li>Give my search function multiple different ways to write words into the Base64 encoded string (1337 speak!) so it's less likely to get stuck and never find a solution which has a valid ascii encoding.</li>
</ol>
<pre><code>var leetSp34k = map[string][]string{
	&quot;a&quot;: {&quot;4&quot;, &quot;@&quot;},
	&quot;b&quot;: {&quot;6&quot;, &quot;8&quot;},
	&quot;e&quot;: {&quot;3&quot;},
	&quot;i&quot;: {&quot;1&quot;},
	&quot;j&quot;: {&quot;7&quot;},
	&quot;l&quot;: {&quot;1&quot;},
	&quot;o&quot;: {&quot;0&quot;},
	&quot;q&quot;: {&quot;9&quot;},
	&quot;s&quot;: {&quot;$&quot;, &quot;5&quot;},
	&quot;t&quot;: {&quot;+&quot;, &quot;7&quot;},
	&quot;u&quot;: {&quot;v&quot;},
	&quot;v&quot;: {&quot;u&quot;},
	&quot;z&quot;: {&quot;2&quot;},
}
</code></pre>
<p>I got promising results right away when trying to generate the word <code>Example</code>:</p>
<pre><code>cFExam91
011100000101000100110001011010100110111101100101
cFExam9l
011100000101000100110001011010100110101001
cFExamp
011100000101000100110001011010100110101001001011
cFExampL
011100000101000100110001011010100110101001110101
cFExamp1
011100000101000100110001011010100110101001100101
cFExampl
011100000101101000110111010111
cFo3X
011100000101101000110111010111000000
cFo3XA
011100000101101000110111010111111000
cFo3X4
011100000101101000110111010111111000001100
cFo3X4M
011100000101101000110111010111111000100110
</code></pre>
<p>And with a bit of tuning, I was able to churn out much longer strings. Allowing the search function to repeat characters helped it find many more valid solutions and extend the length of strings it could represent:</p>
<pre><code>$ echo 'UjBlOGExamp1elBLOGExamplelBLOGExamplelll' | base64 -d
R0e8a1jjuzPK8a1jjezPK8a1jjezYe
</code></pre>
<p>And if we suffix the  decoded string with our chosen text, we get at least a basic version of the thing we were originally after: a value that &quot;says the same thing&quot; regardless of whether it's base64 encoded or not:</p>
<pre><code>$ echo 'R0e8a1jjuzPK8a1jjezPK8a1jjezYeBlogExample1BlogExample1BlogExample' | base64 -w 0
UjBlOGExamp1elBLOGExamplelBLOGExamplelllQmxvZ0V4YW1wbGUxQmxvZ0V4YW1wbGUxQmxvZ0V4YW1wbGUK
</code></pre>
<p>This &quot;half-and-half&quot; approach is as good as I can do so far, I think...  But I would be interested if anyone else has better ideas.</p>
<p>I did also notice some interesting things: some characters seem to work better than others depending on thier position in the string. I noticed <code>t</code> and <code>r</code> to be particularly troublesome: at least with my naive depth-first-search algorithm, I was not able to find a solution for <code>BlogExampleString</code>, but <code>String</code> worked fine.</p>
<p>At any rate, I'm sure there is more to explore, but for now, I'm ready to call it done.</p>
<h1 id="thecode">The Code:</h1>
<h2 id="gitsequentialreadcomforestbase64reverseencoder"><a href="https://git.sequentialread.com/forest/base64-reverse-encoder/src/branch/main">git.sequentialread.com/forest/base64-reverse-encoder</a></h2>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Matrix Synapse Out of Disk Space state_groups_state]]></title><description><![CDATA[You log in with your matrix account, And then you can see what's using disk space. if there are any big rooms, you can delete them, including deleting the state_groups_state rows]]></description><link>https://sequentialread.com/matrix-synapse-out-of-disk-space-state_groups_state/</link><guid isPermaLink="false">63bc5c2eaaa5360001e877e0</guid><category><![CDATA[cyberia]]></category><category><![CDATA[matrix]]></category><category><![CDATA[backend]]></category><category><![CDATA[FLOSS]]></category><category><![CDATA[golang]]></category><category><![CDATA[Monitoring]]></category><category><![CDATA[operations]]></category><category><![CDATA[products]]></category><category><![CDATA[tools]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Tue, 10 Jan 2023 00:07:08 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2023/01/state_groups_state.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2023/01/state_groups_state.jpg" alt="Matrix Synapse Out of Disk Space state_groups_state"><p>Matrix certainly has its problems, which folks are often quick to criticize:</p>
<ul>
<li>The protocol design makes moderation harder than it should be</li>
<li>There's no &quot;admin panel&quot; for a matrix server; it's administered via CLI</li>
<li>It has bugs and can be hard to maintain</li>
<li>IRC users hate it because the matrix IRC bridge was made by matrix people not IRC people, so it ham-fistedly tries to fit matrix content into IRC instead of trying to limit a bridged room to content that IRC supports.</li>
</ul>
<p>But I think Matrix's benefits outweigh its drawbacks, and I am happy to support the <a href="https://cyberia.club/matrix">cyberia.club matrix server</a> as its needed.</p>
<p>Disk space has been our single most troublesome problem so far.</p>
<p>For a long time, we kicked the can down the road by increasing the disk space on the VM we use to host our matrix server. But obviously, we can't do this forever.</p>
<p>Upon further investigation, we learned that the postgres database was the one using up all the disk space, and specifically, that one table in the database, called <code>state_groups_state</code>, was responsible for that.</p>
<p>Here is an excerpt from the &quot;Origin Story&quot; on the <a href="https://git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor">matrix-synapse-diskspace-janitor repository</a>:</p>
<hr>
<p>The problem at hand:</p>
<p>Matrix-synapse stores a lot of data that it has no way of cleaning up or deleting.</p>
<p>Specifically, there is a table it creates in the database called <code>state_groups_state</code>:</p>
<pre><code>root@matrix:~# sudo -u postgres pg_dump synapse -t state_groups_state --schema-only
--
-- PostgreSQL database dump
--
...

CREATE TABLE public.state_groups_state (
    state_group bigint NOT NULL,
    room_id text NOT NULL,
    type text NOT NULL,
    state_key text NOT NULL,
    event_id text NOT NULL
);
</code></pre>
<p>I don't understand what this table is for, however, I can recognize fairly easily that it accounts for the grand majority of the disk space bloat of a matrix-synapse instance:</p>
<h4 id="top10tablesbydiskspaceusedcyberiaclubinstance">top 10 tables by disk space used, cyberia.club instance:</h4>
<p><img src="https://git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor/raw/commit/a11f555307507a4840e634eed27db6866cd9edb1/readme/state_groups_state.png" alt="Matrix Synapse Out of Disk Space state_groups_state"></p>
<p>So, I think it's safe to say that if we can cut down the size of <code>state_groups_state</code>, then we can solve our disk space issues.</p>
<p>I know that there are other projects dedicated to this, like <a href="https://github.com/matrix-org/rust-synapse-compress-state">https://github.com/matrix-org/rust-synapse-compress-state</a></p>
<p>However, a cursory examination of the data in <code>state_groups_state</code> led me to believe maybe there is an easier and better way.</p>
<p><code>state_groups_state</code> <em>DOES</em> have a <code>room_id</code> column on it. It's not <em>indexed</em> by <code>room_id</code>, but we can still count the # of rows for each room and rank them:</p>
<h4 id="top100roomsbynumberofstate_groups_staterowscyberiaclubinstance">top 100 rooms by number of <code>state_groups_state</code> rows, cyberia.club instance:</h4>
<p><img src="https://git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor/raw/commit/a11f555307507a4840e634eed27db6866cd9edb1/readme/top100rooms.png" alt="Matrix Synapse Out of Disk Space state_groups_state"></p>
<p>In summary, it looks like</p>
<blockquote>
<p><strong>about 90% of the disk space used by matrix-synapse is in <code>state_groups_state</code>, and about 90% of the rows in <code>state_groups_state</code> come from just a handfull of rooms</strong>.</p>
</blockquote>
<p>So from this information we have hatched a plan:</p>
<blockquote>
<p><em>Just delete those rooms from our homeserver <img src="https://git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor/raw/commit/a11f555307507a4840e634eed27db6866cd9edb1/readme/4head.png" alt="Matrix Synapse Out of Disk Space state_groups_state"></em></p>
</blockquote>
<p>However, unfortunately the <a href="https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#version-2-new-version">matrix-synapse delete room API</a> does not remove anything from <code>state_groups_state</code>.</p>
<p>This is similar to the way that the <a href="https://github.com/matrix-org/synapse/blob/develop/docs/message_retention_policies.md">matrix-synapse message retention policies</a> also do not remove anything from <code>state_groups_state</code>.</p>
<p>In fact, probably helps explain why <code>state_groups_state</code> gets hundreds of millions of rows and takes up so much disk space: Nothing ever deletes from it!!</p>
<p>We did come up with some shell scripts to handle this, but it was an annoying recurring manual maintenance burden.  Due to the <a href="https://picopublish.sequentialread.com/files/matrix-manual-room-bonk.txt">complicated, multi-step nature of the cleanup process</a>,  I ended up creating an application to handle it instead of continuing to try to script it.  Yes, its probably overkill, but it was fun. And who knows, maybe it can be useful to someone else.</p>
<p>Anyways, this is what it looks like:</p>
<p>You log in with your matrix account:</p>
<p><img src="https://sequentialread.com/content/images/2023/01/janitorlogin.png" alt="Matrix Synapse Out of Disk Space state_groups_state"></p>
<p>And then you can see what's using disk space, and if there are any big rooms, you can delete them, including deleting the <code>state_groups_state</code> rows:</p>
<p><img src="https://git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor/raw/commit/a11f555307507a4840e634eed27db6866cd9edb1/readme/screenshot.png" alt="Matrix Synapse Out of Disk Space state_groups_state"></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[I was wrong about SBCs (Single Board Computers)]]></title><description><![CDATA[I can't think of a more compelling story for the hardware of the community-hosted internet revolution than that it was essentially dumpster-dived from behind a Cisco office park somewhere.]]></description><link>https://sequentialread.com/i-was-wrong-about-arm-sbcs/</link><guid isPermaLink="false">637e7d7f87b40e0001d97420</guid><category><![CDATA[SBCs]]></category><category><![CDATA[Self-Hosting]]></category><category><![CDATA[hardware]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Thu, 24 Nov 2022 00:02:28 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2022/11/ebay2.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img style="float: right; width:300px; margin: 10px; max-width: 50%;" src="https://sequentialread.com/content/images/2021/03/output2.gif" alt="I was wrong about SBCs (Single Board Computers)">
<img src="https://sequentialread.com/content/images/2022/11/ebay2.jpg" alt="I was wrong about SBCs (Single Board Computers)"><p>Long ago, I believed that Single Board Computers (SBCs) like the Raspberry Pi were going to become the ultimate hardware platform for home servers.  The idea of <a href="https://sequentialread.com/website-updates/">taking a mass-produced cellphone System-on-a-Chip (SoC) and attaching an ethernet + SATA port</a> seemed perfect. Everything was lining up for SBC dominance as the Raspberry Pi 3 and 4 finally fixed the IO and power issues that had plagued the earlier models &amp; their compettitors. Gone are the days of <a href="https://sequentialread.com/docker-on-odroid-xu4-installation-and-creating-a-base-image-2/">sub-par software support for arm CPUs</a>, as pretty much any major software can be installed on ARM these days and if not, it's usually a <a href="https://git.sequentialread.com/forest/gitea/compare/f4729e241827574ba7ccedc48729487a527ddbae..7e45fce1f66d415088a3222ad41837fcc215ac60">relatively painless process</a> to run a quick build for your device.</p>
<p>SBCs had overcome cheap $5/month cloud instances in terms of their performance characteristics, with some models boasting 8 cores &amp; quite impressive multi-core CPU benchmarks, especially when you consider their power consumption. The software support was looking excellent. New chips with another massive <em><strong>4x</strong></em> leap in performance were just around the corner...</p>
<p>And then covid hit. War broke out in Europe, demand for chips soared while supply ran dry. Just In Time Manufacturing experienced its first major system shock, and industry is still recovering years later. Now you can't even buy a new Raspberry Pi if you wanted to, they're all permanently out of stock everywhere and have been for years.</p>
<p>There are still some arm SBCs for sale, including those 4x faster ones I mentioned, like the <a href="https://ameridroid.com/collections/rock5-model-b">radxa Rock 5B on ameridroid</a>. But there aren't any more $35 computers to be found; these days SBCs tend to run about $60-$200.</p>
<h3 id="therearentanymore35computerstobefoundunless">There aren't any more $35 computers to be found. Unless... 👀</h3>
<p>Someone on the <a href="https://wiki.radxa.com/Rock5/FAQs">radxa discord</a> had pointed out to me that maybe single board computers aren't the right choice for cheap homebrew server hardware. At first I was reluctant to give up on my SBC evangelism:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->
<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/01/forest-ava-80.png" alt="I was wrong about SBCs (Single Board Computers)">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <p>Where I live, 1 watt of electricity costs about $1/year. ($0.12 per kWh)</p>
        <p><code>20 * 24 * 365 * 0.12 * (1/1000) = $21 per year</code> to run a 20w device</p>
        
        <p>so if I was homo economicus, I should be willing to pay about $80 more for a device that uses 2-5 watts instead of 20 watts, assuming im gonna use it for 6+ years</p>
    </div>
</div>
</div>

<br>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>What I didn't know, however, was that while the ARM chips of today are in extremely short supply, there appears to be a GLUT of used x86 hardware, so much so that some of these suckers are being listed for $30 with free shipping on ebay:</p>
<p><img src="https://sequentialread.com/content/images/2022/11/ebay.jpeg" alt="I was wrong about SBCs (Single Board Computers)"></p>
<p>Shipping ain't cheap. Something being sold for $30 with free shipping is practically being given away <em>for free</em> from the seller's perspective.</p>
<p>By the way, if you want to replicate my ebay search, I believe it was:</p>
<p><a href="https://www.ebay.com/b/PC-Desktops-All-In-One-Computers/179/bn_661752?_udlo=15&amp;rt=nc&amp;_fsrp=0&amp;RAM%2520Size=8%2520GB%7C4%2520GB&amp;LH_BIN=1&amp;_sacat=179&amp;LH_ItemCondition=1500%7C2010%7C2020%7C2030%7C2500%7C3000%7C2000&amp;_udhi=50&amp;mag=1">PC Desktops &amp; All-In-Ones between $15.00 and $50.00 with the following options:</a></p>
<ul>
<li><strong>RAM</strong>:
<ul>
<li>☑️ 4GB</li>
<li>☑️ 8GB</li>
</ul>
</li>
<li><strong>Selling Format</strong>:
<ul>
<li>☑️ Buy It Now</li>
</ul>
</li>
<li><strong>Condition</strong>:
<ul>
<li>☑️ Used</li>
<li>☑️ Refurbished</li>
<li>☑️ Open Box</li>
</ul>
</li>
</ul>
<h3 id="mooreslegacy">Moore's Legacy</h3>
<p>I've been saying for a long time that computers are fast enough and we shouldn't need to buy new ones any more.  Maybe that just means I'm getting old, but I think there's a lot of truth to it as well.</p>
<p>In the past, I imagined a future where computer hardware just kept getting cheaper and cheaper, better and better. To some extent that has been true, but not for NEW new hardware like the new arm SBCs. They have only ever gotten more expensive.</p>
<p>On the other hand, I had been ignoring the used hardware market; I wrote off the used PC &amp; server options as:</p>
<ul>
<li>too physically large</li>
<li>too loud</li>
<li>too energy-hungry</li>
<li>not as compatible</li>
</ul>
<p>My <a href="https://sequentialread.com/the-recycle-computer/">first ever post on this blog, <em>The &quot;recycle&quot; Computer ♻️</em></a> can speak to that a bit.  That computer was definitely annoying.  The fan and HDD made noise, it didn't support booting from USB, so re-installing the OS required a CD burner.</p>
<p>But these days, five years later, the computers that folks are willing to throw away or sell for $30 on ebay have changed dramatically! Now they have:</p>
<ul>
<li>Small form factor instead of <em>Big Ole ATX</em></li>
<li>Fanless/passive cooling heatsink designs</li>
<li>Low-power x86 chips instead of 65 watt Pentiums</li>
<li>8GB of RAM instead of 2 or less</li>
<li>Gigabit Ethernet / USB 3.0</li>
<li>Modern BIOS which supports booting from USB</li>
<li>eMMC modules or SSDs instead of 3.5&quot; hard drives</li>
</ul>
<p>I remember back when I started getting excited about ARM SBCs, these kind of computers existed, but they were new-ish, and almost always quite pricey. But now they're abundant like garage sale walmart bikes.  Huh. Go figure.</p>
<h3 id="caveats">Caveats</h3>
<p>According to netizens from <a href="https://www.freegeektwincities.org/">Free Geek</a>, while almost all of the thin client boxes support SATA for storage, some of them only have room inside for the SATA SOM (System On Module) form factor. So they can't be used with hard drives, one would have to either purchase an SSD that comes in this form factor or else purchase a regular one that is known to contain a short stubby little board inside and remove the tin cover from the outside of it.</p>
<p><img src="https://sequentialread.com/content/images/2022/11/aSOM.jpg" alt="I was wrong about SBCs (Single Board Computers)"></p>
<h3 id="upcyclethatdell">Upcycle That Dell</h3>
<p>The dream of home-brewed redundancy and  failover for less than $100 is still very much alive!  I don't see any reason why these machines can't/shouldn't be picked up for a pittance and used as servers.  I also don't see much of a reason to prefer SBCs or arm CPUs any more.</p>
<p>So while I'm figuring out where I want to go with my home-brew / community hosting software endeavors, I will be putting some of these ex-corporate thin client boxes on my holiday wishlist.</p>
<p>I can't think of a more compelling story for the hardware of the community-hosted internet revolution than that it was essentially dumpster-dived from behind a Cisco office park somewhere.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Federation vs. Clustering: Self-determination vs. distributed computing?]]></title><description><![CDATA[Self-hosting is such a shitty term because it sorta implies that you do it all by yourself. Not only is that unrealistic, it's also a lot less fun...  I'm still trying to figure out what I want to build, let alone what to call it. ]]></description><link>https://sequentialread.com/federation-vs-clustering-self-hosting/</link><guid isPermaLink="false">62ec4a621c49670001a0f8d9</guid><category><![CDATA[backend]]></category><category><![CDATA[high-availability]]></category><category><![CDATA[Self-Hosting]]></category><category><![CDATA[cloud]]></category><category><![CDATA[fediverse]]></category><category><![CDATA[cyberia]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Sun, 07 Aug 2022 05:26:28 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2022/08/littleredhen2-1.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2022/08/littleredhen2-1.jpg" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"><p>In my <a href="https://sequentialread.com/greenhouse-retrospective-and-future/">last post</a>, I wrote a about how I'm changing direction away from my <a href="https://git.sequentialread.com/sqr/greenhouse">&quot;trustless cloud-based gateway to make self-hosting easier&quot; project called greenhouse</a>. I  also wrote little bit about where I want to go with my software projects in the future.  In this post, I'm going to expand further on that. In fact, I'm writing this now so that I can help myself organize my thoughts and clarify my own ideas.</p>
<hr>
<p>Last time I said that I wanted to produce a piece of software in which two server admins' servers can &quot;federate&quot; with each-other. Another 3rd friend who doesn't run thier own server can have an account on these two servers.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><script>
(function(window, undefined){
  window.showHideSpoiler = function(spoilerId) {
    var header = document.getElementById('spoiler'+spoilerId);
    var body = document.getElementById('spoiler'+spoilerId+'content');
    var isVisible = body.style.display == 'block';
    var trimmed = header.textContent.trim();

    var originalHeaderClientY = header.getBoundingClientRect().y;

    if(!isVisible) {
      header.textContent = trimmed.substring(0, trimmed.length-6) + '- Hide';
      body.style.display = 'block';
    } else {
      header.textContent = trimmed.substring(0, trimmed.length-6) + '+ Show';
      body.style.display = 'none';
    }
    if(header.parentElement.classList.contains('spoiler')) {
      header.parentElement.className = isVisible ? 'spoiler closed' : 'spoiler open';
    }
      
    var scrollDelta = originalHeaderClientY - header.getBoundingClientRect().y;
      
    window.scrollBy(0, -scrollDelta);
      
	setTimeout(function(){
        var scrollDelta = originalHeaderClientY - header.getBoundingClientRect().y;
      
    window.scrollBy(0, -scrollDelta);
	}, 20);
  };
})(window)
</script>

<img src="https://sequentialread.com/content/images/2022/08/sync1-2.jpg" style="width: 32rem; max-width:100%;" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?" user a's account". the servers sync with eachother. a logs into server 1, but they can also log 2"><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>If the server on which the account was originally registered gets powered off or lost in a fire, the friend can still log in and use their account like normal on the one remaining server.</p>
<p>However, I think when I said &quot;federates&quot;, it may have been a bit of a misnomer, perhaps it might be more accurate to say that the two servers form a cluster with each-other and that they <em>replicate</em> the data.</p>
<p>But wait a minute. <em><strong>Cluster?</strong> Really?</em> What is this, a &quot;cloud scale&quot; enterprise solution? &quot;Cluster&quot; makes it sound like a supercomputer.  I thought this was about something that folks run at home, ya know, livingroom servers?</p>
<p>But before we get into that, its time I define some of the terms I'm throwing around and give a brief refresher on what the heck these things actually are, how they work, why they exist, and most importantly, how they tend to fail.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="federation">Federation</h2>
<p>Federation has become a bit of a buzzword among the <a href="https://en.wikipedia.org/wiki/Free_and_open-source_software">FLOSS</a> and <a href="https://homebrewserver.club/">self-hosting</a> communities within the past few years. Federated systems like <a href="https://matrix.org">matrix</a> are beginning to provide (in my opinion) the first real viable alternative to centralized social media platforms. Federated social media protocol standards like <a href="https://activitypub.rocks/">ActivityPub</a> connect users not just on different server instances, but between different types of server software.</p>
<p>For example, if you post a photo on your <a href="https://pixelfed.org/">PixelFed</a> account, my <a href="https://github.com/superseriousbusiness/gotosocial">GotoSocial</a> account can &quot;like&quot; it and someone else's <a href="https://joinmastodon.org/">Mastodon</a> account can leave a comment.  Such is the nature of the emergent social network dubbed <a href="https://joinfediverse.wiki/What_is_the_Fediverse%3F">&quot;The Fediverse&quot;</a>.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><div style="display: flex; flex-direction: row; flex-wrap: wrap-reverse; align-items:center; justify-content:space-around;">

<img src="https://sequentialread.com/content/images/2022/08/federation4.drawio.jpg" style="width: 60rem; max-width:100%;" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">

<div style="background:#ddd; border-radius: 1rem; padding:1rem;">
📊 <strong>Legend</strong>
<ul style="list-style-type: none;">
<li><img style="height:1em; display:inline-block;" src="https://sequentialread.com/content/images/2022/08/pencil2.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"> Writable</li>
<li><img style="height:1em; display:inline-block;" src="https://sequentialread.com/content/images/2022/08/replica.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"> Read-only Replica</li>
</ul>
</div>



</div><!--kg-card-end: html--><!--kg-card-begin: markdown--><blockquote>
<p>ℹ️ <strong>Note:</strong> Each account on this style of federated platform is intrinsically linked to its &quot;homeserver&quot;, the server on which the account was created. Federation partners are free to store copies of accounts and activity records from other servers, but in general they're not allowed to create or update anything that &quot;belongs&quot; to another server.</p>
<p>The homeserver concept is deeply embedded in the design of the ActivityPub protocol; there's no way around it. Every single ActivityPub message contains an <code>id</code> property which must be a URL identifying the dial-able address of the server who is considered the authority for that information.</p>
<pre><code>{
 &quot;@context&quot;:&quot;https://www.w3.org/ns/activitystreams&quot;,
 &quot;id&quot;:&quot;https://friend.camp/users/darius/outbox&quot;,
 &quot;type&quot;:&quot;OrderedCollection&quot;,
 ...
</code></pre>
</blockquote>
<p>These new &quot;grassroots&quot; distributed platforms are succeeding because they have managed to generate/harness network effects:  They're good enough at forming and maintaining connections between servers that even though there may be hundreds of different people and organizations operating servers, having one account on one server means you can theoretically &quot;reach&quot; the entire network.</p>
<p>At the same time, federation is opt-in and fine-grained enough that new servers joining in aren't instantly swamped by traffic from the entire network. If I operate a federated social media server and invite <a href="https://runyourown.social/">100 users</a> to join, they may eventually &quot;follow&quot; thousands of other accounts on hundreds of other servers.</p>
<p>That might sound like a lot for my poor server to process! But it's nothing compared to the millions and millions of fediverse accounts that exist. Computers are really fast, and for them, hundreds or thousands of things is a piece of cake.</p>
<blockquote>
<p>💡 <strong>Key Idea:</strong> Each fediverse server is operated by a different administrator, and may play by different rules. Servers can ban eachother, limit eachother's federation based on arbitrary criteria, etc. They can have software bugs and be incompatible with each-other, the sky is the limit. Each server admin has complete control over what happens on thier server.</p>
</blockquote>
<h2 id="clustering">Clustering</h2>
<p>&quot;Cluster&quot; usually refers to software that was designed to run on multiple computers in order to harness thier aggregate computational power and data bandwidth. This practice is often called &quot;scaling horizontally&quot;, and it is desirable from a business perspective because, in simple terms, it's a lot cheaper to buy &amp; maintain a bunch of small computers than it is to buy &amp; maintain one huge computer. Plus, if we play our cards right the army of smaller computers can be much more reliable, since it can avoid single points of failure.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><div style="display: flex; flex-direction: row; flex-wrap: wrap-reverse; align-items:center; justify-content:space-around;">

<img src="https://sequentialread.com/content/images/2022/08/cluster.drawio.jpg" style="width: 68rem; max-width:100%;" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">

<div style="background:#ddd; border-radius: 1rem; padding:1rem;">
📊 <strong>Legend</strong>
<ul style="list-style-type: none;">
<li><img style="height:1em; display:inline-block;" src="https://sequentialread.com/content/images/2022/08/pencil2.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"> Writable</li>
<li><img style="height:1em; display:inline-block;" src="https://sequentialread.com/content/images/2022/08/replica.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"> Read-only Replica</li>
</ul>
</div>



</div><!--kg-card-end: html--><!--kg-card-begin: markdown--><br>
<p>As you can see, this one is a bit of a doozy.</p>
<blockquote>
<p>ℹ️ <strong>Note:</strong> In this example, there is only one administrator. Every single cluster system I have ever encountered is like this: Intended to be deployed entirely by one organization, entirely under one umbrella, always within a single datacenter LAN.</p>
<p>In this case, there is no self-determination angle, clusters as we know them were borne primarily from economic factors that only influence large scale software and web service companies.</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->

<ul>
    <li>There are three servers, <code>A</code>, <code>B</code>, and <code>C</code>
<ul>
    <li>Three is often the <b><em>minimum</em></b> number of servers for a cluster to reach its full potential.</li>
<li>Large clusters may include hundreds of servers.</li>
</ul></li>

<li>The data is split into three "shards", <code>1</code>, <code>2</code>, and <code>3</code>.<br>

<ul><li><div class="spoiler closed">
<a id="spoiler2" href="#spoiler2" style="color: #808080; text-decoration: none; font-family: monospace; " onclick="javascript:showHideSpoiler(2); return false">
   🧐 Like the number of servers, the number of shards will vary... + Show
</a><br>
    
<div id="spoiler2content" style="display: none;">

    <ul>
        <li>For example, in <a href="https://kafka.apache.org/">Apache Kafka</a>, the number of Partitions (another word for shards) determines the maximum number of clients (Kafka consumers) who can all collaborate on the same task at the same time.</li>
        <li>Since one node will often be designated as the "primary" or single writer for a shard, the number of shards will also limit the number of cluster nodes who can work together on writing data to the sharded index.</li>
        <li>Too many shards may increase the amount of metadata handling and shuffling that the cluster has to do, in some cases dramatically slowing down it's operation.</li>
    </ul>
   

</div>

</div>
 </li> 


</ul> </li>
<li>
  Note that each shard is replicated to two nodes. 
<ul><li>This guarantees that if one node goes down, the system continues operating.</li></ul>
    <ul><li>As long as the replication is working, even if the node who is currently the writer for a given shard goes down, its replication partner node can be instantly promoted to be the new writer. </li></ul>
</li>
    
   
<li>The cluster client library may be able to predict which shards it needs for a given query and only contact nodes owning those shards.
<ul>

<li>If it can't, that's fine too. Whichever node is contacted by the client can forward the request to the appropriate server(s).</li>
    <li>For example in the above diagram, if the client asked <code>Server B</code>  to write to <code>shard 1</code>, <code>Server B</code> would simply proxy the request to <code>Server A</code>, because <code>Server A</code> holds the "primary" of <code>shard 1</code>, that is, <code>Server A</code> is the designated single writer for <code>shard 1</code>.</li>
</ul>
</li>

</ul>



<!--kg-card-end: html--><!--kg-card-begin: markdown--><blockquote>
<p>🤔 <strong>Remember</strong> how the ActivityPub federation protocol put a single <code>id</code> property on everything, and that <code>id</code> is a URL which specifies the dial-able location of the authoritative server for this info?</p>
<pre><code>{
 &quot;@context&quot;:&quot;https://www.w3.org/ns/activitystreams&quot;,
 &quot;id&quot;:&quot;https://friend.camp/users/darius/outbox&quot;,
 &quot;type&quot;:&quot;OrderedCollection&quot;,
 ...
</code></pre>
</blockquote>
<p>Lets try to find a comparable example for Apache Kafka. How does kafka handle this?</p>
<p><em>Apologies for the incoming wall of information; please bear with me through it. Or if you're not feelin it, just skip this part, most of it is details that don't matter for the overall story anyways.</em></p>
<img class="float-right-image" style="max-width: 100%; width: 30em;" src="https://sequentialread.com/content/images/2022/08/kafka.svg" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<p>A Kafka cluster is composed of one or more servers called Brokers. A Kafka cluster also has one or more Topics. You can think of a Topic as following the spirit of a Table in a relational SQL database. Topics are broken up into multiple partitions, where each partition represents some arbitrary slice of the data inside the topic. Not as a slice of time, but as a slice of the entire history of the topic.</p>
<p>In the following screenshot from the cute kafka explainer <a href="https://www.gentlydownthe.stream">https://www.gentlydownthe.stream</a>, a kafka topic is represented as a river:</p>
<img style="max-width: 100%;" src="https://sequentialread.com/content/images/2022/08/gently-kafka.jpg" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<br><br>
<p>Each partition is just a sequence of messages, and each message in a partition is identified by its offset within the sequence. So the first message in the partition would be at offset <code>0</code> and the 100th message would be at offset <code>99</code>. The offset can only increase, never decrease, as the partition is an append-only log.</p>
<div class="float-right-image" style="background:#ddd; border-radius: 1rem; padding:1rem;  width: 27rem; display: inline-block; margin-left:2rem;">
<img style="max-width: 25rem;" src="https://sequentialread.com/content/images/2022/08/zookeeper-image.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<p style="font-size: 0.85em;">
    This part of kafka utilizes a separate consensus cluster software called <a href="https://zookeeper.apache.org/"><b>ZooKeeper</b></a>. Yes, that disturbingly proportioned little cartoon man holding a poop shovel really is the official logo <span style="font-size: 1.3em;">😬</span>
</p>
</div>
<p>So at the end of the day, the full &quot;address&quot; of a message in kafka might look something like <code>&lt;topic&gt;/&lt;partition&gt;/&lt;offset&gt;</code>. (Although, you would never it see written out as a path like that.) Anyways, when it comes time to grab a single message like that, how would Kafka do it? There's no directly dial-able address here like there is in ActivityPub.</p>
<p>Why, it uses the <a href="https://kafka.apache.org/documentation/#impl_zktopic">Broker Index and Topic Index</a>, of course 🤪!</p>
<pre><code>/brokers/ids/[0...N] --&gt; { &quot;host&quot;:...,&quot;port&quot;:... }
</code></pre>
<pre><code>/brokers/topics/[topic]/partitions/[0...N]/state --&gt; {
  &quot;leader&quot;:...,
  &quot;isr&quot;:[...]
}
</code></pre>
<p>So first we look up the partition's current state. The <code>leader</code> will be the broker ID of the broker who has been assigned as the single writer for that topic partition.</p>
<p>The <code>isr</code> stands for &quot;In Sync Replicas&quot; (No, unfortunately its not an <a href="https://www.youtube.com/watch?v=_ZcmuKsyvzg">nsync</a> cover band 😥)</p>
<p>The &quot;In Sync Replicas&quot; is a list of broker IDs representing all brokers who are currently replicating the partition from the leader in real time.</p>
<p>From there, we simply look up the hostname and port of the broker in the broker index by id. In this way, we can connect to any of the brokers we found and issue a fetch request to grab messages starting from a specific offset. Or, if we want to issue a publish request to append a new message to the end of the partition, we have to specifically use the leader broker.</p>
<blockquote>
<p>💡 <strong>Key Idea:</strong> The topics, partitions, and messages don't know or care which broker they are on, how many brokers there are, etc. This is a separation of the logical data organization scheme from the underlying hardware configuration.</p>
<p>The cluster's internal protocols handle the real-time tracking of where everything is, and that internal metadata is kept separate from the data that the system is storing/processing.</p>
<p>This kind of separation exists in every piece of cluster software that I've seen. The replication we see in the logical layout provides <strong>&quot;high avaliability&quot;</strong>, while the separation between the logical layout and the physical reality of the dial-able hardware helps with <strong>&quot;fault tolerance&quot;</strong>. It's much easier to recover from a node failure if it doesn't require millions of URLs embedded in the data to be updated.</p>
</blockquote>
<p>Put together, <strong>these two properties give clusters superpowers that normal single-computer programs can only dream of.</strong></p>
<ol>
<li>A computer can catch on fire and explode without causing a noticeable impact to the system.</li>
<li>A destroyed node can be replaced while the system is running, again, no noticable impact from a client's perspective.</li>
<li>The system can be upgraded to a new version, including breaking changes, <em>while it's running.</em> You don't have to turn it off to upgrade it.</li>
<li>The software's performance isn't limited as much by the hardware it runs on because its design spreads load out evenly among multiple computers.
<ul>
<li>Often times if you need the cluster to do more work or do the work faster, you can simply add another node.</li>
</ul>
</li>
</ol>
<hr>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->
<blockquote>


<div class="spoiler closed">
<a id="spoiler4" href="#spoiler4" style="color: #808080; text-decoration: none; font-family: monospace; " onclick="javascript:showHideSpoiler(4); return false">
   🧐 Fun Fact: why clusters can provide faster storage if you're on a LAN + Show
</a><br>

<div id="spoiler4content" style="display: none;">
<br>
<p>Clustered databases and message brokers can also employ unique tricks to accelerate important storage tasks.</p>
<p>Engineers noticed that network and CPU hardware has improved beyond the limits of storage hardware like hard drives and SSDs. Specifically, the latency (round trip time) between two servers in a datacenter is hundreds of times faster than the time it takes to write data to an SSD.</p>
<p>Clusters can use thier strength in numbers to leverage thier fast CPUs and fast network for a massive performance gain over a traditional database performing the same task. When an application wants to write data and have confidence the data wont be lost, the cluster only has to replicate it to another node over the network before it can report the write as "suceeded". It doesn't have to wait for the write to be flushed to disk.</p>
<p>The chance of both of those nodes crashing at exactly the same time before either of them can flush the data to disk is so small that most clustered systems simply assume it will never happen. So a write can be considered "durable" or "fully persisted" before it even touches the disk.</p>
</div>

</div>
</blockquote><!--kg-card-end: html--><!--kg-card-begin: markdown--><hr>
<p><a id="kubernetes"></a><br>
Of course, I can't talk about clusters without mentioning the Big Important One that everyone is excited about: <strong><a href="https://kubernetes.io/">☸️ Kubernetes</a></strong>.</p>
<p>Mostly up to this point, I have been talking about storage-related cluster software. ElasticSearch, Kafka, Cassandra. But Kubernetes is different, it's all about computation, running lots of programs instead of storing data.</p>
<p>I don't dislike Kubernetes, in fact on the surface level it seems like it could be an option for any project that aims to configure &amp; run multiple programs spread across multiple computers.</p>
<p>But about three or four years ago I made an explicit decision not to try to use it for home-server related projects. I think it's simply not the appropriate technology.</p>
<p>Why?</p>
<img class="float-right-image" style="max-width: 100%; width: 40rem; margin-left: 2rem; margin-bottom: 2rem;" src="https://sequentialread.com/content/images/2022/08/kube.drawio.svg" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<p>Ultimately it came down to the network. As far as I know, from design, development, testing, and into production, the core of the Kubernetes cluster system, the &quot;etcd&quot; / &quot;control plane&quot; components were always intended to live on the same LAN. Whether the control plane nodes could talk to each-other directly wasn't in question, <em>it was simply assumed that they always could</em>. Not only that, it was also assumed they could talk to eachother with super low latency.</p>
<p>But in the home server environment, it just ain't so. Sure, you could run multiple servers in the same house. That would work fine, but it isn't going to increase the reliability of your services very much. Most outages will be caused by things like ISP outages, power outages, the cat knocks over the router and unplugs it, you lose your house in a natural disaster or move the server to a different house in a different city, etc.</p>
<p>Really, if we're trying to make a home server &quot;cluster&quot;, we're gonna need multiple people's homes as well as multiple servers. But therein lies the problem: unlike the cloud and datacenter machines, these home servers are living in what an aquaintance of mine calls &quot;<a href="https://en.wikipedia.org/wiki/Network_address_translation">NAT</a> Jail.&quot; They can't be directly contacted from outside unless the self-hoster performs some <a href="https://homebrewserver.club/fundamentals-port-forwarding.html">port forwarding configuration</a> on thier router first.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/08/kubernetes.drawio.svg" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"></figure><!--kg-card-begin: markdown--><p>And even then, those home IP address are liable to change somewhat regularly. Many self-hosters may not even be able to configure port forwarding if they wanted to ☹️.  It's all situational. So if I wanted to use Kubernetes in this context, it would probably require a <a href="https://en.wikipedia.org/wiki/Virtual_private_network">VPN</a> to be set up across all the nodes ahead of time.  That's already enough of a red flag for me to call it quits; I want my multi-server solution to <u>help</u> me network my servers together from behind NATs and firewalls, not to <em>demand that I have already done that part perfectly before I can turn it on.</em></p>
<p>I could definitely use Kubernetes as a single-node cluster, as many people do. In fact, someone is already <a href="https://kubesail.com/homepage">marketing a self-hosting focused product around it</a>. But if I was going to do that, why not just use <a href="https://docs.docker.com/compose/">docker-compose</a>? Or use single-node docker swarm like <a href="https://coopcloud.tech/">co-op cloud</a> does?</p>
<p>To be fair, I haven't kept up with the developement of various different lightweight Kubernetes distributions like <a href="https://github.com/k3s-io/k3s">k3s</a>, and who knows, <s>maybe in the year 2022 there is now a kubernetes distribution designed specifically for people who run servers in the mountains with solar panels and handmade radio antennas 😛</s></p>
<p><strong>EDIT:</strong> after sharing a draft of this post on the <a href="https://cyberia.club/matrix">cyberia.club matrix chat</a>, my friend <a href="https://github.com/Winterhuman">Winterhuman</a> from the very cool <a href="https://ipnslink.com/">IPNS-Link project</a> pointed me to exactly the thing I was missing:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><div class="chat ">
<img src="https://sequentialread.com/content/images/2022/08/winterhuman-80.jpg" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<div>
	<span class="username bluepurple"> 
        Winterhuman
    </span>
    <div class="body self"> 
<p>
        <span style="background:red; color:white; padding: 0 5px;  border-radius: 0.7em; display: inline-flex; flex-direction:row; align-items:center; justify-content:center;position:relative; top:3px;"><img style="height:1em; border-radius: 1em;" src="https://sequentialread.com/content/images/2022/01/forest-ava-80.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"> <span style="line-height: initial;">&nbsp; forest (he/him)</span></span> Just read [your post] and thought you'd be interested in <a href="https://github.com/c3os-io/c3os">https://github.com/c3os-io/c3os</a>, it's a Kubernetes distro, but, it uses Libp2p to escape the "NAT Jail" you talked about</p>
    </div>
</div>
    
</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/01/forest-ava-80.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <p>heh, I knew it had to exist</p>
    </div>
</div>

</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/01/forest-ava-80.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <p>very cool, although I think I would prefer something that has an installer  instead of being a linux image build script.  Installer is more portable, the ARM hardware requires different images for every single different board 🤮<br>and I still have reservations about kubernetes and to a lesser extent VPNs<br>but the parts it is composed of / overall approach  is super interesting.</p>
    </div>
</div>

</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/08/winterhuman-80.jpg" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<div>
	<span class="username bluepurple"> 
        Winterhuman
    </span>
    <div class="body self"> 
        <p>You could do something similar, minus the config and the Libp2p stuff being built-in, by using <a href="https://github.com/hyprspace/hyprspace">https://github.com/hyprspace/hyprspace</a> to create a VPN first (it can adapt to IP changes) and then layer stuff on top of it</p>
    </div>
</div>
    
</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/01/forest-ava-80.png" alt="Federation vs. Clustering: Self-determination vs. distributed computing?">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <p>I still have to figure out how I wanna structure it <br>I've never used a VPN with docker before so that would be my 1st step.</p>
    </div>
</div>

</div>

<hr>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>At least for now, I stand by my decision to skip the Kubernetes for self-hosting. At the end of the day this <code>c3os</code> thing is still just an easier way to set up a VPN as a pre-req, and Kubernetes still has a lot of issues with complexity and usability in my opinion.</p>
<h2 id="honorablementionp2pnetworks">Honorable Mention: P2P Networks</h2>
<blockquote>
<p><em>But Forest,</em></p>
</blockquote>
<p>You ask,</p>
<blockquote>
<p><em>Distributed Computing? Decentralization? Self Determination? My friend, you are describing a peer-to-peer network. You should just use <a href="https://ipfs.tech/">ipfs</a> and all of your problems will be solved.</em></p>
</blockquote>
<p>Well, dear reader, if I caught you thinking this, don't worry. I get it. I love P2P as much as the next guy; I've <a href="https://git.sequentialread.com/forest/tuber">dreamed up a hairbrained scheme</a> to rebuild <a href="https://twitch.tv">twitch.tv</a> as a peer to peer network that runs in the web browser, <a href="https://stream.sequentialread.com/">hacked on WebTorrent on my livestream</a>, and I even wrote a <a href="https://sequentialread.com/how-to-register-a-namecoin-bit-domain-with-electrum-nmc/">passionate plea to breathe life into Namecoin</a>.</p>
<blockquote>
<p><em>Ok, wow Forest, if you love P2P so much why don't you marry it?</em></p>
</blockquote>
<p>The TL;DR is that the web browsers everyone uses still do not support IPFS in 2022 and I'm unsure if they ever will. I'm not holding my breath.</p>
<p>I do believe in transformative P2P applications that are worth downloading a client app for. I've seen two so far in my lifetime:</p>
<ul>
<li>BitTorrent</li>
<li>cryptocurrency</li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><blockquote>

<div class="spoiler closed">
<a id="spoiler5" href="#spoiler5" style="color: #808080; text-decoration: none; font-family: monospace; " onclick="javascript:showHideSpoiler(5); return false">
   😇 Anti-cryptocurrency folks, please don't write me off right away! + Show
</a><br>

<div id="spoiler5content" style="display: none;">
<p>... please don't write me off right away! I probably agree with most of your position.  Measured in dollars or work hours, the majority of activity in cryptocurrency <i>does</i> probably fall under the "scam" category.</p>

<p>The future doesn't look bright for Proof of Work cryptocurrencies like Bitcoin. As they begin to chomp at a slice of the macroeconomic pie, they start to incentivize really counterproductive dead-end behavior like 
</p>
<blockquote>
  <i>"Hey, lets buy every single GPU on the market and mine Etherium with them!"</i>
</blockquote>

<p>I know this is a divisive topic, but I can't see it in black and white.</p>
    
<p>If you look at the history of Cryptocurrency and see what went down with Bitcoin, Yes, it is primarily a story of misguided dreams, greed, human folly, capitalism, scams, stolen pirate treasure hoards, etc. But I also see a bit of a David vs Golaith story where Bitcoin, the software, protocol, and community holds its own against stormy seas and the will of the incumbent financial system. </p>

<p>According to Blockstream, everyone was supposed to be using the Lightning Network for cryptocurrency payments by now. But that didn't happen. Folks rejected it because that wasn't what they wanted.  Why participate in turning cryptocurrency into a bank-style network where everybody owes everybody else and none of the debts can ever be paid when the original idealistic Bitcoin from the 2009 whitepaper seems to work fine? Especially when the people pushing Lightning network, people on a large bank's payroll, are also ferociously defending a nonsensical technical decision which permanently limits the bandwidth of "original style" bitcoin transactions network-wide. Instead, everyone started adopting altcoins for thier small payments. Coins whose developers refused to be bought out,  like Litecoin, Monero and Bitcoin Cash. </p> 

<p>Cryptocurrency has not yet been assimilated or destroyed by banks, despite some definite effort on thier part. It may be destroying itself instead. Being voluntarily dismanted those who are tired of trying to find the diamond in the rough and see no long-term path forward. Since we've started to see systemic issues with Proof of Work mining, I can't be sad about that. </p>

<p>May the part that is worth keeping survive, if any. I would <a href="https://sequentialread.com/how-to-register-a-namecoin-bit-domain-with-electrum-nmc/#securenameswithoutauthority">still be hyped</a> for a workable long-term solution to break <a href="https://en.wikipedia.org/wiki/Zooko%27s_triangle">Zooko's triangle</a> like how Bitcoin and Namecoin do in "production" today.</p>

 
</div>

</div>
</blockquote><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>But I think anything to do with media or publishing is still going to have to at least <em>support</em> the web browser, either with oldskool HTTP or with P2P support directly in the browser. Not a special browser like Tor Browser, but at least Firefox, and probably Chrome too.</p>
<p>IMO, things like <a href="https://zeronet.io/">ZeroNet</a>, <a href="https://beakerbrowser.com/">Beaker Browser</a> and <a href="https://www.gnunet.org/en/index.html">GNUnet</a> are interesting and compelling, but with no mainstream web-browser adoption anywhere on the horizon, they're nothing more than toys or prototypes with no possibility of generating enough network effect to get going.</p>
<p>It's like I wrote 6 years ago:</p>
<blockquote>
<p><a href="https://sequentialread.com/pragmatic-path-towards-non-technical-users-owning-their-own-data/">If your app doesn't have a URL, who's going to use it?</a></p>
</blockquote>
<p><a href="https://webtorrent.io/">WebTorrent</a> is a perfect example of P2P support in the browser. If you haven't heard of it before, I highly recommend checking out the demo on thier front page: <a href="https://webtorrent.io/">https://webtorrent.io/</a></p>
<p>In fact, it's so cool, I'll include a screenshot of it here:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/08/webtorrent.jpg" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"></figure><!--kg-card-begin: markdown--><p>In case you aren't sure what's going on here, it really is torrenting a video file in real time in your web-browser. Those yellow dots on the left are the &quot;torrent swarm&quot;, the other people accessing the demo at the same time as you. They are all sharing chunks of the video file to each-other to accelerate the download. In fact, unlike a traditional HTTP file server, the more people who attempt to torrent this file at once, the <em>FASTER</em> the download will go for everyone.</p>
<p>It's amazing to me that software like this is already mature and has been doing this in the web browser for years, and it's now powering potentially useful apps like <a href="https://joinpeertube.org/">PeerTube</a>. I think there's a lot more untapped potential for these kind of peer-to-peer enabled web apps.</p>
<p>However, we can never lose ourselves in the hype. Remember that this is only ever going to work on your mom or dad's computer because someone is running a traditional HTTP server to provide them with the &quot;backwards compatible&quot; web application and JavaScript to make this happen.</p>
<p>If you want to harness this kind of power yourself, if you want to send someone a link to your own version of this, <em>you're probably going to have to set up that HTTP server too.</em></p>
<p>I think we're going to be using HTTP and TLS for many more years; in my opinion it makes sense to start from there.</p>
<p>I'm still searching for a way for folks to set up thier own  <strong><em>publicly dialable</em></strong> HTTP servers hosted at home or within thier own community. Ideally, without <a href="https://sequentialread.com/forwarding-port-443-on-centurylink-technicolor-c2100t-modem/">nearly tearing thier hair out in frustration</a> in the process.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="broyousaidbriefrefresherwtf">Bro you said &quot;brief refresher,&quot; wtf</h2>
<p>Ok, that exposition may have gotten a little out of hand. These are complex topics:</p>
<ul>
<li>Federated Social Media Protocols/Platforms</li>
<li>Storage and Compute Cluster Applications</li>
<li>Also, Peer-to-Peer networks apparently?</li>
</ul>
<p>And they're all so close to my heart, filled with personal stories, feelings, funny anecdotes, etc.</p>
<p>Ok ok, I promised I would cover:</p>
<blockquote>
<p>how they work, why they exist, and most importantly, how they tend to fail.</p>
</blockquote>
<p>I believe I have covered the first two fairly well. So now it's time for...</p>
<h2 id="mostimportantlyhowtheytendtofail">most importantly, how they tend to fail</h2>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>I used to work on distributed systems a lot at a my old job. I did a lot of work upgrading Kafka and Cassandra clusters while they were running &amp; spent a lot of time monitoring the health of the clusters, learning about what can go wrong with them and debugging issues as they came up.</p>
<p>Cluster applications are much more complex, so they're harder to interact with. They almost always require special client software or client libraries to interact with. Sometimes these clients feel like they're missing some important features, but doing anything with the cluster &quot;by hand&quot; without the client can be scary or prohibitively difficult.</p>
<p>They're also very hard to debug. When you get off the beaten path, thier user-interfaces, especially when it comes to error handling, range from &quot;very poor&quot; to &quot;non-existant&quot;.  It was so bad. I remember having to <a href="https://www.youtube.com/watch?v=Fow7iUaKrq4"><code>SIGKILL</code> (<strong>run <code>kill -9</code></strong>)</a> on stuck kafka broker processes quite often. Not a comfortable feeling on a production system. We never lost any data tho!</p>
<hr>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><blockquote>

<div class="spoiler closed">
<a id="spoiler3" href="#spoiler3" style="color: #808080; text-decoration: none; font-family: monospace; " onclick="javascript:showHideSpoiler(3); return false">
   🎶🎤 I guess I’ll have to shut you down for good this time... 🎶 + Show
</a><br>

<pre id="spoiler3content" style="display: none;">
I guess I’ll have to shut you down for good this time
Already tried a SIGQUIT, so now it’s KILL DASH 9
You gotta learn when it’s time for your thread to yield;
It shoulda slept; instead you stepped and now your fate is sealed
I’ll take your process off the run queue without even asking
Cause my flow is like reentrant and preemptive multitasking
Your sad rhymes are spinnin’ like you’re in a deadlock
You’re like a synchronous sock that don’t know when to block;
</pre>

</div>
</blockquote><!--kg-card-end: html--><!--kg-card-begin: markdown--><hr>
<p>All in all, I think that clusters are easily misunderstood and almost always difficult to work with. There's just so much going on with them, especially complex clusters like Kubernetes, it can be overwhelming.</p>
<p>Part of this is because of the complexity, but I think it's also because contemporary demands for clusters were almost exclusively about more storage, more bandwidth, more stuff. These clusters were only ever intended to be operated by highly trained mega-nerds in a professional setting; they could get away with little to no interface for the operators. Instead, all the development effort went into optimizations and new features.</p>
<p>The environments where these clusters run and the jobs they fulfil are some of the most demanding ever known to computing. It's not surprising to me that they're difficult to use.</p>
<blockquote>
<p>💢 Clusters fail before they get started.  People like me refuse to use them because they are simply too big and difficult, the requirements are too high and inflexible and there's relatively little payoff.</p>
<p>In the homebrew realm, clusters fail specifically because they've all been designed to run on a shared LAN. In thier current implementations, the individual cluster nodes can't/shouldn't be spread to multiple different localities.</p>
<p>Whoever has power and internet at home that's reliable enough to justify deploying a whole cluster on... Well, they must live in a datacenter, so they're irrelevant in this case 😆</p>
</blockquote>
<hr>
<p><strong>Federated social media services</strong>, on the otherhand <em>ARE</em> often designed to be easy to self-host, sometimes even including how-to guides for hosting at home. They're fairly easy to get started with, but sometimes struggle to achieve longevity.</p>
<p>Many people have commented on this phenomenon at length. Personally, I'm a fan of Darius Kazemi's discussion of some of the issues at play on his how-to guide <a href="https://runyourown.social/">https://runyourown.social/</a>.</p>
<p>Some highlights from my memory of that guide:</p>
<ul>
<li>Administering one of these sites <strong><em>is</em></strong> a significant &amp; continuous burden.
<ul>
<li>It's a big responsibilty: if you stop maintaining it, everyone who has been depending on you will at best have to go through a migration process, and at worst lose their account and everything that was attached to it.</li>
<li>You'll probably want to maintain a federation block list to keep various kinds of 4chan-esque &quot;nasties&quot; off of your server.</li>
</ul>
</li>
<li>You probably want to limit the number of users; servers that have grown out of control invariably run into problems.
<ul>
<li>Often these problems are more social than technical: There <em>will</em> be disputes. There will be situations where you have to make moderation decisions which will alienate some of your users. You can't please everybody.</li>
</ul>
</li>
<li>Allowing anyone to register an account at any time is probably a recipie for disaster for similar reasons.</li>
</ul>
<p>The way I see it, the average Fedi instance is somewhere in between:</p>
<ol>
<li>a personal experiment that can always be shut down</li>
</ol>
<p>and</p>
<ol start="2">
<li>a service that many people rely on &amp; can't be allowed to fail</li>
</ol>
<p>It's an awkward in-between for sure, and I think this is the crux of the issue with the federation concept as it's implemented in the Fediverse.</p>
<p>In order to preserve the autonomy of each fedi admin, all of the user data under thier care is intrinsically linked to the domain name of the server they operate.  It may be possible for one admin to pass the torch to another person, but I suspect this rarely happens in practice.</p>
<p>Usually when an admin loses interest in maintaining something like this, it'll happen without warning, and by the time it becomes apparent, it might already be too late to try to transfer ownership. Life happens. Sometimes circumstances change and folks won't have the spare time for this kinda thing. That has to be ok. Technology is supposed to work for <em><strong>US</strong></em>, not the other way around.</p>
<blockquote>
<p>💢 Instances of Federated network software fail because they place a constant burden of maintenance on (usually) a single administrator.</p>
<p>These instances are often designed to be easy to set up, but as soon as folks start using them, they can become entrenched and both difficult to maintain as well as difficult to transfer to someone else. For example, it's not enough to transfer a backup of the server's data or the physical server itself, one would also have to transfer ownership of the domain name.</p>
<p>It can turn into a quagmire where the users stand to lose a lot and the admin doesn't have any easy way out.</p>
</blockquote>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="coolstorybutwhatisthisallabout">Cool story. But what is this all about?</h2>
<p>I created <a href="https://greenhouse.server.garden/">greenhouse</a> because I was frustrated with how difficult the network configuration part of self-hosting can be. Specifically, I was frustrated by the lack of solutions that always work no matter what.</p>
<p>I thought I saw a need: All of these self-hosted software setup guides are super easy and simple until you get to the part about NATs, routers, port forwarding, DNS, dynamic DNS, etc.  Then everything <a href="https://yunohost.org/en/isp_box_config">goes off the rails</a> into <a href="https://yunohost.org/en/finding_the_local_ip">seemingly endless tangents</a> and <a href="https://en.wikipedia.org/wiki/Carrier-grade_NAT">what-ifs</a>.</p>
<p>I've written on this <a href="https://sequentialread.com/serviceworker-webrtc-the-p2p-web-solution-i-have-been-looking-for/">before</a>: Shells are well standardized. Linux is fairly standardized. The web protocols like TCP, TLS, and HTTP are incredibly well standardized. But <a href="https://sequentialread.com/forwarding-port-443-on-centurylink-technicolor-c2100t-modem/">every home network is different</a>, there's no single way to configure the network for a server.</p>
<p>Greenhouse was an attempt to allow the self-hoster to flip the table and refuse to configure the home network at all. It was Infrastructure as a Service that outsources the DNS configuration and TCP listener into an easy-to-use public-cloud-based service while keeping the TLS (encryption stuff) on the self-hosted server.</p>
<p>I believe that greenhouse failed because on its own, it failed to address a widespread need. As I explained in my <a href="https://sequentialread.com/greenhouse-retrospective-and-future/">last post</a>, greenhouse catered to self-hosting fundamentalists, but at the same time, it <em><strong>IS</strong></em> a 3rd party service, the bane of every self-hosting fundamentalist's existence.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/08/Untitled-Diagram.drawio-2-.svg" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"></figure><!--kg-card-begin: markdown--><p>So I ended up trying to rethink the whole thing, rethink my whole approach.</p>
<p>I was inspired by the way that the fediverse had seemed to address two extremely common needs at once.</p>
<ol>
<li>Convenience. Just sign up for an account; don't have to run your own server</li>
<li>A sense of data custody and belonging within a tight-knit community</li>
</ol>
<p>Even better, unlike greenhouse, this time the venn diagram has a huge overlap, it's practically a circle.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/08/Untitled-Diagram.drawio-3-.svg" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"></figure><!--kg-card-begin: markdown--><br>
<p>In fact, I think the same venn diagram can be generalized to talk about  huge swaths of the internet today. It's funny to me that everyone wants a server but no-one wants to run one. But I think it's all too true.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/08/Untitled-Diagram.drawio-4-.svg" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"></figure><!--kg-card-begin: markdown--><br>
<p>The list of internet usecases which could conceivably fall into this category goes on and on.</p>
<p>It reminds me of the children's story <a href="http://gooseberryjamman.blogspot.com/2011/03/little-red-hen.html"><em>The Little Red Hen</em></a>.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/08/littleredhen2.jpg" class="kg-image" alt="Federation vs. Clustering: Self-determination vs. distributed computing?"></figure><!--kg-card-begin: markdown--><br>
<p>Except this time she's baking digital bread with near-zero marginal costs, so even if the hen and her chicks eat first, there should still be enough left at the end for the the Pig, the Duck, the Cat, and everyone else in the pasture.</p>
<p>But there's just one problem with this little &quot;online era&quot; adaptation of <em>The Little Red Hen</em>. If there's one thing I learned in my career as a digital bread factory technician (and my subsequent mini-retirement as a trappist digital bread monk), it's that when it comes to the best of breads that bring the boys to the yard, <strong>nobody can bake em alone</strong>.</p>
<p>That is, popular web technology, especially when we consider it as the entire stack all the way from the server's power supply to the router, the operating system, processes, protocols, html, images, text, usability, design appeal, accessibility, client compatibility and everything else, its simply way too much for one person to carry.</p>
<p>I have been trying to carry all of it myself in my work on greenhouse and my other personal projects that never saw the light of day, and it's been hard. I haven't seen any success yet.</p>
<p>But I'm trying to stay positive. I love the internet because no matter how uniquely impossible your personal struggles may seem, there's probably a niche community out there somewhere where you can commiserate or find some kind of solace. No matter how obscure your problem, you can always find someone else who has the same problem.  The internet is amazing for forming communities and meeting new people who you have something in common with.</p>
<p>And in those communities, through collaborating with my friends, I <em>have</em> seen some successes. The graph of greenhouse users and activity went <a href="https://sequentialread.com/greenhouse-retrospective-and-future/#notstonks">down and to the right</a>.  But another project that I developed with my friend <a href="https://j3s.sh">j3s</a>, <a href="https://capsul.org">capsul.org</a>, is going <strong>up</strong> and to the right, and it has been for over two years now!</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><script src="https://picopublish.sequentialread.com/files/capsul-stonks-may-2022/Chart.bundle.js">
</script>

<style>
.chart-container {
  margin: 1rem;
}
</style>


<div class="chart-container">
<h3>capsul.org revenue - $<span id="monthlyTotal"></span>/mo all-time avg</h3>
<canvas id="mainRevenue" width="1000px" height="400px"></canvas>
<canvas id="mainAccounts" width="1000px" height="400px"></canvas>
</div>



<script>

var raw = [
  {
    "date": 1583452800000,
    "monthlyActiveAccounts": 1,
    "monthlyRevenue": 64,
  },
  {
    "date": 1583971200000,
    "monthlyActiveAccounts": 2,
    "monthlyRevenue": 171,
  },
  {
    "date": 1584057600000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 235,
  },
  {
    "date": 1585785600000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 299,
  },
  {
    "date": 1586390400000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 299,
  },
  {
    "date": 1586908800000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 192,
  },
  {
    "date": 1587772800000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 256,
  },
  {
    "date": 1588636800000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 278,
  },
  {
    "date": 1589587200000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 160,
  },
  {
    "date": 1589587200000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 280,
  },
  {
    "date": 1589673600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 280.01,
  },
  {
    "date": 1589673600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 280.02,
  },
  {
    "date": 1589673600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 280.03,
  },
  {
    "date": 1589673600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 280.04,
  },
  {
    "date": 1589673600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 280.05,
  },
  {
    "date": 1589673600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 280.07,
  },
  {
    "date": 1590019200000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 344.07000000000005,
  },
  {
    "date": 1591056000000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 285.07000000000005,
  },
  {
    "date": 1591920000000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 200.07000000000002,
  },
  {
    "date": 1592611200000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 11,
  },
  {
    "date": 1592611200000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 12,
  },
  {
    "date": 1592784000000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 20,
  },
  {
    "date": 1593216000000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 25,
  },
  {
    "date": 1594598400000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 29,
  },
  {
    "date": 1594598400000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 79,
  },
  {
    "date": 1594684800000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 87,
  },
  {
    "date": 1595289600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 86,
  },
  {
    "date": 1596412800000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 98,
  },
  {
    "date": 1596758400000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 103,
  },
  {
    "date": 1597190400000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 49,
  },
  {
    "date": 1597190400000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 54,
  },
  {
    "date": 1597881600000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 51,
  },
  {
    "date": 1598054400000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 151,
  },
  {
    "date": 1598227200000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 161,
  },
  {
    "date": 1599523200000,
    "monthlyActiveAccounts": 2,
    "monthlyRevenue": 151,
  },
  {
    "date": 1599523200000,
    "monthlyActiveAccounts": 2,
    "monthlyRevenue": 161,
  },
  {
    "date": 1599696000000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 176,
  },
  {
    "date": 1600300800000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 170,
  },
  {
    "date": 1600732800000,
    "monthlyActiveAccounts": 2,
    "monthlyRevenue": 80,
  },
  {
    "date": 1600905600000,
    "monthlyActiveAccounts": 3,
    "monthlyRevenue": 75,
  },
  {
    "date": 1600992000000,
    "monthlyActiveAccounts": 4,
    "monthlyRevenue": 90,
  },
  {
    "date": 1601337600000,
    "monthlyActiveAccounts": 5,
    "monthlyRevenue": 95,
  },
  {
    "date": 1601424000000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 125,
  },
  {
    "date": 1601424000000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 130,
  },
  {
    "date": 1601596800000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 131,
  },
  {
    "date": 1601942400000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 151,
  },
  {
    "date": 1603065600000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 121,
  },
  {
    "date": 1603238400000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 141,
  },
  {
    "date": 1603324800000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 171,
  },
  {
    "date": 1603324800000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 191,
  },
  {
    "date": 1603584000000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 181,
  },
  {
    "date": 1603584000000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 201,
  },
  {
    "date": 1603670400000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 261,
  },
  {
    "date": 1603756800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 281,
  },
  {
    "date": 1604102400000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 246,
  },
  {
    "date": 1604275200000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 265,
  },
  {
    "date": 1604361600000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 275,
  },
  {
    "date": 1604620800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 280,
  },
  {
    "date": 1605225600000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 285,
  },
  {
    "date": 1605225600000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 288,
  },
  {
    "date": 1605398400000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 318,
  },
  {
    "date": 1605744000000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 318,
  },
  {
    "date": 1606176000000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 203,
  },
  {
    "date": 1606262400000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 155,
  },
  {
    "date": 1606262400000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 170,
  },
  {
    "date": 1606262400000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 175,
  },
  {
    "date": 1606867200000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 145,
  },
  {
    "date": 1607040000000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 155,
  },
  {
    "date": 1607385600000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 155,
  },
  {
    "date": 1607644800000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 185,
  },
  {
    "date": 1607731200000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 225,
  },
  {
    "date": 1607904000000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 237,
  },
  {
    "date": 1608681600000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 207,
  },
  {
    "date": 1609113600000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 175,
  },
  {
    "date": 1609200000000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 220,
  },
  {
    "date": 1609286400000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 240,
  },
  {
    "date": 1609372800000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 290,
  },
  {
    "date": 1609372800000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 320,
  },
  {
    "date": 1609372800000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 350,
  },
  {
    "date": 1609459200000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 340,
  },
  {
    "date": 1610150400000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 335,
  },
  {
    "date": 1610496000000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 345,
  },
  {
    "date": 1610582400000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 375,
  },
  {
    "date": 1610668800000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 405,
  },
  {
    "date": 1610668800000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 430,
  },
  {
    "date": 1610668800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 460,
  },
  {
    "date": 1611532800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 460,
  },
  {
    "date": 1611792000000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 417.5,
  },
  {
    "date": 1611792000000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 418,
  },
  {
    "date": 1611792000000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 418.07,
  },
  {
    "date": 1611792000000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 418.24,
  },
  {
    "date": 1611878400000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 398.49,
  },
  {
    "date": 1611878400000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 418.49,
  },
  {
    "date": 1611878400000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 430.99,
  },
  {
    "date": 1611964800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 321.23,
  },
  {
    "date": 1611964800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 321.33000000000004,
  },
  {
    "date": 1612051200000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 322.33000000000004,
  },
  {
    "date": 1612051200000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 337.33000000000004,
  },
  {
    "date": 1612051200000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 367.33,
  },
  {
    "date": 1612051200000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 397.33,
  },
  {
    "date": 1612569600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 412.3299999999999,
  },
  {
    "date": 1612656000000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 442.3299999999999,
  },
  {
    "date": 1612828800000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 412.3299999999999,
  },
  {
    "date": 1612915200000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 417.33,
  },
  {
    "date": 1613174400000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 317.33,
  },
  {
    "date": 1613433600000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 242.32999999999998,
  },
  {
    "date": 1613433600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 242.33999999999997,
  },
  {
    "date": 1613692800000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 342.34,
  },
  {
    "date": 1613779200000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 347.34,
  },
  {
    "date": 1613952000000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 367.34,
  },
  {
    "date": 1613952000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 379.84,
  },
  {
    "date": 1614038400000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 387.34,
  },
  {
    "date": 1614038400000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 392.84,
  },
  {
    "date": 1614297600000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 387.84,
  },
  {
    "date": 1614297600000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 410.84,
  },
  {
    "date": 1614297600000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 435.84000000000003,
  },
  {
    "date": 1614384000000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 452.6,
  },
  {
    "date": 1614556800000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 454.51,
  },
  {
    "date": 1614556800000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 464.51,
  },
  {
    "date": 1614643200000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 398.51,
  },
  {
    "date": 1614729600000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 473.51,
  },
  {
    "date": 1614729600000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 498.51,
  },
  {
    "date": 1614816000000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 528.51,
  },
  {
    "date": 1614816000000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 533.51,
  },
  {
    "date": 1614988800000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 583.51,
  },
  {
    "date": 1615161600000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 578.51,
  },
  {
    "date": 1615161600000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 608.51,
  },
  {
    "date": 1615248000000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 608.51,
  },
  {
    "date": 1615248000000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 628.51,
  },
  {
    "date": 1615248000000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 638.51,
  },
  {
    "date": 1615248000000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 645.51,
  },
  {
    "date": 1615248000000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 665.51,
  },
  {
    "date": 1615680000000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 700.51,
  },
  {
    "date": 1615680000000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 750.51,
  },
  {
    "date": 1615939200000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 735.51,
  },
  {
    "date": 1616025600000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 750.5,
  },
  {
    "date": 1616025600000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 765.5,
  },
  {
    "date": 1616284800000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 685.5,
  },
  {
    "date": 1616371200000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 830.5,
  },
  {
    "date": 1616457600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 895.5,
  },
  {
    "date": 1616457600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 945.5,
  },
  {
    "date": 1616544000000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 963,
  },
  {
    "date": 1616544000000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 988,
  },
  {
    "date": 1616630400000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 983,
  },
  {
    "date": 1616889600000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 925,
  },
  {
    "date": 1617062400000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 907,
  },
  {
    "date": 1617148800000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 874.5,
  },
  {
    "date": 1617408000000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 729.5,
  },
  {
    "date": 1617494400000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 747.5,
  },
  {
    "date": 1617580800000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 757.5,
  },
  {
    "date": 1617753600000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 747.5,
  },
  {
    "date": 1618012800000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 675.5,
  },
  {
    "date": 1618012800000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 725.5,
  },
  {
    "date": 1618099200000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 731.5,
  },
  {
    "date": 1618272000000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 641.5,
  },
  {
    "date": 1618531200000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 656.5,
  },
  {
    "date": 1618531200000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 686.5,
  },
  {
    "date": 1618876800000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 631.5,
  },
  {
    "date": 1618876800000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 651.5,
  },
  {
    "date": 1618876800000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 661.5,
  },
  {
    "date": 1618963200000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 531.5,
  },
  {
    "date": 1618963200000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 531.9,
  },
  {
    "date": 1618963200000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 532.9,
  },
  {
    "date": 1618963200000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 534.5,
  },
  {
    "date": 1619049600000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 427,
  },
  {
    "date": 1619049600000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 435,
  },
  {
    "date": 1619222400000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 357,
  },
  {
    "date": 1619222400000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 367,
  },
  {
    "date": 1619222400000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 387,
  },
  {
    "date": 1619481600000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 407,
  },
  {
    "date": 1619740800000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 387.51,
  },
  {
    "date": 1619740800000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 447.51,
  },
  {
    "date": 1619740800000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 452.51,
  },
  {
    "date": 1619827200000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 543.51,
  },
  {
    "date": 1620086400000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 525.51,
  },
  {
    "date": 1620086400000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 589.51,
  },
  {
    "date": 1620172800000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 559.51,
  },
  {
    "date": 1620345600000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 579.51,
  },
  {
    "date": 1620345600000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 629.51,
  },
  {
    "date": 1620345600000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 659.51,
  },
  {
    "date": 1620691200000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 606.51,
  },
  {
    "date": 1620950400000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 646.51,
  },
  {
    "date": 1621209600000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 594.01,
  },
  {
    "date": 1621209600000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 614.01,
  },
  {
    "date": 1621209600000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 644.01,
  },
  {
    "date": 1621296000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 664.01,
  },
  {
    "date": 1621900800000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 585.51,
  },
  {
    "date": 1622073600000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 565.51,
  },
  {
    "date": 1622332800000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 550.5,
  },
  {
    "date": 1622419200000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 659.5,
  },
  {
    "date": 1622505600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 729.5,
  },
  {
    "date": 1622592000000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 737,
  },
  {
    "date": 1622592000000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 801,
  },
  {
    "date": 1622678400000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 747,
  },
  {
    "date": 1622678400000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 757,
  },
  {
    "date": 1622764800000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 762,
  },
  {
    "date": 1622764800000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 782,
  },
  {
    "date": 1623024000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 654,
  },
  {
    "date": 1623024000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 654.5,
  },
  {
    "date": 1623024000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 654.8,
  },
  {
    "date": 1623024000000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 664.8,
  },
  {
    "date": 1623196800000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 669.8,
  },
  {
    "date": 1623369600000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 701.8,
  },
  {
    "date": 1623542400000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 664.3,
  },
  {
    "date": 1623542400000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 684.3,
  },
  {
    "date": 1623801600000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 641.8,
  },
  {
    "date": 1623801600000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 691.8,
  },
  {
    "date": 1623888000000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 721.8,
  },
  {
    "date": 1623974400000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 821.8,
  },
  {
    "date": 1623974400000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 851.8,
  },
  {
    "date": 1624060800000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 856.8,
  },
  {
    "date": 1624320000000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 876.8,
  },
  {
    "date": 1624406400000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 877.8,
  },
  {
    "date": 1624406400000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 881.8,
  },
  {
    "date": 1624492800000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 871.8,
  },
  {
    "date": 1624492800000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 881.8,
  },
  {
    "date": 1624579200000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 890.8,
  },
  {
    "date": 1624665600000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 893.3,
  },
  {
    "date": 1624665600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 943.3,
  },
  {
    "date": 1624752000000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 944.3,
  },
  {
    "date": 1624924800000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 924.3,
  },
  {
    "date": 1624924800000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 949.3,
  },
  {
    "date": 1625097600000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 712.3,
  },
  {
    "date": 1625097600000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 717.3,
  },
  {
    "date": 1625184000000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 647.6,
  },
  {
    "date": 1625270400000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 630.71,
  },
  {
    "date": 1625270400000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 650.71,
  },
  {
    "date": 1625356800000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 610.71,
  },
  {
    "date": 1625616000000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 600.91,
  },
  {
    "date": 1625616000000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 614.11,
  },
  {
    "date": 1625702400000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 614.12,
  },
  {
    "date": 1625702400000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 614.13,
  },
  {
    "date": 1625875200000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 624.13,
  },
  {
    "date": 1625875200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 629.13,
  },
  {
    "date": 1625961600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 604.13,
  },
  {
    "date": 1625961600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 629.13,
  },
  {
    "date": 1626048000000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 652.13,
  },
  {
    "date": 1626134400000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 627.13,
  },
  {
    "date": 1626134400000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 637.13,
  },
  {
    "date": 1626220800000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 662.13,
  },
  {
    "date": 1626393600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 617.13,
  },
  {
    "date": 1626393600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 637.13,
  },
  {
    "date": 1626480000000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 647.1300000000001,
  },
  {
    "date": 1626480000000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 652.1300000000001,
  },
  {
    "date": 1626652800000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 547.1300000000001,
  },
  {
    "date": 1626652800000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 552.1300000000001,
  },
  {
    "date": 1626739200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 592.1300000000001,
  },
  {
    "date": 1626739200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 594.96,
  },
  {
    "date": 1626825600000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 602.46,
  },
  {
    "date": 1626825600000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 622.46,
  },
  {
    "date": 1626912000000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 607.46,
  },
  {
    "date": 1627171200000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 568.96,
  },
  {
    "date": 1627171200000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 588.96,
  },
  {
    "date": 1627257600000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 631.46,
  },
  {
    "date": 1627516800000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 588.46,
  },
  {
    "date": 1627603200000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 593.46,
  },
  {
    "date": 1627603200000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 599.46,
  },
  {
    "date": 1627689600000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 571.46,
  },
  {
    "date": 1627776000000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 599.6600000000001,
  },
  {
    "date": 1627862400000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 581.55,
  },
  {
    "date": 1627948800000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 581.55,
  },
  {
    "date": 1628121600000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 601.55,
  },
  {
    "date": 1628294400000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 635.3299999999999,
  },
  {
    "date": 1628467200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 620.3299999999999,
  },
  {
    "date": 1628467200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 695.3299999999999,
  },
  {
    "date": 1628640000000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 627.3299999999999,
  },
  {
    "date": 1628985600000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 549.8299999999999,
  },
  {
    "date": 1629244800000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 454.83,
  },
  {
    "date": 1629590400000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 429.5,
  },
  {
    "date": 1629676800000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 429.9,
  },
  {
    "date": 1629763200000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 409.4,
  },
  {
    "date": 1629849600000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 317.4,
  },
  {
    "date": 1629936000000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 322.4,
  },
  {
    "date": 1629936000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 327.4,
  },
  {
    "date": 1630022400000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 732.4,
  },
  {
    "date": 1630022400000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 733.4,
  },
  {
    "date": 1630368000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 674.4,
  },
  {
    "date": 1630627200000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 669.4,
  },
  {
    "date": 1630713600000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 659.4,
  },
  {
    "date": 1630800000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 669.4,
  },
  {
    "date": 1630800000000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 719.4,
  },
  {
    "date": 1631232000000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 604.4,
  },
  {
    "date": 1631318400000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 624.4,
  },
  {
    "date": 1631577600000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 634.4,
  },
  {
    "date": 1631664000000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 639.4,
  },
  {
    "date": 1631664000000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 659.4,
  },
  {
    "date": 1632268800000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 619,
  },
  {
    "date": 1632528000000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 611,
  },
  {
    "date": 1633046400000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 300,
  },
  {
    "date": 1633219200000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 284,
  },
  {
    "date": 1633305600000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 284,
  },
  {
    "date": 1633478400000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 254,
  },
  {
    "date": 1633564800000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 284,
  },
  {
    "date": 1633564800000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 322,
  },
  {
    "date": 1633824000000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 332,
  },
  {
    "date": 1633910400000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 322,
  },
  {
    "date": 1634083200000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 372,
  },
  {
    "date": 1634774400000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 400,
  },
  {
    "date": 1634860800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 410,
  },
  {
    "date": 1634860800000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 460,
  },
  {
    "date": 1634947200000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 465,
  },
  {
    "date": 1635033600000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 467,
  },
  {
    "date": 1635379200000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 457,
  },
  {
    "date": 1635379200000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 589.52,
  },
  {
    "date": 1635724800000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 514.52,
  },
  {
    "date": 1636502400000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 462.52,
  },
  {
    "date": 1636502400000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 482.52,
  },
  {
    "date": 1637107200000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 437.52,
  },
  {
    "date": 1637625600000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 302.52,
  },
  {
    "date": 1638144000000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 215,
  },
  {
    "date": 1638144000000,
    "monthlyActiveAccounts": 6,
    "monthlyRevenue": 220,
  },
  {
    "date": 1638230400000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 240,
  },
  {
    "date": 1638316800000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 235,
  },
  {
    "date": 1638403200000,
    "monthlyActiveAccounts": 7,
    "monthlyRevenue": 240,
  },
  {
    "date": 1638403200000,
    "monthlyActiveAccounts": 8,
    "monthlyRevenue": 290,
  },
  {
    "date": 1638489600000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 300,
  },
  {
    "date": 1638835200000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 315,
  },
  {
    "date": 1639180800000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 215,
  },
  {
    "date": 1639353600000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 225,
  },
  {
    "date": 1639440000000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 230,
  },
  {
    "date": 1639785600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 250,
  },
  {
    "date": 1640649600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 260,
  },
  {
    "date": 1640649600000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 264,
  },
  {
    "date": 1640736000000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 609,
  },
  {
    "date": 1641168000000,
    "monthlyActiveAccounts": 9,
    "monthlyRevenue": 604,
  },
  {
    "date": 1641254400000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 616.5,
  },
  {
    "date": 1641513600000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 606.5,
  },
  {
    "date": 1641513600000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 611.5,
  },
  {
    "date": 1641772800000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 606.5,
  },
  {
    "date": 1641859200000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 610.5,
  },
  {
    "date": 1641859200000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 640.5,
  },
  {
    "date": 1642032000000,
    "monthlyActiveAccounts": 10,
    "monthlyRevenue": 641.5,
  },
  {
    "date": 1642032000000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 691.5,
  },
  {
    "date": 1642377600000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 671.5,
  },
  {
    "date": 1642377600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 731.5,
  },
  {
    "date": 1642464000000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 831.5,
  },
  {
    "date": 1642723200000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 896.5,
  },
  {
    "date": 1643068800000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 926.5,
  },
  {
    "date": 1643155200000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 931.5,
  },
  {
    "date": 1643500800000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 517.5,
  },
  {
    "date": 1643760000000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 422.5,
  },
  {
    "date": 1643932800000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 415,
  },
  {
    "date": 1643932800000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 465,
  },
  {
    "date": 1644105600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 460,
  },
  {
    "date": 1644105600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 466,
  },
  {
    "date": 1644624000000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 352,
  },
  {
    "date": 1644883200000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 392,
  },
  {
    "date": 1644969600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 377,
  },
  {
    "date": 1644969600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 379,
  },
  {
    "date": 1645574400000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 219,
  },
  {
    "date": 1645660800000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 199,
  },
  {
    "date": 1645747200000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 494,
  },
  {
    "date": 1645747200000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 514,
  },
  {
    "date": 1645833600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 515,
  },
  {
    "date": 1645833600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 516,
  },
  {
    "date": 1645833600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 526,
  },
  {
    "date": 1645920000000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 536,
  },
  {
    "date": 1645920000000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 576,
  },
  {
    "date": 1646179200000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 562,
  },
  {
    "date": 1646265600000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 764.5,
  },
  {
    "date": 1646438400000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 764.5,
  },
  {
    "date": 1646611200000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 711,
  },
  {
    "date": 1646784000000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 703,
  },
  {
    "date": 1647216000000,
    "monthlyActiveAccounts": 11,
    "monthlyRevenue": 703,
  },
  {
    "date": 1647302400000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 713,
  },
  {
    "date": 1647302400000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 728,
  },
  {
    "date": 1647388800000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 730,
  },
  {
    "date": 1647475200000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 740,
  },
  {
    "date": 1647907200000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 703,
  },
  {
    "date": 1647907200000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 721,
  },
  {
    "date": 1648166400000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 766,
  },
  {
    "date": 1648166400000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 769,
  },
  {
    "date": 1648252800000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 764,
  },
  {
    "date": 1648339200000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 544,
  },
  {
    "date": 1648339200000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 644,
  },
  {
    "date": 1648425600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 670,
  },
  {
    "date": 1648512000000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 650,
  },
  {
    "date": 1648684800000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 850,
  },
  {
    "date": 1648684800000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 854,
  },
  {
    "date": 1648684800000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 954,
  },
  {
    "date": 1648684800000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 1054,
  },
  {
    "date": 1648944000000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 950.5,
  },
  {
    "date": 1649116800000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 1845.5,
  },
  {
    "date": 1649203200000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 1879,
  },
  {
    "date": 1649203200000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 1979,
  },
  {
    "date": 1649203200000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 2029,
  },
  {
    "date": 1649289600000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 2059,
  },
  {
    "date": 1649376000000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 2066,
  },
  {
    "date": 1649548800000,
    "monthlyActiveAccounts": 22,
    "monthlyRevenue": 2096,
  },
  {
    "date": 1649548800000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 2116,
  },
  {
    "date": 1649721600000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 2156,
  },
  {
    "date": 1649721600000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 2166,
  },
  {
    "date": 1649808000000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 2195,
  },
  {
    "date": 1649894400000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 2175,
  },
  {
    "date": 1650067200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 2153,
  },
  {
    "date": 1650153600000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 2168,
  },
  {
    "date": 1650153600000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 2218,
  },
  {
    "date": 1650153600000,
    "monthlyActiveAccounts": 28,
    "monthlyRevenue": 2228,
  },
  {
    "date": 1650412800000,
    "monthlyActiveAccounts": 29,
    "monthlyRevenue": 2328,
  },
  {
    "date": 1650585600000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 2298,
  },
  {
    "date": 1650585600000,
    "monthlyActiveAccounts": 28,
    "monthlyRevenue": 2318,
  },
  {
    "date": 1650585600000,
    "monthlyActiveAccounts": 29,
    "monthlyRevenue": 2338,
  },
  {
    "date": 1650585600000,
    "monthlyActiveAccounts": 30,
    "monthlyRevenue": 2353,
  },
  {
    "date": 1650844800000,
    "monthlyActiveAccounts": 29,
    "monthlyRevenue": 2325,
  },
  {
    "date": 1651017600000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 2092,
  },
  {
    "date": 1651104000000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 2112,
  },
  {
    "date": 1651104000000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 2115,
  },
  {
    "date": 1651190400000,
    "monthlyActiveAccounts": 28,
    "monthlyRevenue": 2120,
  },
  {
    "date": 1651190400000,
    "monthlyActiveAccounts": 29,
    "monthlyRevenue": 2140,
  },
  {
    "date": 1651276800000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 1756,
  },
  {
    "date": 1651363200000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 1769,
  },
  {
    "date": 1651449600000,
    "monthlyActiveAccounts": 28,
    "monthlyRevenue": 1774,
  },
  {
    "date": 1651622400000,
    "monthlyActiveAccounts": 28,
    "monthlyRevenue": 2274,
  },
  {
    "date": 1651708800000,
    "monthlyActiveAccounts": 28,
    "monthlyRevenue": 1382,
  },
  {
    "date": 1651881600000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 1177,
  },
  {
    "date": 1651881600000,
    "monthlyActiveAccounts": 27,
    "monthlyRevenue": 1182,
  },
  {
    "date": 1651968000000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 1174,
  },
  {
    "date": 1652054400000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 1194,
  },
  {
    "date": 1652140800000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 1154,
  },
  {
    "date": 1652227200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 1254,
  },
  {
    "date": 1652227200000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 1257,
  },
  {
    "date": 1652400000000,
    "monthlyActiveAccounts": 23,
    "monthlyRevenue": 1277,
  },
  {
    "date": 1652400000000,
    "monthlyActiveAccounts": 24,
    "monthlyRevenue": 1282,
  },
  {
    "date": 1652486400000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 1297,
  },
  {
    "date": 1652572800000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 1307,
  },
  {
    "date": 1652659200000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 1284,
  },
  {
    "date": 1652659200000,
    "monthlyActiveAccounts": 26,
    "monthlyRevenue": 1344,
  },
  {
    "date": 1652832000000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 1469,
  },
  {
    "date": 1653004800000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 1381,
  },
  {
    "date": 1653004800000,
    "monthlyActiveAccounts": 25,
    "monthlyRevenue": 1386,
  },
  {
    "date": 1653523200000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 1303,
  },
  {
    "date": 1653609600000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 1313,
  },
  {
    "date": 1653955200000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 1217,
  },
  {
    "date": 1654041600000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 1227,
  },
  {
    "date": 1654041600000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 1277,
  },
  {
    "date": 1654214400000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 682,
  },
  {
    "date": 1654214400000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 732,
  },
  {
    "date": 1654387200000,
    "monthlyActiveAccounts": 21,
    "monthlyRevenue": 754,
  },
  {
    "date": 1654560000000,
    "monthlyActiveAccounts": 20,
    "monthlyRevenue": 742,
  },
  {
    "date": 1654992000000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 534,
  },
  {
    "date": 1654992000000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 539,
  },
  {
    "date": 1655078400000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 1519,
  },
  {
    "date": 1655856000000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 1275,
  },
  {
    "date": 1655942400000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 1279,
  },
  {
    "date": 1656028800000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 1284,
  },
  {
    "date": 1656201600000,
    "monthlyActiveAccounts": 12,
    "monthlyRevenue": 1284,
  },
  {
    "date": 1656374400000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 1314,
  },
  {
    "date": 1656460800000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 1321,
  },
  {
    "date": 1656547200000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 1406,
  },
  {
    "date": 1656633600000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 1346,
  },
  {
    "date": 1656633600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 1356,
  },
  {
    "date": 1656720000000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 1376,
  },
  {
    "date": 1656979200000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 1331,
  },
  {
    "date": 1657065600000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 1336,
  },
  {
    "date": 1657152000000,
    "monthlyActiveAccounts": 13,
    "monthlyRevenue": 1391,
  },
  {
    "date": 1657238400000,
    "monthlyActiveAccounts": 14,
    "monthlyRevenue": 1401,
  },
  {
    "date": 1657324800000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 1461,
  },
  {
    "date": 1657584000000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 1431,
  },
  {
    "date": 1657756800000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 481,
  },
  {
    "date": 1658016000000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 484,
  },
  {
    "date": 1658016000000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 489,
  },
  {
    "date": 1658275200000,
    "monthlyActiveAccounts": 17,
    "monthlyRevenue": 529,
  },
  {
    "date": 1658275200000,
    "monthlyActiveAccounts": 18,
    "monthlyRevenue": 599,
  },
  {
    "date": 1658275200000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 619,
  },
  {
    "date": 1658534400000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 580,
  },
  {
    "date": 1658707200000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 579,
  },
  {
    "date": 1658793600000,
    "monthlyActiveAccounts": 19,
    "monthlyRevenue": 574,
  },
  {
    "date": 1659312000000,
    "monthlyActiveAccounts": 15,
    "monthlyRevenue": 407,
  },
  {
    "date": 1659312000000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 437,
  },
  {
    "date": 1659571200000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 412,
  },
  {
    "date": 1659571200000,
    "monthlyActiveAccounts": 16,
    "monthlyRevenue": 416,
  }
]

var monthlyTotal = "534.34";



document.getElementById('monthlyTotal').innerText = monthlyTotal;


var makeChart = function(id, label, color, valueSelector) {

  var canvas = document.getElementById(id);
  var data = {
      labels: raw.map(x => x.date),
      datasets: [
          {
              label: label,
              yAxisID: 'A',
              fill: false,
              lineTension: 0.1,
              borderColor: color,
              borderCapStyle: 'butt',
              borderJoinStyle: 'miter',
              pointBorderColor: "rgba(0,0,0,0)",
              pointBackgroundColor: "rgba(0,0,0,0)",
              pointBorderWidth: 0,
              pointHoverRadius: 5,
              pointHoverBackgroundColor: "rgba(75,192,192,1)",
              pointHoverBorderColor: "rgba(220,220,220,1)",
              pointHoverBorderWidth: 1,
              pointRadius: 0,
              pointHitRadius: 10,
              data: raw.map(valueSelector),
          }
      ]
  };


  var option = {
	  showLines: true,
    scales: {
      xAxes: [ 
        {
          display: true,
          type: 'time',
          time: {
            unit: 'month'
          }
         }
        ],
        yAxes: [{
          id: 'A',
          type: 'linear',
          offset: true,
          position: 'right',
        }]
      },
     legend: {
       position: "right"
     }
  };
  var myLineChart = Chart.Line(canvas,{
	  data:data,
    options:option
  });

}

makeChart("mainRevenue", "revenue last 30d", "#2299ff", x => x.monthlyRevenue);
makeChart("mainAccounts", "paying accounts last 30d", "red", x => x.monthlyActiveAccounts);


</script>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><br>
<p>As happy as I am to see capsul succeed,  I'm still a bit of a self-hosting fundamentalist myself, and I want to put the majority of my work into projects that're friendly to self-determination and <a href="https://sequentialread.com/website-updates/">small energy efficient servers at home</a>.</p>
<p>But I'm tired of trying to &quot;manifest&quot; a reality where anyone can do this themselves. It's hard enough for me to do what I do, and I've been working my ass off for my entire life to get to where I am.  Self-hosting is such a shitty term because it sorta implies that you do it all by yourself. Not only is that unrealistic, it's also a lot less fun.</p>
<p>I'm still trying to figure out what I want to build, let alone what to call it.  I want to build slow-cluster software that's all about NAT traversal and good user interface instead of about scalability.  I want to build something like the fediverse, software that can form circles of friends. Not a social media of URLs, but a homebrew web server infrastructure cleanly abstracted away from the hardware, abstracted away from the individual, and free to flow like water.</p>
<p>(Albiet slowly when it has to flow through the internet connection in grandma's basement 👵)</p>
<p>Maybe I want to make a homebrew &quot;cloud&quot; IaaSS (Infrastructure as a SelfService 😛) that server admins can operate with a couple friends. Whatever it is, I think it should combine the best aspects of both Federation and Clusters, while attempting to avoid the worst pitfalls of both.</p>
<p><strong>So in the end, It's not Federation <em>vs.</em> Clustering...</strong></p>
<p>It's</p>
<h2 id="federationclustering"><em><strong>Federation 💘 Clustering</strong></em></h2>
<p>👶?</p>
<p>Tune in next time 😉</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Greenhouse Retrospective and Future]]></title><description><![CDATA[I think the short, pithy description on homebrewserver.club might summarize it best: 

Take the ‘home’ in homebrew literally and the ‘self’ in self-hosting figuratively]]></description><link>https://sequentialread.com/greenhouse-retrospective-and-future/</link><guid isPermaLink="false">62e401cb3b133a00010a199c</guid><category><![CDATA[products]]></category><category><![CDATA[cloud]]></category><category><![CDATA[FLOSS]]></category><category><![CDATA[Self-Hosting]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Fri, 29 Jul 2022 21:41:15 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2022/07/not_stonks.jpeg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html--><img src="https://sequentialread.com/content/images/2022/07/not_stonks.jpeg" alt="Greenhouse Retrospective and Future"><p><em>Previous post in this series: <a href="https://sequentialread.com/greenhouse-alpha/">🥳 Greenhouse Enters Alpha Test Phase!! 🎉</a></em></p>
<hr>
<p>I did the thing! I created <a href="https://greenhouse-alpha.server.garden/">greenhouse</a> almost exactly like how I imagined it <a href="https://sequentialread.com/pragmatic-path-towards-non-technical-users-owning-their-own-data/">six years ago in 2016</a>.</p>
<p>I recieved a lot of feedback on the project when I shared it with the world. While most people agreed it was pretty cool, I recieved a lot of criticism as well:</p>
<blockquote>
<p>👹 Self-hosting is dangerous and you are inviting people to get hacked! The lawyers will come after you! The world is scary and everyone should just keep hiding in thier webcage that the big-tech panopticons provide!</p>
</blockquote>
<blockquote>
<p>🤮 Your website sucks! It looks bad!</p>
</blockquote>
<p>Some criticism came in the form of valid questions, questions to which my answers had to be disappointing. Because greenhouse is so different &amp; doesn't really fit into a well-known category, it's architechture, feature set, and user experience confused and surprised many potential users:</p>
<blockquote>
<p>🧐 How do I self-host the tunnnel gateway server part of greenhouse?</p>
</blockquote>
<p><em>You can, but it's really hard. You aren't supposed to, you don't need to, that's the whole point. You just run the tunnel client daemon on your server computer and configure it with the CLI or GUI app. It's like this because I wanted it to be as easily as possible to get started with.</em></p>
<blockquote>
<p>🤨 Does it support caching? Like on the "edge" server where caching is most effective?</p>
</blockquote>
<p><em>No, it can't support caching like that because the greenhouse tunnel server can't read your traffic; the HTTP traffic stays encrypted via HTTPS. IMO this is a good thing.</em></p>
<blockquote>
<p>🥱 Why does my page load so slowly? How can I make it faster?</p>
</blockquote>
<p><em>It's because you live in Europe and right now there is only one greenhouse tunnel server, and it's in NA. In the future I'll have multiple servers in different regions but for now it's just the one.</em></p>
<p><em>Also, the tunnel introduces multiple network roundtrips right now. In the future I can at least cut those extra trips in half by replacing <a href="https://github.com/hashicorp/yamux"><code>yamux</code></a> with the <a href="https://en.wikipedia.org/wiki/QUIC">QUIC protocol</a>, but the traffic will always have to go through the tunnel during the first page load no matter what, so it will always be higher latency than normal.</em></p>
<p>The harshest criticism, however, came in the form of usage statistics. Most people agreed it was a neat idea, and some people did try it out, but over time, it got used less and less. It did not grow organically.</p><!--kg-card-end: html--><!--kg-card-begin: html-->
<!--kg-card-end: markdown--><!--kg-card-begin: html--><script src="https://picopublish.sequentialread.com/files/capsul-stonks-may-2022/Chart.bundle.js">
</script>

<style>
.chart-container {
  margin: 1rem;
}
</style>

<div class="chart-container"><div class="chartjs-size-monitor"><div class="chartjs-size-monitor-expand"><div class></div></div><div class="chartjs-size-monitor-shrink"><div class></div></div></div>
<h1 id="notstonks">new accounts - <span id="monthlyAccounts">5.25</span>/mo all-time avg</h1>
<canvas id="mainAccounts" style="display: block; width: 534px; height: 213px;" class="chartjs-render-monitor" width="534" height="213"></canvas>
</div>

<hr>


<div class="chart-container"><div class="chartjs-size-monitor"><div class="chartjs-size-monitor-expand"><div class></div></div><div class="chartjs-size-monitor-shrink"><div class></div></div></div>
<h1>traffic - <span id="monthlyTraffic">1684.93</span>MB/mo all-time avg</h1>
<canvas id="mainTraffic" style="display: block; width: 534px; height: 213px;" class="chartjs-render-monitor" width="534" height="213"></canvas>
</div>



<script>
var raw = [
  {
    "date": 1634533200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 72.808282
  },
  {
    "date": 1634619600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.90566
  },
  {
    "date": 1634706000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 83.73611
  },
  {
    "date": 1634792400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 233.976987
  },
  {
    "date": 1634878800000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 355.203912
  },
  {
    "date": 1634965200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 466.724363
  },
  {
    "date": 1635051600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 594.593379
  },
  {
    "date": 1635138000000,
    "monthlyAccounts": 8,
    "monthlyTraffic": 788.790712
  },
  {
    "date": 1635224400000,
    "monthlyAccounts": 19,
    "monthlyTraffic": 1017.247512
  },
  {
    "date": 1635310800000,
    "monthlyAccounts": 22,
    "monthlyTraffic": 1186.057461
  },
  {
    "date": 1635397200000,
    "monthlyAccounts": 22,
    "monthlyTraffic": 1188.083505
  },
  {
    "date": 1635483600000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 1224.354182
  },
  {
    "date": 1635570000000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 2043.564828
  },
  {
    "date": 1635656400000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 2096.786794
  },
  {
    "date": 1635829200000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 2462.781837
  },
  {
    "date": 1635915600000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 2464.090316
  },
  {
    "date": 1636002000000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 2464.692234
  },
  {
    "date": 1636088400000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 2465.068404
  },
  {
    "date": 1636174800000,
    "monthlyAccounts": 25,
    "monthlyTraffic": 2550.256488
  },
  {
    "date": 1636261200000,
    "monthlyAccounts": 25,
    "monthlyTraffic": 2551.134553
  },
  {
    "date": 1636351200000,
    "monthlyAccounts": 25,
    "monthlyTraffic": 2551.656154
  },
  {
    "date": 1636437600000,
    "monthlyAccounts": 25,
    "monthlyTraffic": 2552.554761
  },
  {
    "date": 1636524000000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2626.077031
  },
  {
    "date": 1636610400000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2665.795173
  },
  {
    "date": 1636696800000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2667.077631
  },
  {
    "date": 1636783200000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2668.476325
  },
  {
    "date": 1636869600000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2675.04069
  },
  {
    "date": 1636956000000,
    "monthlyAccounts": 27,
    "monthlyTraffic": 2732.695539
  },
  {
    "date": 1637042400000,
    "monthlyAccounts": 27,
    "monthlyTraffic": 2749.472028
  },
  {
    "date": 1637128800000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2705.036821
  },
  {
    "date": 1637215200000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2712.927563
  },
  {
    "date": 1637301600000,
    "monthlyAccounts": 27,
    "monthlyTraffic": 2753.457577
  },
  {
    "date": 1637388000000,
    "monthlyAccounts": 27,
    "monthlyTraffic": 2650.462887
  },
  {
    "date": 1637474400000,
    "monthlyAccounts": 26,
    "monthlyTraffic": 2557.948606
  },
  {
    "date": 1637560800000,
    "monthlyAccounts": 24,
    "monthlyTraffic": 2464.127287
  },
  {
    "date": 1637647200000,
    "monthlyAccounts": 23,
    "monthlyTraffic": 2339.385448
  },
  {
    "date": 1637733600000,
    "monthlyAccounts": 20,
    "monthlyTraffic": 2149.260418
  },
  {
    "date": 1637820000000,
    "monthlyAccounts": 9,
    "monthlyTraffic": 1927.034346
  },
  {
    "date": 1637906400000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 1779.53129
  },
  {
    "date": 1637992800000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 1778.002964
  },
  {
    "date": 1638079200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 1742.406217
  },
  {
    "date": 1638165600000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 923.811004
  },
  {
    "date": 1638252000000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 871.227702
  },
  {
    "date": 1638338400000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 1034.083024
  },
  {
    "date": 1638424800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 751.582652
  },
  {
    "date": 1638511200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 796.816409
  },
  {
    "date": 1638597600000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 837.239406
  },
  {
    "date": 1638684000000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 859.148301
  },
  {
    "date": 1638770400000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 811.867354
  },
  {
    "date": 1638856800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 827.255891
  },
  {
    "date": 1638943200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 838.900042
  },
  {
    "date": 1639029600000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 874.53852
  },
  {
    "date": 1639116000000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 824.953039
  },
  {
    "date": 1639202400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 810.539011
  },
  {
    "date": 1639288800000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 825.41092
  },
  {
    "date": 1639375200000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1082.540014
  },
  {
    "date": 1639461600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1123.279478
  },
  {
    "date": 1639548000000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 1087.90728
  },
  {
    "date": 1639634400000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 1097.48824
  },
  {
    "date": 1639720800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 1259.441332
  },
  {
    "date": 1639807200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 1432.601423
  },
  {
    "date": 1639893600000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 6097.606313
  },
  {
    "date": 1639980000000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 6242.620789
  },
  {
    "date": 1640066400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 6387.103907
  },
  {
    "date": 1640152800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 6552.028638
  },
  {
    "date": 1640239200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 6550.099464
  },
  {
    "date": 1640325600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 6546.959138
  },
  {
    "date": 1640412000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 6541.12041
  },
  {
    "date": 1640498400000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 6574.640348
  },
  {
    "date": 1640584800000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 6679.618928
  },
  {
    "date": 1640671200000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 6823.215283
  },
  {
    "date": 1640757600000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 6862.36139
  },
  {
    "date": 1640844000000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 6871.892362
  },
  {
    "date": 1640930400000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 6810.773089
  },
  {
    "date": 1641535200000,
    "monthlyAccounts": 8,
    "monthlyTraffic": 6555.208886
  },
  {
    "date": 1641621600000,
    "monthlyAccounts": 8,
    "monthlyTraffic": 6541.763228
  },
  {
    "date": 1641708000000,
    "monthlyAccounts": 8,
    "monthlyTraffic": 6529.29868
  },
  {
    "date": 1641794400000,
    "monthlyAccounts": 8,
    "monthlyTraffic": 6512.459578
  },
  {
    "date": 1641880800000,
    "monthlyAccounts": 8,
    "monthlyTraffic": 6502.430548
  },
  {
    "date": 1641967200000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 6252.948923
  },
  {
    "date": 1642053600000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 6216.035284
  },
  {
    "date": 1642140000000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 6203.015665
  },
  {
    "date": 1642226400000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 6186.488681
  },
  {
    "date": 1642312800000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 6003.140056
  },
  {
    "date": 1642399200000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 5828.422903
  },
  {
    "date": 1642485600000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 1125.080542
  },
  {
    "date": 1642572000000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 954.696357
  },
  {
    "date": 1642658400000,
    "monthlyAccounts": 7,
    "monthlyTraffic": 789.883039
  },
  {
    "date": 1642744800000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 610.129624
  },
  {
    "date": 1642831200000,
    "monthlyAccounts": 6,
    "monthlyTraffic": 640.280779
  },
  {
    "date": 1642917600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 675.461355
  },
  {
    "date": 1643004000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 689.065739
  },
  {
    "date": 1643090400000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 680.835133
  },
  {
    "date": 1643176800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 594.378774
  },
  {
    "date": 1643263200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 465.95402
  },
  {
    "date": 1643349600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 446.609138
  },
  {
    "date": 1643436000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 446.207943
  },
  {
    "date": 1643522400000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 369.627553
  },
  {
    "date": 1643608800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 390.675359
  },
  {
    "date": 1643695200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 397.869871
  },
  {
    "date": 1643781600000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 414.947796
  },
  {
    "date": 1643868000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 530.68707
  },
  {
    "date": 1643954400000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 531.909178
  },
  {
    "date": 1644040800000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 533.65218
  },
  {
    "date": 1644127200000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 536.475643
  },
  {
    "date": 1644213600000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 525.959206
  },
  {
    "date": 1644300000000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 534.512739
  },
  {
    "date": 1644386400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 539.205617
  },
  {
    "date": 1644472800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 607.262657
  },
  {
    "date": 1644559200000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 613.421823
  },
  {
    "date": 1644645600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 696.101253
  },
  {
    "date": 1644732000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 703.85535
  },
  {
    "date": 1644818400000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 706.011906
  },
  {
    "date": 1644904800000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 999.290073
  },
  {
    "date": 1644991200000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1023.417398
  },
  {
    "date": 1645077600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1021.972066
  },
  {
    "date": 1645164000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1021.289379
  },
  {
    "date": 1645250400000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1025.96726
  },
  {
    "date": 1645336800000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1056.354781
  },
  {
    "date": 1645423200000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1052.190275
  },
  {
    "date": 1645509600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1059.497989
  },
  {
    "date": 1645596000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1065.002373
  },
  {
    "date": 1645682400000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 1464.804437
  },
  {
    "date": 1645768800000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2175.519266
  },
  {
    "date": 1645855200000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2289.696612
  },
  {
    "date": 1645941600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2284.884069
  },
  {
    "date": 1646028000000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2304.88704
  },
  {
    "date": 1646114400000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2280.071337
  },
  {
    "date": 1646200800000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2291.77703
  },
  {
    "date": 1646287200000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2286.199786
  },
  {
    "date": 1646373600000,
    "monthlyAccounts": 5,
    "monthlyTraffic": 2296.0944
  },
  {
    "date": 1646460000000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 2180.925912
  },
  {
    "date": 1646546400000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 2179.970574
  },
  {
    "date": 1646632800000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 2190.760835
  },
  {
    "date": 1646719200000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 2216.18816
  },
  {
    "date": 1646805600000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 2212.968488
  },
  {
    "date": 1646892000000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 2202.705591
  },
  {
    "date": 1646978400000,
    "monthlyAccounts": 4,
    "monthlyTraffic": 2196.98695
  },
  {
    "date": 1647064800000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 2123.434007
  },
  {
    "date": 1647320400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 2020.911085
  },
  {
    "date": 1647406800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 2024.726696
  },
  {
    "date": 1647493200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 2020.726852
  },
  {
    "date": 1647579600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1725.890716
  },
  {
    "date": 1647666000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1712.713334
  },
  {
    "date": 1647752400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1715.149909
  },
  {
    "date": 1647838800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1698.548164
  },
  {
    "date": 1647925200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1691.914016
  },
  {
    "date": 1648011600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1672.778206
  },
  {
    "date": 1648098000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1660.100497
  },
  {
    "date": 1648184400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1626.750105
  },
  {
    "date": 1648270800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1619.455655
  },
  {
    "date": 1648357200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1194.700654
  },
  {
    "date": 1648443600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 477.300225
  },
  {
    "date": 1648530000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 350.66929
  },
  {
    "date": 1648616400000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 383.467499
  },
  {
    "date": 1648702800000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 365.928241
  },
  {
    "date": 1648789200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 375.426896
  },
  {
    "date": 1648875600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 352.530151
  },
  {
    "date": 1648962000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 359.383987
  },
  {
    "date": 1649048400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 339.309129
  },
  {
    "date": 1649134800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 348.041046
  },
  {
    "date": 1649221200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 351.839511
  },
  {
    "date": 1649307600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 351.555598
  },
  {
    "date": 1649394000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 328.134836
  },
  {
    "date": 1649480400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 324.822061
  },
  {
    "date": 1649566800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 318.193291
  },
  {
    "date": 1649653200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 349.036025
  },
  {
    "date": 1649739600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 359.157301
  },
  {
    "date": 1649826000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 366.641569
  },
  {
    "date": 1649912400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 370.550768
  },
  {
    "date": 1649998800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 362.535815
  },
  {
    "date": 1650085200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 360.569667
  },
  {
    "date": 1650171600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 378.122408
  },
  {
    "date": 1650258000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 364.667212
  },
  {
    "date": 1650344400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 362.982164
  },
  {
    "date": 1650430800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 364.413485
  },
  {
    "date": 1650517200000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 477.200502
  },
  {
    "date": 1650603600000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 475.563301
  },
  {
    "date": 1650690000000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 475.164936
  },
  {
    "date": 1650776400000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 474.763327
  },
  {
    "date": 1650862800000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 479.552494
  },
  {
    "date": 1650949200000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 465.079233
  },
  {
    "date": 1651035600000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 552.382128
  },
  {
    "date": 1651122000000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 582.436279
  },
  {
    "date": 1651208400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 621.181437
  },
  {
    "date": 1651294800000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 647.334033
  },
  {
    "date": 1651381200000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 665.792555
  },
  {
    "date": 1651467600000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 674.181541
  },
  {
    "date": 1651554000000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 738.632468
  },
  {
    "date": 1651640400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 754.5735
  },
  {
    "date": 1651726800000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 767.38806
  },
  {
    "date": 1651813200000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 911.410975
  },
  {
    "date": 1651899600000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 996.877173
  },
  {
    "date": 1651986000000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1040.74583
  },
  {
    "date": 1652072400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1113.491469
  },
  {
    "date": 1652158800000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1180.719398
  },
  {
    "date": 1652245200000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1194.577436
  },
  {
    "date": 1652331600000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1205.644463
  },
  {
    "date": 1652418000000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1252.595328
  },
  {
    "date": 1652504400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1294.930573
  },
  {
    "date": 1652590800000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1345.922857
  },
  {
    "date": 1652677200000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1410.138008
  },
  {
    "date": 1652763600000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1492.859433
  },
  {
    "date": 1652850000000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1548.459415
  },
  {
    "date": 1652936400000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1591.818121
  },
  {
    "date": 1653022800000,
    "monthlyAccounts": 3,
    "monthlyTraffic": 1670.476223
  },
  {
    "date": 1653109200000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1599.536821
  },
  {
    "date": 1653195600000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1645.472519
  },
  {
    "date": 1653282000000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1699.664075
  },
  {
    "date": 1653368400000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1742.937476
  },
  {
    "date": 1653454800000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1763.734983
  },
  {
    "date": 1653541200000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1779.424238
  },
  {
    "date": 1653627600000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1719.105864
  },
  {
    "date": 1653714000000,
    "monthlyAccounts": 2,
    "monthlyTraffic": 1739.428303
  },
  {
    "date": 1653800400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1821.767053
  },
  {
    "date": 1653886800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1842.333067
  },
  {
    "date": 1653973200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1830.94481
  },
  {
    "date": 1654059600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 1976.42464
  },
  {
    "date": 1654146000000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1913.796148
  },
  {
    "date": 1654232400000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1891.124482
  },
  {
    "date": 1654318800000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1869.451951
  },
  {
    "date": 1654405200000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1721.624884
  },
  {
    "date": 1654491600000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1624.651984
  },
  {
    "date": 1654578000000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1572.071156
  },
  {
    "date": 1654664400000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1493.450242
  },
  {
    "date": 1654750800000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1423.494148
  },
  {
    "date": 1654837200000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1371.796847
  },
  {
    "date": 1654923600000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1350.150299
  },
  {
    "date": 1655010000000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1296.045913
  },
  {
    "date": 1655096400000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1244.356217
  },
  {
    "date": 1655182800000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1181.043747
  },
  {
    "date": 1655269200000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1111.412341
  },
  {
    "date": 1655355600000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 1006.182876
  },
  {
    "date": 1655442000000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 946.101419
  },
  {
    "date": 1655528400000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 891.62694
  },
  {
    "date": 1655614800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 807.76336
  },
  {
    "date": 1655701200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 759.637991
  },
  {
    "date": 1655787600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 701.585366
  },
  {
    "date": 1655874000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 633.950655
  },
  {
    "date": 1655960400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 581.27063
  },
  {
    "date": 1656046800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 543.627913
  },
  {
    "date": 1656133200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 562.041912
  },
  {
    "date": 1656219600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 528.157972
  },
  {
    "date": 1656306000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 474.554373
  },
  {
    "date": 1656392400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 305.316436
  },
  {
    "date": 1656478800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 246.78945
  },
  {
    "date": 1656565200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 230.114652
  },
  {
    "date": 1656651600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 66.539895
  },
  {
    "date": 1656738000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 56.564784
  },
  {
    "date": 1656824400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 56.974087
  },
  {
    "date": 1656910800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 57.265027
  },
  {
    "date": 1656997200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 57.505917
  },
  {
    "date": 1657083600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 57.843176
  },
  {
    "date": 1657170000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.953802
  },
  {
    "date": 1657256400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 74.082395
  },
  {
    "date": 1657342800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.942437
  },
  {
    "date": 1657429200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.89817
  },
  {
    "date": 1657515600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 74.049201
  },
  {
    "date": 1657602000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.88806
  },
  {
    "date": 1657688400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.851539
  },
  {
    "date": 1657774800000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.796376
  },
  {
    "date": 1657861200000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.45886
  },
  {
    "date": 1657947600000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.481258
  },
  {
    "date": 1658034000000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.500713
  },
  {
    "date": 1658120400000,
    "monthlyAccounts": 1,
    "monthlyTraffic": 73.641131
  },
  {
    "date": 1658206800000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 73.190194
  },
  {
    "date": 1658293200000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 73.220721
  },
  {
    "date": 1658379600000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 73.170294
  },
  {
    "date": 1658466000000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 72.912631
  },
  {
    "date": 1658552400000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 73.019384
  },
  {
    "date": 1658638800000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 72.984411
  },
  {
    "date": 1658725200000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 32.032836
  },
  {
    "date": 1658811600000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 27.384952
  },
  {
    "date": 1658898000000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 27.777735
  },
  {
    "date": 1658984400000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 28.096367
  },
  {
    "date": 1659070800000,
    "monthlyAccounts": 0,
    "monthlyTraffic": 27.997835
  }
];

var totalTraffic = "1684.93";
var totalAccounts = "5.25";

document.getElementById('monthlyTraffic').innerText = totalTraffic;

document.getElementById('monthlyAccounts').innerText = totalAccounts;


var makeChart = function(id, label, color, valueSelector) {

  var canvas = document.getElementById(id);
  var data = {
      labels: raw.map(x => x.date),
      datasets: [
          {
              label: label,
              yAxisID: 'A',
              fill: false,
              lineTension: 0.1,
              borderColor: color,
              borderCapStyle: 'butt',
              borderJoinStyle: 'miter',
              pointBorderColor: "rgba(0,0,0,0)",
              pointBackgroundColor: "rgba(0,0,0,0)",
              pointBorderWidth: 0,
              pointHoverRadius: 5,
              pointHoverBackgroundColor: "rgba(75,192,192,1)",
              pointHoverBorderColor: "rgba(220,220,220,1)",
              pointHoverBorderWidth: 1,
              pointRadius: 0,
              pointHitRadius: 10,
              data: raw.map(valueSelector),
          }
      ]
  };


  var option = {
	  showLines: true,
    scales: {
      xAxes: [ 
        {
          display: true,
          type: 'time',
          time: {
            unit: 'month'
          }
         }
        ],
        yAxes: [{
          id: 'A',
          type: 'linear',
          offset: true,
          position: 'right',
        }]
      },
     legend: {
       position: "right"
     }
  };
  var myLineChart = Chart.Line(canvas,{
	  data:data,
    options:option
  });

}

makeChart("mainAccounts", "new accounts last 30d", "#2299ff", x => x.monthlyAccounts);
makeChart("mainTraffic", "traffic MB last 30d", "red", x => x.monthlyTraffic);




</script>


<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>So where does this leave me? Did I fail? What does it all mean?</p>
<p>It's no secret that greenhouse has always been an ideologically motivated project. It was practically <em><strong>fundamentalist</strong></em> in terms of how it viewed self-hosting: The greenhouse service was supposed to make it easier for someone to run thier own server, leave it on 24/7, without them having to relinquish any privacy, agency, ownership, and control.  If you didn't want to run a server, greenhouse wouldn't be very useful to you.</p>
<p>But at the same time, in the interest of ease of use, it was designed from the ground up as a public-cloud-style Infrastructure As A Service (IaaS) provider. You pay the service some miniscule amount of money per unit of bandwidth that you consume; at the end of the month it would bill your credit card automatically. At least that was the plan.</p>
<p>Even though it was designed to follow the <a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege">principle of least authority</a> and defer all security-relevant decisions and processes to the self-hoster and thier server, at the end of the day greenhouse <em>is</em> a 3rd party service provider.</p>
<p>Through the combination of these two factors, I think it was always doomed to fail. On one had, it'll probably only appeal to self-hosting fundamentalists. But on the other hand, its a 3rd party service, and self-hosting fundamentalists tend to hate 3rd party services.</p>
<p>In terms of the technology and how it worked, greenhouse was fine. Great, even.  But in terms of how it made people feel, it was disapointing. And to make matters worse, because it required a server computer to be running 24/7, its usefulness to the average person was severely limited.</p>
<p>I spent a lot of time and effort creating a GUI for greenhouse and porting it to the Mac and Windows platforms because I wanted to include those platforms and include everyone who uses them.  But I think in the end what I produced was nothing more than a curiosity. No one wants to leave thier Mac or PC laptop on all the time so the server will stay up.</p>
<h2 id="howtofixithowmythinkinghaschanged">How to fix it? How my thinking has changed</h2>
<p>I mentioned that the idea for greenhouse is over 6 years old at this point.  A lot has changed in 6 years, both in terms of the cutting edge of self-hosted services as well as my own personal experiences with them and feelings about them.</p>
<p>First of all, I saw the rise of <a href="https://en.wikipedia.org/wiki/Fediverse">&quot;the fediverse&quot;</a> (<a href="https://activitypub.rocks/">ActivityPub</a>-compatible social-media and microblogging servers starting with <a href="https://github.com/mastodon/mastodon">mastodon</a>) and similar networks like <a href="https://matrix.org">matrix</a>.</p>
<p>I also joined / helped build and maintain infrastructure for <a href="https://cyberia.club">Cyberia Computer Club</a>, including the loosely associated VPS provider <a href="https://capsul.org">capsul.org</a>. At first I was reluctant to spend my time on capsul because I saw it as only marginally better than the large-scale commercial offerings like DigitalOcean. Same shit, different day kind of thing. My ideology was telling me that instead of custodial services, I should be focusing on projects that allow people to have <em><strong>ownership</strong></em> (physical custody) of their data and processes.</p>
<p>But over time, my outlook started to change. I had a lot of fun with Cyberia as it continued to grow and develop, including renting a commercial unit &amp; founding a new hackerspace dubbed <a href="https://layerze.ro/">Layer Zero</a>.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sequentialread.com/content/images/2022/07/lz3.jpg" class="kg-image" alt="Greenhouse Retrospective and Future"><figcaption>Layer Zero has become a comfy and excellent place to socialize, meet new people, and work on projects of all kinds, from software to hardware. If you are ever in the Twin Cities of Minneapolis and Saint Paul Minnesota, come visit us!</figcaption></figure><!--kg-card-begin: markdown--><br>
<p>I also read some very passionate takes on the Fediverse architechture and how it can change the way we view software and networks, like <a href="https://friend.camp/@darius">Darius Kazemi's</a> <a href="https://runyourown.social/">runyourown.social</a> and <a href="https://test.roelof.info/author/rra.html">Roel Roscam Abbing (<code>rra</code>)'s</a> <a href="https://test.roelof.info/seven-theses.html">Seven Theses On The Fediverse And The Becoming Of Floss</a>. These ideas made a big impact on me.  I think the short, pithy description on <a href="https://homebrewserver.club/">homebrewserver.club</a> might summarize it best:</p>
<blockquote>
<p><strong>Take the ‘home’ in homebrewserver.club literally and the ‘self’ in self-hosting figuratively</strong><br>
That means we try to host from our homes rather than from data centres - a.k.a. ‘the cloud’ - and we try to host <em>for and with our communities rather than just for ourselves.</em></p>
</blockquote>
<p>That &quot;for and with our communities&quot; bit was the part I was missing. It's not &quot;same shit, different day&quot; like I had originally feared; with fediverse-style networks and similar community projects, yes, the person with the server can still surveil, censor, and falsify everything of &quot;yours&quot; that they host for you...  But that power dynamic is different when it exists outside of commerce and capital, when it's a part of a local gift economy or federated network of &quot;indie&quot; &amp; self-determined servers.</p>
<p>So I think I ultimately want to redesign and re-brand greenhouse/server.garden as software that folks mostly run on thier own computers, and as something that can be connected to form ad-hoc federated networks.</p>
<h2 id="notjusttechbutalsotrust">Not just tech, but also trust</h2>
<p>Right now greenhouse is purely a &quot;trustless&quot; networking service, but redesigning it this way could open up new possibilities because it would open up a new dimension of trust: The trust the fedi-users have for thier server admins, and the trust that fedi-admins have for the fellow admins that they federate with.</p>
<p>&quot;Trustless&quot; can be cool, but having a server to trust is incredibly practical. I can imagine:</p>
<ul>
<li>Github Pages / Backblaze / neocities style static content hosting and object storage</li>
<li>Amazon RDS style &quot;managed&quot; replicated relational databases</li>
<li>Fly<a https: sequentialread.com greenhouse-retrospective-and-future href></a>.io style distributed linux container platform
<ul>
<li>Specifically, an easy-ish way to do redundancy and failover for arbitrary server apps.</li>
</ul>
</li>
</ul>
<p>I have a lot of ideas for this, as usual I've been thinking about it a lot...  In my imagination, two or more friends can set up thier own home servers and install this software on it, then trust each-other's servers so they'll federate with eachother.  Then our two-or-more admins can create accounts for anyone who wants to use the servers.  The admins' friends can use them for static content publishing, as a greenhouse-style network gateway for thier own server, and potentially even as a &quot;cloud-ish&quot; compute provider.  The best part: if one of those servers goes down, the other one can pick up the slack.</p>
<p>Its a lot, but I think this is the direction I wanna go in. I'll probably start with the static content hosting / object storage feature, eventually integrate the <a href="https://git.sequentialread.com/sqr/threshold">threshold</a> / <a href="https://git.sequentialread.com/sqr/greenhouse-daemon">greenhouse daemon</a> tunnel gateway feature, and see how things progress from there.</p>
<p>I have all kinds of ideas for this; with both a tunnel gateway and static content capability, the platform could offer a kind of hybrid hosting where you run a server app on your computer or phone which is &quot;live&quot; while your computer is turned on, greenhouse style, but as soon as you turn it off, it falls back to a cached version that is hosted by the federated platform.</p>
<p>For example, folks could run thier own <a href="https://owncast.online/">owncast</a> video livestreams this way without having to set up a server for it. When they turn off their computer the stream goes down, but the website stays up.  I think this could be useful for all kinds of apps, it would add a new dimension of flexibility where folks can casually self-host applications on the internet, even interactive applications, from computers which aren't servers — laptops/phones/gaming rigs which aren't on all the time.</p>
<p>By the way, I'm still trying to figure out what to call it. I need to come up with a name and a way to &quot;market&quot; it so that it makes people feel cool or &quot;sexy&quot; when they use it. (Thanks to my friend <a href="https://j3s.sh/">j3s</a> for these insights 😛)</p>
<p>If you have any suggestions, leave a comment!</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[When It Does Not Listen for Thee, Ask for Whom the Server Listens  (Understanding Listening Addresses)]]></title><description><![CDATA[Why isn't it working? Why can't we connect? ....

The first step is to figure out whether or not the application is listening or not, and if so, what listening address(es) it's using.
]]></description><link>https://sequentialread.com/understanding-listening-addresses/</link><guid isPermaLink="false">61f6d953d30cc100015a8e47</guid><category><![CDATA[networking]]></category><category><![CDATA[Self-Hosting]]></category><category><![CDATA[operations]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Tue, 26 Jul 2022 23:18:20 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2022/07/bullet.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2022/07/bullet.png" alt="When It Does Not Listen for Thee, Ask for Whom the Server Listens  (Understanding Listening Addresses)"><p><em>This post is part of a new experiment; I'm toying with the idea of writing a concise and approachable &quot;folk textbook&quot; covering the conceptual basics of what I do; using Linux and other operating systems like MacOS and Windows, home routers, networking software, servers, virtual machines, linux containers, etc. I'm starting out by writing a few drafts or tidbits in this style as blog articles.</em></p>
<p><em>If you're new to running server applications or even if you're an experienced application developer who's never studied computer networking (TCP/IP specifically), there's a good chance you use listening addresses all the time without fully understanding how they work or what they're doing.</em></p>
<p><em>I definitely spent many years as a professional software developer before I started to <a href="https://sequentialread.com/http-empty-response-problem-with-asp-net-core-1-0-on-docker-linked-container/">learn</a> all the details of how listening addresses work, and I wish I had found a resource like this article earlier.</em></p>
<p><em>To make matters worse, it seems like every application has slightly different behaviour when it comes to listening addresses and may parse or represent them differently. Too often these applications are not designed to be easy to use and they don't explain anything; they simply assume that you already know what you are doing.</em></p>
<h2 id="whylisteningaddressesmatter">Why Listening Addresses Matter</h2>
<p>When starting to run a new server application such as a webserver or other network service, often we will start it up, try to connect to it, and...  No dice. Why isn't it working? Why can't we connect? There are many possible reasons why, and this article aims to explain what could go wrong and how to figure out what's happening.</p>
<p>Our first step: Figure out whether or not the application is listening at all, and if so, <em>what listening address(es) it's using.</em></p>
<h3 id="identifyingalisteningaddress">Identifying a Listening Address</h3>
<p>A listening address is typically writen like a normal network address, that is, it's written as <code>&lt;host&gt;:&lt;port&gt;</code>.</p>
<p><code>&lt;host&gt;</code> here should almost always be an IP address, and <code>&lt;port&gt;</code> should almost always be a single port number. However, in terms of how they are represented in configuration files and logs, there's no strict standard, and developers ocasionally implement some, shall we say, &quot;creative&quot; representations.</p>
<p>Here are some examples of what I would call &quot;standard&quot; listening addresses that use IPv4 addresses:</p>
<ul>
<li><code>127.0.0.1:8080</code></li>
<li><code>0.0.0.0:22</code></li>
<li><code>123.45.67.8:35871</code></li>
</ul>
<p>And here are some that use IPv6 addresses:</p>
<ul>
<li><code>[::1]:8080</code></li>
<li><code>[::]:22</code></li>
<li><code>[2607:fb90:1788:efbd:d3f9:555:4ddb:c8ed]:35871</code></li>
</ul>
<p>And here are some that use various shorthand notations or alternate representations:</p>
<ul>
<li><code>localhost:8080</code></li>
<li><code>::1:8080</code></li>
<li><code>:22</code></li>
<li><code>*:22</code></li>
<li><code>:::22</code></li>
<li><code>example.com:35871</code></li>
<li><code>127.0.0.1:3000-4000</code></li>
</ul>
<p>Any given server application <em><strong>should</strong></em> write the address it decided to listen on to its log immediately after it starts up. We <em><strong>should</strong></em> be able to connect to the application by connecting to that address. But how would one connect to <code>0.0.0.0:22</code> or <code>:22</code>? What do those addresses even mean?</p>
<h3 id="aboutspecialipv4andipv6addresses">About &quot;Special&quot; IPv4 and IPv6 Addresses</h3>
<p>You may already be aware of some &quot;special&quot; IP addresses, for example</p>
<p><strong><code>127.0.0.1</code></strong> which is called the <code>loopback</code> address, the address of the <code>localhost</code> domain.</p>
<p>OR</p>
<p><strong><code>192.168.0.1</code></strong> which might be the address of your home router.</p>
<p>If you want to learn more about these, I recommend the <a href="https://en.wikipedia.org/wiki/IPv4">Wikepedia article on IPv4</a>.</p>
<p>There's another special IPv4 address which (as far as I know) is only used by listening addresses:</p>
<p><strong><code>0.0.0.0</code></strong> technically means something like &quot;<code>null</code>&quot; or &quot;no address&quot;, but when used as a listening address, it's interpreted as <strong>&quot;Listen on ALL Addresses&quot;</strong>.</p>
<p>Don't ask me why, but IPv6 addresses are written with colons <code>:</code> in between the numbers instead of periods <code>.</code> (in my opinion this was a huge mistake 😫)</p>
<p>So this special address for IPv6 would be written as <code>0:0:0:0:0:0:0:0</code>, but you never actually see it written that way. IPv6 also introduced a special shorthand where repeated <code>0:</code>s are abbreviated as <code>::</code>. So, confusingly, the IPv6 &quot;Listen on ALL Addresses&quot; IP address is written as <code>::</code>.</p>
<p>To make matters worse, when an IPv6 address is represented as a part of a network address, we have to be able to tell the difference between the IPv6 colons and the <code>&lt;host&gt;:&lt;port&gt;</code> colon. So those network addresses are written like <code>[::]:22</code> or <code>[::1]:8080</code>.</p>
<p>In case you were wondering, <code>::1</code> (expanded, it would be <code>0:0:0:0:0:0:0:1</code>) is the IPv6 &quot;loopback address&quot; like IPv4's <code>127.0.0.1</code>, the address that loops back to the same computer who is dailing.</p>
<h2 id="rantaserverapplicationisgivingusthesilenttreatment">Rant: A Server Application is Giving Us The Silent Treatment...</h2>
<p>Here's a concrete example of just how terrible the user interface and usability of server software tends to be. The following is based on a true story...</p>
<hr>
<p>The venerable web server <a href="https://nginx.org/en/">nginx</a> (pronounced &quot;engine-x&quot; 🤦) doesn't log anything at all when we run it:</p>
<pre><code>$ sudo /usr/sbin/nginx
$ 
</code></pre>
<p>In fact, it exits immediately (gives me my command prompt back) as if it refused to run or perhaps crashed. So what gives? Lets check which <a href="https://sequentialread.com/what-is-a-process/#exitcode">process exit code</a> it outputted. (For process exit codes, <code>0</code> (Zero) means success, and any positive number means failure).</p>
<pre><code>$ sudo /usr/sbin/nginx
$ echo $?
0
</code></pre>
<p>It returned <code>0</code>, so it says that it worked, what gives? We get a clue if we try running it again:</p>
<pre><code>$ sudo /usr/sbin/nginx
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
</code></pre>
<p>This time it's complaining that it can't listen on port 80 because some other process is already listening on port 80...  But it's not referring to it as a &quot;port&quot;, it's referring to it as an &quot;address&quot;. We've spotted our first listening addresses, <code>0.0.0.0:80</code> and <code>[::]:80</code>, in the wild!  In this case, it looks like nginx is trying and failing to listen on port 80 on all IPv4 and IPv6 addresses.</p>
<p>It turns out that by default, nginx runs in &quot;daemon&quot; mode. And by default, it doesn't log anything while it does this. That means when I ran <code>sudo /usr/sbin/nginx</code> and it appeared to have cancelled or failed, what actually happened was the nginx server spawned in a separate process. I never saw any output because my shell was not attached to that other process. That's also why it complained about <code>Address already in use</code> the second time I ran it: it was trying to start up a second copy of the server while the first one was still running.</p>
<p>After a bit of looking things up online, I returned with the following command line options for nginx:</p>
<pre><code>$ sudo /usr/sbin/nginx -g 'daemon off; error_log stderr info;'
2022/01/30 21:54:21 [notice] 431278#431278: using the &quot;epoll&quot; event method
2022/01/30 21:54:21 [notice] 431278#431278: nginx/1.18.0 (Ubuntu)
2022/01/30 21:54:21 [notice] 431278#431278: OS: Linux 5.4.0-96-generic
2022/01/30 21:54:21 [notice] 431278#431278: getrlimit(RLIMIT_NOFILE): 1024:1048576
2022/01/30 21:54:21 [notice] 431278#431278: start worker processes
2022/01/30 21:54:21 [notice] 431278#431278: start worker process 431279
2022/01/30 21:54:21 [notice] 431278#431278: start worker process 431280
2022/01/30 21:54:21 [notice] 431278#431278: start worker process 431281
...
</code></pre>
<p>The <code>-g</code> flag stands for &quot;<code>-g</code>lobal directives&quot;, it allows me to add a couple extra configurations on top of the existing configuration file.</p>
<p>The <code>daemon off;</code> part tells it to run like a normal process instead of in &quot;daemon mode&quot; (That is, run in my shell, output its log to the shell so we can see what it's doing).</p>
<p>Finally, <code>error_log stderr info;</code> tells it to output its error log to the <a href="https://sequentialread.com/what-is-a-process/#standarderrorstream">process' <code>stderr</code> stream</a>, which will be displayed in the shell.</p>
<p>However, it still doesn't log the port(s) it's listening on 😒</p>
<p>IMO, this should be embarrassing for the developers/maintainers of nginx; that someone trying to use thier software couldn't even tell if it was running or not. Thier software did not report anything at all when it was executed. Even after traversing the first usability gap, the user still doesn't know whether or not its listening on some port, and if so, which port it's listening on.</p>
<p>But they probably aren't embarrassed at all; they probably view this whole kerfluffle as &quot;the user's fault&quot;  for being ignorant; for not already knowing how nginx works or how to answer thier questions on their own.  In my opinion, that's a toxic take. The whole point is that we are <em>trying to learn</em> about nginx and server software in general. It is nginx that is at fault here for making this such a painful experience; it doesn't have to be this way IMO. There is plenty of other server oriented software which manages to keep its command line interface approachable.</p>
<p>It turns out that nginx DOES in fact log the ports / addresses it's listening on, however, it logs them at the <code>debug</code> log level, so they are hidden by default unless we specify that we want to see the log at the <code>debug</code> log level instead of the <code>info</code> log level. <code>error_log stderr debug</code> instead of <code>error_log stderr info</code>:</p>
<pre><code>forest@thingpad:~$ sudo /usr/sbin/nginx -g 'daemon off; error_log stderr debug;'
2022/07/26 12:08:21 [debug] 19486#19486: bind() 0.0.0.0:80 #6 
2022/07/26 12:08:21 [debug] 19486#19486: bind() [::]:80 #7 
2022/07/26 12:08:21 [notice] 19486#19486: using the &quot;epoll&quot; event method
2022/07/26 12:08:21 [debug] 19486#19486: counter: 00007F4CAA115080, 1
2022/07/26 12:08:21 [notice] 19486#19486: nginx/1.18.0 (Ubuntu)
2022/07/26 12:08:21 [notice] 19486#19486: OS: Linux 5.4.0-122-generic
2022/07/26 12:08:21 [notice] 19486#19486: getrlimit(RLIMIT_NOFILE): 1024:1048576
2022/07/26 12:08:21 [debug] 19486#19486: write: 8, 00007FFFEC8AE470, 6, 0
2022/07/26 12:08:21 [debug] 19486#19486: setproctitle: &quot;nginx: master process /usr/sbin/nginx -g daemon off; error_log stderr debug;&quot;
2022/07/26 12:08:21 [notice] 19486#19486: start worker processes
</code></pre>
<p>There are those two listening addresses, <code>0.0.0.0:80</code> and <code>[::]:80</code>, again! Last time we saw them in an error message, but this time we see them in a debug log. Finally, nginx is running properly and it's also at least sort of letting us know what it's doing.</p>
<p>In my opinion, an application with a proper user interface wouldn't run in daemon mode by default, and it would log something like this when it starts up.</p>
<pre><code>nginx is starting up!
...
I am now listening publicly on 0.0.0.0:80 and [::]:80
You can connect to me at http://localhost/
</code></pre>
<p>Or if it <em><strong>was</strong></em> going to run in daemon mode by default, it would log something like this when run:</p>
<pre><code>I am now spawning the nginx daemon process. 
...
Startup succeeded, nginx daemon is now running!
If you would like to see the output of that process, please check the log file /var/log/nginx.log
</code></pre>
<br>
<hr>
<h2 id="flippingthetable">Flipping the Table (╯°□°)╯︵ ┻━┻</h2>
<p>Luckily, we don't have to depend on applications like nginx which may be &quot;unreliable narrators&quot; at times.</p>
<p>We can always just ask the operating system what network addresses are listening and <a href="https://sequentialread.com/what-is-a-process/#currentlyopensocketsnetworkconnectionsorlisteningservers">which application process those &quot;listening sockets&quot; are associated with</a>.</p>
<p>Use:</p>
<ul>
<li><strong>Linux</strong>  <code>sudo ss -plunt</code> or <code>sudo netstat -plunt</code></li>
<li><strong>MacOS</strong> <code>lsof -i -P -n | grep LISTEN</code></li>
<li><strong>Windows</strong> <code>netstat -aon</code> on Windows.</li>
</ul>
<blockquote>
<p><strong>ℹ️ NOTE:</strong> Those flags stand for <code>-t</code>cp, <code>-u</code>dp, <code>-l</code>istening, inlcude <code>-p</code>rogram name, and display port number as a <code>-n</code>umber instead of listing the associated protocol. <code>sudo</code> is included because the <code>-p</code>rogram name flag only works when <code>netstat</code>/<code>ss</code> are running as root)</p>
</blockquote>
<p>On my computer, the <code>netstat</code> and <code>ss</code> commands outputted too much text to copy and paste here, so I've edited the output down a bit:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><pre class="scroll">Netid  State    Recv-Q  Send-Q  Local Address:Port   Peer Address:Port  Process
...
tcp    LISTEN   0       4096        127.0.0.1:35625       0.0.0.0:*     containerd
tcp    LISTEN   0       50            0.0.0.0:139         0.0.0.0:*     smbd
tcp    LISTEN   0       4096          0.0.0.0:111         0.0.0.0:*     rpcbind
tcp    LISTEN   0       511           0.0.0.0:80          0.0.0.0:*     nginx
tcp    LISTEN   0       32      192.168.122.1:53          0.0.0.0:*     dnsmasq
...
tcp    LISTEN   0       250              [::]:3142           [::]:*     apt-cacher-ng
tcp    LISTEN   0       50               [::]:139            [::]:*     smbd
tcp    LISTEN   0       4096             [::]:111            [::]:*     rpcbind
tcp    LISTEN   0       511              [::]:80             [::]:*     nginx
tcp    LISTEN   0       5               [::1]:631            [::]:*     cupsd
...  
</pre><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Here we can obtain the same information:  There are two listening sockets owned by the <code>nginx</code> process, and they have the <code>Local Address:Port</code>s <code>0.0.0.0:80</code> and <code>[::]:80</code>.  In otherwords, nginx is listening on port 80 on all IPv4 and IPv6 addresses.</p>
<h2 id="howlisteningaddresseswork">How Listening Addresses Work</h2>
<p>The <strong><code>0.0.0.0:80</code></strong> and <strong><code>[::]:80</code></strong> example above demonstrates the simplest case for a listening address.</p>
<p>With those two listening sockets, if anyone dials that computer from <em><strong>anywhere</strong></em>, literally anywhere, they will be able to connect on that port (<code>:80</code>). Of course, there are caveats. The listening computer may not be routable from everywhere. Or a firewall might be blocking the traffic. The firewall could be anywhere; on the dialing computer (client), the listening computer (server) or some equipment in-between.</p>
<p>At any rate, when they connect, the listening computer's operating system will notify the <a href="https://sequentialread.com/what-is-a-process/">process</a> attached to the listening socket, thus triggering the connection handler code to run inside the server application process that created the listening socket.</p>
<p>A listening address like <code>127.0.0.1:80</code> or <code>[::1]:80</code> is more complicated.  It's a real address, so when a process asks the operating system to listen on this address, the OS doesn't mess around: it listens exactly only on that port on that address. This can quickly become a problem, in fact I would say this is probably the most common source of pain that humans encounter when it comes to listening addresses.</p>
<p>The address can only be dialed via the network that it is a part of. (See the <a href="https://en.wikipedia.org/wiki/IPv4#Special-use_addresses">List of IPv4 Networks</a> that are part of the IPv4 specification).</p>
<p><code>127.0.0.1</code> is in the <code>127.0.0.0/8</code> loopback network range, so in order to be able to dial it, you have to be on the loopback network.  And by definition, the loopback network only has one computer on it: The computer who dialed!  This network is used exclusively for a computer to dial itself.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><blockquote>

<script>
(function(window, undefined){
  window.showHideSpoiler = function(spoilerId) {
    var header = document.getElementById('spoiler'+spoilerId);
    var body = document.getElementById('spoiler'+spoilerId+'content');
    var isVisible = body.style.display == 'block';
    var trimmed = header.textContent.trim();
    if(!isVisible) {
      header.textContent = trimmed.substring(0, trimmed.length-6) + '- Hide';
      body.style.display = 'block';
    } else {
      header.textContent = trimmed.substring(0, trimmed.length-6) + '+ Show';
      body.style.display = 'none';
    }
    if(header.parentElement.classList.contains('spoiler')) {
      header.parentElement.className = isVisible ? 'spoiler closed' : 'spoiler open';
    }
  };
})(window)
</script>
<div class="spoiler closed">
<a id="spoiler1" href="#" style="color: #808080; text-decoration: none; font-family: monospace; " onclick="javascript:showHideSpoiler(1); return false">
   💀 If you are morbidly curious what the heck the /8 in 127.0.0.0/8 is, + Show
</a><br>

<div id="spoiler1content" style="display: none;">
<br>
<p>It's called a "netmask". Overall, the whole <code>127.0.0.0/8</code> format is called <a href="https://www.ipaddressguide.com/cidr">CIDR Block notation</a>.  </p>
<p>
The netmask controls how many bits of the address are used to represent the address of the network itself, and how many bits are used to represent the different addresses inside the network. Decreasing the CIDR netmask number by one doubles the number of addresses inside the network, and increasing it by one cuts the number of addresses inside the network in half.  There can only be one <code>/0</code> network, <code>0.0.0.0/0</code> which contains every possible IPv4 address. And there are as many <code>/32</code> networks as there are possible addresses, with one address per network. 
</p>
<p>Might seem like a whole heck of a lot of addresses to allocate for the sole purpose of a computer dialing itself, but hey, at least if we ever see an address starting with <code>127</code>, we know its for sure a local address.
</p>

<p>I don't know how many <code>127.0.0.0/8</code> addresses I've seen besides <code>127.0.0.1</code>. Maybe one? Two? If you use these addresses for something and you've been able to make use of the massive number of them that are avaliable, leave a comment. I would love to hear about it.</p>
</div>

</div>
</blockquote><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>So if your application is listening on  <code>127.0.0.1:80</code> and <code>[::1]:80</code>, then it will only accept connections coming from the same computer.</p>
<p>The same effect can be achieved for any other network by listening on whatever address your computer has on that network. For example, my home LAN's network is <code>192.168.0.0/24</code>  and my server's address on that network is <code>192.168.0.24</code>:</p>
<pre><code>root@odroidxu4:~# ip addr show
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
4: enx001e0636dda6: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:1e:06:36:dd:a6 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.24/24 brd 192.168.0.255 scope global dynamic noprefixroute enx001e0636dda6
       valid_lft 3117sec preferred_lft 3117sec
    inet6 fe80::21a3:f80e:be6a:e049/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

</code></pre>
<p>So if I told an application running on that server to listen on <code>192.168.0.24:80</code>, it would only be dial-able by computers on my home LAN.</p>
<p>Typically this behaviour is only utilized by choosing to listen on either <code>0.0.0.0</code> and <code>::</code> (all addresses) or listen on <code>127.0.0.1</code> and <code>::1</code> (listen for local connections only).</p>
<p>But it's important to choose the right one. You may wish to listen for <em>local connections only</em> for security reasons, or you may wish to listen on all addresses because you have something you want to share with the world.</p>
<p>So if your server software isn't responding, you can use <code>ss</code> or <code>netstat</code> to figure out if it's listening at all and if so, what listening addresses it's using. It's possible that it may be listening on the wrong address:</p>
<ul>
<li>Listening only on the loopback address while someone from outside is trying to connect
<ul>
<li>This is a common problem with docker containers. Processes running inside docker containers shouldn't listen on the loopback address; if they do they will only be accessible from inside that container.</li>
</ul>
</li>
<li>Listening only on IPv6 while someone is trying to connect via IPv4</li>
<li>Listening only on IPv4 while someone is trying to connect via IPv6</li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="dualstacksockets">Dual-Stack Sockets</h2>
<p>I just said that sometimes the application might be</p>
<blockquote>
<p>Listening only on IPv6 while someone is trying to connect via IPv4</p>
</blockquote>
<p>It's worth noting that this is <em><strong>supposed</strong></em> to never happen.</p>
<p>Linux specifically uses something called &quot;dual stack sockets&quot; by default, controlled by the <a href="https://sysctl-explorer.net/net/ipv6/bindv6only/">net.ipv6.bindv6only</a> <a href="https://www.man7.org/linux/man-pages/man8/sysctl.8.html">sysctl</a>.  This means that if you create a listener for IPv6 connections on all addresses, it should &quot;automagically&quot; listen for IPv4 connections on all addresses as well. This behaviour appears to be <a href="https://github.com/nodejs/node/issues/9390#issuecomment-278001837">fairly well-standardized</a> across operating systems.</p>
<p>In my previous nginx example, nginx is actually going out of its way to avoid using a dual-stack socket; in its default configuration</p>
<pre><code># Default server configuration
#
server {
	listen 80 default_server;
	listen [::]:80 default_server;

</code></pre>
<p>it <a href="https://serverfault.com/a/638370">very clearly specifies to the OS</a> that it wants to create one socket for IPv4 and one socket for IPv6.</p>
<p>However, if I write my own program in Golang or Node.js to create a listening server:</p>
<p>📄 <code>main.go</code></p>
<pre><code>package main

import (
	&quot;net/http&quot;
)

func main() {
	http.ListenAndServe(&quot;:8080&quot;, nil)
}
</code></pre>
<p>📄 <code>index.js</code></p>
<pre><code>&quot;use strict&quot;;

const express = require(&quot;express&quot;);

const app = express();

app.listen(8080);
</code></pre>
<p>Then I'll only see one listening socket, even though I should be able to dial the app via both IPv4 and IPv6:</p>
<pre><code>forest@thingpad:~$ sudo ss -tlpn 
State     Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process  
...
LISTEN    0       511     *:8080              *:*                node
...

forest@thingpad:~$ curl 192.168.0.46:8080
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;...blahblahhtmlblah...

forest@thingpad:~$ curl '[::1]:8080'
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;...blahblahhtmlblah..
</code></pre>
<p>It's worth noting that <code>netstat</code> on linux won't provide any indication that this is happening, it will display the dual-stack listening socket as if it was a normal IPv6 listening socket. However, the newer program <code>ss</code> will differentiate between the two. Also, <code>ss</code> uses the unambiguous IPv6 address format <code>[::]:8080</code> over the messier <code>:::8080</code>. So <code>ss</code> is definitely preferred.</p>
<p>Compare the <code>netstat</code> output:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><pre class="scroll">
forest@thingpad:~$ sudo netstat -tlpn 
Active Internet connections (only servers)
Proto Recv-Q Send-Q  Local Address  Foreign Address  State    PID/Program name    
...            
tcp        0      0  0.0.0.0:80     0.0.0.0:*        LISTEN   1567/nginx               
tcp6       0      0  :::80          :::*             LISTEN   1567/nginx      
...
tcp6       0      0  :::8080        :::*             LISTEN   35378/node    
...
</pre>

<p>Versus the <code>ss</code> output:</p>

<pre class="scroll">
forest@thingpad:~$ sudo ss -tlpn 
State     Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process                                                                                                                                               
...
LISTEN    0       511     0.0.0.0:80          0.0.0.0:*          nginx       
LISTEN    0       511     [::]:80             [::]:*             nginx
...
LISTEN    0       511     *:8080              *:*                node
...
</pre>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>It's rare, but sometimes something <em>will</em> go wrong with a dual-stack socket and we'll wish that we could split it up like how nginx does. I've usually encountered this when using uncommon software or in a funky operating system environment. Some examples:</p>
<ul>
<li>I was unable to get <a href="https://gpsd.gitlab.io/gpsd/index.html">gpsd</a> to listen on both IPv4 and IPv6 at the same time on my Odroid single board computer.
<ul>
<li>To solve this, I ended up configuring it to listen on IPv4 only, and I wrote a simple UDP proxy server to listen on IPv6 and forward packets to it.</li>
</ul>
</li>
<li>Once upon a time I encountered an issue where <a href="https://caddyserver.com/">Caddy Server</a> appeared to have dual-stack-related listening issues on an <a href="https://www.alpinelinux.org/">alpine linux</a> <a href="https://capsul.org/">capsul</a>.</li>
<li>A fellow <a href="https://cyberia.club/">cyberian</a> reported issues with dual-stack listening sockets on an Ubuntu <a href="https://docs.microsoft.com/en-us/windows/wsl/about">WSL</a> virtual machine. The issue only occurred when dialing the <a href="https://medium.com/@TimvanBaarsen/how-to-connect-to-the-docker-host-from-inside-a-docker-container-112b4c71bc66">host.docker.internal</a> from inside a docker container.
<ul>
<li>To solve this, the cyberian switched the server application from listening on <code>[::]:1337</code> to listening on <code>0.0.0.0:1337</code>.</li>
</ul>
</li>
</ul>
<h2 id="theorymeetpracticality">Theory, Meet Practicality</h2>
<p>When running a server application that opens a listening port, you might not even be able to specify which address it listens on. Or even if you <em>could</em> specify the address, you may not be able to specify multiple addresses or specify the details of how the application asks the operating system to listen. (For example, what value it assigns to a socket option flag like <a href="https://www.man7.org/linux/man-pages/man7/ipv6.7.html">IPV6_V6ONLY</a>)</p>
<p>In fact, operating systems or environments can be slightly different in how they handle these listen <a href="https://sequentialread.com/what-is-a-process/#overviewofprocesses">syscalls</a>. Programing language standard libraries / runtimes differ in how they make the syscalls to <em>request</em> listening sockets as well.  So even as an application developer, <a href="https://github.com/golang/go/issues/9334">you may not always have the luxury of specifying exactly how your application listens for connections</a>.</p>
<p>However, despite any limitations that may exist, understanding some of these details should help your server software efforts bear fruit and may help you avoid frustration / disapointment. By understanding the inner workings of any limitation you may run up against, you open up the possibility of side-stepping it with a quick hack or even directly addressing/eliminating it. Happy hosting!</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Homebrew Unsweetened Grapefruit Energy Drink]]></title><description><![CDATA[No, I'm not changing this into a food blog, but I have been taking a break from working on my software projects for a couple months now while I'm trying to take care of my health..]]></description><link>https://sequentialread.com/fancy-homebrew-grapefruit-energy-drink/</link><guid isPermaLink="false">6260e491d6905b0001944bef</guid><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Sun, 24 Apr 2022 20:22:31 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2022/04/grapefruit7.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2022/04/grapefruit7.jpg" alt="Homebrew Unsweetened Grapefruit Energy Drink"><p><em>No, I'm not changing this into a food blog, but I have been taking a break from working on my software projects for a couple months now while I'm trying to take care of my health.</em></p>
<p><em>I had fun with this little side project and it's easy for me to use this blog as a format for sharing it.</em></p>
<p>When I first tried coffee and started drinking it regularly in my early 20s, I loved the drug but hated the taste.   Back then I  bought a bottle of caffeine powder online and started simply eating the powder instead of making coffee.  It still tasted bad, but &quot;in a pinch&quot; (quite literally) it'd get the job done.  If you've never tasted pure caffeine, its very bitter, kind of similar to the taste of the white part on the inside of a grapefruit rind.</p>
<p>While I was in the dormitory crudely shoveling white powder into my mouth, I was thinking about how I could make a better energy drink.  Back then I couldn't afford energy drinks, but even if I could, I wouldn't have wanted to drink em all the time, they're usually syrupy sweet, even worse than a typical soda. I was never liked artificial sweeteners either.  I wanted something other than a <code>40 gram</code> mound of sugar to mask the bitterness.</p>
<p>I know that salt can help cut down bitter flavors in food, and I also figured that if I went for a flavor that's &quot;supposed&quot; to be bitter, it would jive better with the caffeine.  Back then I guess all of this was firmly in daydream territory, I had other stuff going on and no time for caffeine adventures in the kitchen.</p>
<p>Fast forward 8 years:  Since then, a lot has changed:</p>
<ul>
<li>I got over my aversion to coffee and now I drink it regularly</li>
<li>Some poor kid decided to shovel a whole spoonful of caffeine powder into his mouth and ended up with a heart attack, so the United States Food and Drug Administration banned the retail sale of anhydrous caffeine powder</li>
<li>I was given homebrewing equipment as a gift from a stranger, and</li>
<li>Before I ever got around to using the homebrewing equipment, I realized that I needed to quit drinking alcohol 😬</li>
<li>I guess La Croix somehow became cool, everyone started drinking White Claw, and the craze even expanded to energy drinks</li>
</ul>
<hr>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->
<img style="float:right; max-width:50vw;" alt="Homebrew Unsweetened Grapefruit Energy Drink" src="https://sequentialread.com/content/images/2022/04/true-north.jpg">
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p>So what was I to do?</p>
<p>I never forgot about my dream of a tasty but unsweetened or semi-sweet energy drink.  Like most of my daydreamed creations, someone else beat me to it. This little number on the right is produced by everyone's favorite energy drink monopolist Monster Beverage. I drank a couple of these recently and I can say I think my theory about the bitter flavor is correct. I tried thier cucumber lime flavor as well as the grapefruit, and you can definitely taste the caffeine in the cucumber one while its almost unnoticeable in the grapefruit.</p>
<p>That said, energy drinks are ridiculously expensive for seemingly no reason ( I mentioned they're ruled by a monopolist, right? ) and this one's no exception. Oh, and it's a bit weak for my taste at 160mg. Over the course of a day, I want the full caffeine experience, and for me that weighs in at 300mg or more. (About 16oz of coffee)</p>
<p>This was the straw that broke the camel's back; I had to finally try to concoct my own, and fate had granted me the tools to do so:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sequentialread.com/content/images/2022/04/bottles-1.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"><figcaption>I must be getting old or something because I was absolutely tickled pink by these "pic-a-pop" bottles that we're given by an old man at a yard sale. They're just so cute. I 'm not sure but I suspect they may date back to an ancient era when people would actually return the bottle to the manufacturer &amp; it could be refilled.</figcaption></figure><!--kg-card-begin: markdown--><h2 id="thecaffeinequestion">The Caffeine Question</h2>
<p>When I was discussing this at first, the question came up,</p>
<blockquote>
<p>will caffeine kill or inhibit the yeast?</p>
</blockquote>
<p>I did some research on this, and came across a paper on this exact subject:</p>
<h4 id="investigatingthecaffeineeffectsintheyeastsaccharomycescerevisiae"><a href="https://onlinelibrary.wiley.com/doi/full/10.1111/j.1365-2958.2006.05300.x"><em>Investigating the caffeine effects in the yeast Saccharomyces cerevisiae...</em></a></h4>
<p>A small excerpt:</p>
<blockquote>
<p>...the IC50 was taken as the value that causes a <code>50% reduction</code> of the maximal growth rate...<br>
[the IC50 was] <code>8 mM</code> (<code>1.55 mg ml−1</code>) for caffeine</p>
</blockquote>
<p>I was a little bit confused by these units, <code>8 mM</code> here means <code>8 millimolar concentration</code> in other words, <code>0.008 moles of caffeine per liter</code>.  <code>1.55 mg ml−1</code> appears to be just a fancy way of saying <code>1.55mg per ml</code> or <code>1.5 grams per liter</code>. If you do the math, it checks out: <code>0.008 moles</code> of caffeine weighs about <code>1.5 grams</code>.</p>
<p>So in other words, in order to slow the yeast by about 50%, the caffeine concentration has to be <code>1.5 grams per liter</code>. But coffee is only about <code>0.6 grams per liter</code>, and that's the concentration I was planning on targeting, so I figured it should be fine.</p>
<h2 id="decidingonaprocess">Deciding on a Process</h2>
<p>At first, I set out to learn a little bit about homebrewing. I knew that it's possible to brew a beer with a negligible alcohol content, and I knew that one can brew low-alchohol sweet and fizzy ginger beer at home without any fancy equipment, but I didn't know how to approach the project.</p>
<p>I wanted to come up with a process which would yield a relatively shelf-stable product that's carbonated and ready to drink without having to be stored in the fridge.</p>
<p>The wild yeast &quot;probiotic active cultures&quot; ginger-beer-type process seemed like a non-starter, because it requires regular care and feeding and has to stay in the fridge.</p>
<p>I also knew that if you bottle an actively fermenting liquid like beer, and it has a lot of sugar in it, the yeast will create so much pressure inside the bottle that it'll explode on the shelf. Or even worse, as soon as you try to pick it up and open it!</p>
<p>At first, I thought my goal would be to figure out how to calibrate my mixture somehow so that the yeast would stop growing at the right time in order to prevent the bottle from exploding. But it turns out that's simply not a thing; it can't be done. Yeast love to eat, and there's no trivial way to stop them as long as more sugar is available in the bottle:</p>
<ul>
<li><strong>Non-Fermentable Sugars</strong> exist, but these are those same weird sugars and artificial sweeteners I was trying to avoid in the first place.</li>
<li><strong>Pasteurization</strong> is an option, but it takes a lot of work and involves risk:
<ul>
<li>Heating the bottles to <code>180°F</code> after they have already been carbonated and pressurized by the yeast may cause them to explode if you aren't careful</li>
<li>If you don't manage to kill all the yeasts they will definitely explode the bottle later</li>
<li>Heating the bottles will reduce the carbonation overall since all the CO2 will come out of solution and hang out in the air pocket at the top of the bottle until it cools down</li>
</ul>
</li>
</ul>
<p>In the end, I decided to eschew both of these options and choose simplicity: I would only include enough sugar to carbonate the drink, no more, no less. The yeast should eat all the sugar, and in the end, the drink should be essentially zero-sugar.  From various eyewitness accounts and &quot;priming sugar&quot; calculators online, I estimated this amount of sugar at approximately <code>160 grams</code> for a <code>5 gallon</code> batch.</p>
<p>Essentially the process would be exactly the same as the process for making beer, except my &quot;wort&quot; (fermentable liquid) has much less sugar in it, with caffeine taking its place, and I'm skipping the bulk fermentation process and going directly to bottling shortly after &quot;pitching&quot; the yeast (adding the yeast to the mixture after it cools down).</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sequentialread.com/content/images/2022/04/bottles2.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"><figcaption>Just like when brewing beer or canning food, a proper sterile process is essential for success. Here the bottles are being washed and sanitized for 10 minutes in PBW cleaning solution (a strong base like lye) and Saniclean sanitizing solution (phosphoric acid)</figcaption></figure><!--kg-card-begin: markdown--><h2 id="comingupwitharecipe">Coming up with a Recipe</h2>
<h4 id="akathefullgrapefruitexperience">aka &quot;The Full Grapefruit Experience&quot;</h4>
<p>I created the following diagram to explain my overall thesis here. Each ingredient is located around the area of the grapefruit that it's supposed to &quot;taste like&quot;  or &quot;represent&quot;  when it's in the finished drink.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/04/gf.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"></figure><!--kg-card-begin: markdown--><p>I'm no food scientist or chemist, and to be honest I didn't try very hard in this part. The core idea was to take a grapefruit apart, swap the pith (white part of the rind) out for caffeine, and put it back together as a drink.</p>
<ul>
<li>The oil from the rind is important because that's what's going to make it smell like grapefruit. Smell is a huge part of taste.</li>
<li>It's going to be bitter like the white pith no matter what we do because of the caffeine, so might as well embrace that</li>
<li>I can't make the drink sweet like the inside of the grapefruit, but I can at least try to make it sour and tart.</li>
<li>I think some gatorade-style salts (electrolytes) might help balance the flavor.</li>
</ul>
<p>At the same time, I tried to include ingredients that have health benefits while also complimenting what I'm trying to do with the flavor.</p>
<p>In the end, my recipe for twelve <code>750ml</code> bottles looked like this:</p>
<ul>
<li><code>9 liters</code> of water</li>
<li><code>1 and a half cups</code> of grapefruit juice
<ul>
<li>contains about <code>37 grams</code> of sugar for the carbonation process</li>
</ul>
</li>
<li><code>50 grams</code> of grapefruit &quot;oleo saccharum&quot; (sugar syrup citrus rind oil extract)
<ul>
<li>extracted from the rinds of two large grapefruits</li>
<li>contains about <code>45 grams</code> of sugar for the carbonation process</li>
</ul>
</li>
<li>total <code>82 grams</code> of sugars
<ul>
<li>It's important to have no more than about <code>8.5 grams</code> of sugar per <code>liter</code> of liquid (about <code>160 grams</code> per <code>5 gallons</code>), otherwise the bottles may overcarbonate and explode</li>
</ul>
</li>
<li>Half a packet of champagne yeast</li>
<li><code>6 grams</code> of caffeine
<ul>
<li><code>500mg</code> per bottle, <code>250mg</code> per serving</li>
<li>extracted from the contents of <code>30</code> <code>200mg</code> caffeine gel caps</li>
</ul>
</li>
<li><code>15 grams</code> of calcium ascorbate
<ul>
<li><code>1000mg</code> of vitamin c per bottle, <code>500mg</code> per serving, similar to the &quot;Emergen-C&quot; drink mix</li>
<li>About <code>25% daily value</code> of calcium per bottle</li>
</ul>
</li>
<li><code>38 grams</code> of potassium citrate
<ul>
<li><code>1000mg</code> of potassium per bottle (~<code>30% daily value</code> per bottle)</li>
<li>potassium citrate has an interesting tart taste</li>
<li>My partner had a potassium deficiency recently</li>
</ul>
</li>
<li>I was going to add some plain citric acid or lemon juice for a more sour/acidic flavor, but I forgot.</li>
<li>I wanted to add zinc and magnesium salts for a more complete mix of electrolytes but I got lazy and left them out.
<ul>
<li>I think a single $2 lemon flavor magnesium-based laxative drink from walgreens would have been perfect here.</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sequentialread.com/content/images/2022/04/grapefruit2.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"><figcaption>I peeled the outside of the grapefruit rind with a vegetable peeler, mixed it with sugar, and beat / mashed the mixture with a fork. Since sugar is hygroscopic, it will pull the oils out of the grapefruit rind and dissolve into a syrup if you let it sit for about 8 hours</figcaption></figure><!--kg-card-begin: markdown--><br>
Like I mentioned before, retail sales of caffeine powder have been banned, so I had to use the next best thing: Caffeine gel cap pills.
<p>These pills are cut with rice flour, so to separate the caffeine from the rice flour, I emptied them into a bowl and then poured hot water over it. The caffeine will dissolve and the rice flour will sink to the bottom. Most of the liquid can be decanted off the top, and the rest can be grabbed by pouring the slurry thru a coffee filter.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/04/grapefruit3.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"></figure><!--kg-card-begin: markdown--><p>In order to try to extract the most out of the grapefruit peel, I threw the left over syrupy peel into a blender with some hot/warm water, then poured the results through a coffee filter. <em>(Plastic blender users: Be careful! Too hot of water can crack your blender carafe!)</em></p>
<p>The following video shows my brew after adding all the dry ingredients and pouring in the blended-grapefruit-peel-water. Some solids had formed overnight in the water based peel extract, but they dissolved later.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><div style="display: flex; justify-content: center; background-color: #aaa;">
  <video autobuffer controls preload="auto" style="max-height: 90vh;" width="100%">
    <source src="https://picopublish.sequentialread.com/files/brew.mp4" type="video/mp4">
  <p>Your browser doesn't support HTML5 video. Here is
     a <a href="https://picopublish.sequentialread.com/files/brew.mp4">link to the video</a> instead.</p>
  </video>
</div>
<br>
<br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>For the grapefruit juice, I simply took the red grapefruit flesh out and chucked it in the blender:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/04/grapefruit4.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"></figure><!--kg-card-begin: markdown--><p>After adding the grapefruit juice and the grapefruit peel extract syrup, I heated the mixture to <code>180°F</code> to kill off any bacteria or yeasts in there.</p>
<p>Once I felt confident it was sterilized, I covered the pot and moved the it to the sink so it could chill faster in a water bath. I wanted it to cool down below <code>80°F</code> before I added the yeast.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/04/grapefruit5-1.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"></figure><!--kg-card-begin: markdown--><p>Finally, after the yeast is thoroughly mixed in, it's time to bottle!</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/04/grapefruit6.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"></figure><!--kg-card-begin: markdown--><p>These bottles are currently fermenting and will need some time before they are ready ⁠— about 3-7 days, assuming everything goes to plan.</p>
<p>...</p>
<p>...</p>
<p>...</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/04/time-1.gif" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"></figure><!--kg-card-begin: markdown--><h2 id="72hourslater">72 hours later</h2>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sequentialread.com/content/images/2022/04/72later.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"><figcaption>Unfortunately it looks like a considerable amount of solids from the grapefruit peel / grapefruit juice floated to the top during the fermentation process. This was fixed in future batches.</figcaption></figure><!--kg-card-begin: html--><br>
<p>
After 72 hours I transferred one of the bottles to the fridge, then the next day I cracked it open to try a glass:
</p>
<br>
<div style="display: flex; justify-content: center; background-color: #aaa;">
  <video autobuffer controls preload="auto" style="max-height: 90vh;" width="100%">

<source src="https://picopublish.sequentialread.com/files/try-a-glass1.webm" type="video/webm">
    <source src="https://picopublish.sequentialread.com/files/try-a-glass1.mp4" type="video/mp4">
  <p>Your browser doesn't support HTML5 video. Here is
     a <a href="https://picopublish.sequentialread.com/files/try-a-glass1.mp4">link to the video</a> instead.</p>
  </video>
</div>
<br>
<br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>I have to say, it tasted pretty bad on its own at first. It doesn't taste like it spoiled or something went wrong with the fermentation, it's just... The flavor is way off, especially when compared to a commercial product like the True North drinks.</p>
<p>The fermentation/carbonation seemed to work, although I think it's not quite done yet after 3 days. The pressure was pretty low when I opened the bottle, so I expect it will continue fermenting and becoming more and more bubbly over the coming days.</p>
<p>I will say, however, I succeeded in one aspect: It's potent, but it doesn't taste like caffeine. The caffeine made it through the process intact; I can feel its effects after drinking a <code>10oz</code> glass, however the grapefruit flavors and citrate salts masked the bitterness of the caffeine perfectly.</p>
<p>If I was to do this again, I might try:</p>
<ul>
<li>less salts and more acid</li>
<li><s>filtering the liquid after cooling but before bottling</s>
<ul>
<li><strong>EDIT</strong>: for the 2nd batch I ended up filtering the ingredients with a  coffee filter instead of trying to filter the entire batch. See the comment below for the results!!</li>
</ul>
</li>
<li>I could consider pasteurizing it after sufficient carbonation has been achieved, this would allow me to add more sugar and make it semi-sweet</li>
<li><s>I might do some research on the citrus peel oil and if its possible to make it stay in an emulsion somehow</s> <strong>EDIT</strong>: Nope, the floaters were caused by solids that should have been filtered out, not by oils.</li>
</ul>
<h2 id="epilogue">epilogue</h2>
<p>I ended up making a simple syrup out of a cup of sugar and a cup and a half of water + the left-over &quot;oleo-saccharum&quot;  grapefruit peel extract.  Adding a spoonful or two of this to each serving improves the flavor dramatically.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/04/syrup.jpg" class="kg-image" alt="Homebrew Unsweetened Grapefruit Energy Drink"></figure><!--kg-card-begin: markdown--><p>Having the <strong><em>fresh</em></strong> citrus peel syrup in it makes it &quot;smell right&quot; and taste a lot better.  Most of my friends that tried the final version of the drink with the syrup included seemed to like it, describing it as &quot;refreshing&quot;, &quot;interesting&quot;, and &quot;strong&quot;.</p>
<p><strong>EDIT:</strong> I ended up making 2nd and 3rd batches, see my comments below!  Second batch ended up looking a lot better, with no floaters in it, and I think it tastes better too.  For the third I experimented with a new flavor, cherry-lime.</p>
<p>It gets fully carbonated after about a week, honestly I was very satisfied with how the carbonation turned out. As the brew aged in the bottles, it also started to get more of a sort of &quot;dry&quot; taste that I remembered from extra-dry wine or cider, which used to be my favorite, so obviously I was pretty happy about that. I don't know how long it will stay good in the bottles as I've always drank it all before two months elapsed 😇 but I think it tastes best after at least 2 weeks at room temp.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[What is a Process? (Operating Systems)]]></title><description><![CDATA[All typical operating systems (MacOS, Windows, Linux, BSD, and others) have a notion of a "Process", and there are far more similarities than differences...]]></description><link>https://sequentialread.com/what-is-a-process/</link><guid isPermaLink="false">61f705cbd30cc100015a8f68</guid><category><![CDATA[operations]]></category><category><![CDATA[education]]></category><category><![CDATA[linux]]></category><category><![CDATA[Self-Hosting]]></category><category><![CDATA[windows]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Mon, 31 Jan 2022 02:41:15 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2022/02/process4-1.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2022/02/process4-1.jpg" alt="What is a Process? (Operating Systems)"><p><em>This post is part of a new experiment; I'm toying with the idea of writing a concise and approachable &quot;folk textbook&quot; covering the conceptual basics of what I do; using Linux and other operating systems like MacOS and Windows, home routers, networking software, servers, virtual machines, linux containers, etc. I'm starting out by writing a few drafts or tidbits in this style as blog articles.</em></p>
<p><em>I actually started out on this path when I was writing on a different subject, but I got stuck halfway through: I wanted to mention processes and process exit codes, but I couldn't come up with any high-quality primary sources to cite.  As far as I can tell, neither wikipedia nor the linux man pages (linux manual) contain a concise definition of what a process is including it's common features, i.e. things that every process has / things that every process can do.</em></p>
<p><em>I know I've mentioned linux a lot here, but I'd like to emphasize that this article is about processes in <strong>all</strong> major desktop and mobile phone operating systems, including Windows, and the similarities that they have.</em></p>
<p><em>Folks on the <a href="https://cyberia.club/matrix">cyberia.club matrix server</a> suggested an operating systems textbook...</em></p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><img src="https://sequentialread.com/content/images/2022/02/process4-1.jpg" style="height: 0px" alt="What is a Process? (Operating Systems)"><!--kg-card-end: html--><!--kg-card-begin: html-->
<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/01/forest-ava-80.png" alt="What is a Process? (Operating Systems)">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <p>what would you consider a primary source for this information ?</p>
        <blockquote><i>what is a process exit code / what's the anatomy of a process, what things does a process have / what does it do?</i></blockquote>
<p>I can't find anything on for example the man pages 😦</p>
<p>
Argh. this is hilarious to me that this crucial conceptual information is not even written down in a public online place outside of anecdotes </p>
    </div>
</div>

</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/01/starless.jpeg" alt="What is a Process? (Operating Systems)">
<div>
	<span class="username bluepurple"> 
        Starless
    </span>
    <div class="body self"> 
        <p>A comp sci textbook, perhaps?</p>
    </div>
</div>
    
</div>

<div class="chat ">
<img src="https://sequentialread.com/content/images/2022/01/forest-ava-80.png" alt="What is a Process? (Operating Systems)">
<div>
	<span class="username self"> 
        forest
    </span>
    <div class="body self"> 
        <p>its easy to find textbook pdfs online but they are kinda crap for what I want because</p>
        
        <ul>
            <li>the information is presented in book form and IMO no one wants to read like that any more</li>
            <li>you can't make a hyperlink to a section in a PDF afaik</li>
            <li>they aren't good for linking to anywasy because they aren't permanent because of copyright claims</li>
        </ul>
    </div>
</div>
</div>

<hr>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p><em>... but what I really wanted was something I could link to. So...</em></p>
<p><em>here weeee gooooo!  (If you notice any factual innacuracies in this article, please let me know or leave a comment below!)</em></p>
<hr>
<h2 id="tableofcontents">Table of Contents</h2>
<ul>
<li><a href="#overviewofprocesses">Overview of Processes</a></li>
<li><a href="#thetypicalcreateprocessoptions">The Typical &quot;Create Process&quot; Options</a>
<ul>
<li><a href="#whichprogramtorun">Which Program to Run</a></li>
<li><a href="#programarguments">Program Arguments</a></li>
<li><a href="#standardinputstream">Standard Input Stream</a></li>
<li><a href="#standardoutputstream">Standard Output Stream</a></li>
<li><a href="#standarderrorstream">Standard Error Stream</a></li>
<li><a href="#filedescriptorsstdinstdoutstderrcontinued">File Descriptors (<code>stdin</code>, <code>stdout</code>, <code>stderr</code> continued)</a></li>
<li><a href="#environmentvariables">Environment Variables</a></li>
<li><a href="#workingdirectory">Working Directory</a></li>
<li><a href="#runasuser">Run as User</a></li>
</ul>
</li>
<li><a href="#typicalfeaturesofarunningprocess">Typical Features of a Running Process</a>
<ul>
<li><a href="#processidpid">Process ID (<code>PID</code>)</a></li>
<li><a href="#useriduid">User ID (<code>UID</code>)</a></li>
<li><a href="#groupidgid">Group ID (<code>GID</code>)</a></li>
<li><a href="#currentlyopenfilesfiledescriptorsfilehandles">Currently Open Files (File Descriptors / File Handles)</a></li>
<li><a href="#currentlyopensocketsnetworkconnectionsorlisteningservers">Currently Open Sockets (Network Connections or Listening Servers)</a></li>
<li><a href="#signalssigintsigtermetc">Signals (<code>SIGINT</code>, <code>SIGTERM</code>, etc)</a></li>
</ul>
</li>
<li><a href="#afteraprocessexits">After a Process Exits</a>
<ul>
<li><a href="#exitcode">Exit Code</a></li>
</ul>
</li>
</ul>
<h2 id="overviewofprocesses">Overview of Processes</h2>
<p>All typical operating systems (MacOS, Windows, Linux, BSD, and others) have a notion of a &quot;Process&quot;, and there are far more similarities than differences between the ways that these different OSes implement processes.</p>
<p>When you launch a program (For example, by clicking on that program in the Start Menu (Windows), or Dock (MacOS / Ubuntu Linux), you are instructing the operating system on your computer to launch a new process in order to run the program you clicked on.</p>
<p>There are two other common ways to launch a process on a computer:</p>
<ol>
<li>In an interactive shell or &quot;command line&quot; — <code>cmd.exe</code> (Windows) or <code>bash</code>/<code>zsh</code> (Linux/MacOS) — every command that you run creates a process.</li>
<li>If you are writing your own program, it's almost certain that the programming language you are using includes some <code>process.Start()</code> functionality in its <a href="https://en.wikipedia.org/wiki/Standard_library">standard library</a>. Such a function will spawn a new process in the operating system, typically a subprocess of the process calling the function.</li>
</ol>
<p>Programs running as processes on your computer may perform arbitrary computations, but in order to make themselves useful they'll also need to interact with your computer hardware. For example, to respond to mouse and keyboard input, display pixels on the screen, read and write files, and connect to other computers on the internet.</p>
<p>In order to increase stability, security, and performance, all modern operating systems separate processes from the hardware and from each-other.</p>
<p>Process A is not allowed to read or write data used by Process B.</p>
<p>Any given process is not allowed to directly touch the screen, disk, or network. Instead, it must ask the operating system to do those things on its behalf.  This is called a &quot;system call&quot; or &quot;syscall&quot;.</p>
<p>Once started, a process will run either until the loaded program calls the <a href="https://www.man7.org/linux/man-pages/man3/exit.3.html"><code>exit()</code> syscall</a>, or until the process is forcefully killed by the operating system.</p>
<p>When it comes to creation of new processes, the syscalls involved, <a href="https://www.man7.org/linux/man-pages/man2/fork.2.html"><code>fork()</code></a> and <a href="https://www.man7.org/linux/man-pages/man3/exec.3.html"><code>exec()</code></a>, are much more complex.  Typically, programmers interact with these syscalls through a more simplified process <a href="https://en.wikipedia.org/wiki/API">API</a> in their language of  choice.</p>
<h2 id="thetypicalcreateprocessoptions">The Typical &quot;Create Process&quot; Options</h2>
<p>When we wish to start a new process, whether we do it by clicking on an application icon, running a command, or in our code, there are many settings being applied to that new process. Depending on how the process is launched, it may or may not be possible for you to control all of these settings, but it's important to understand that they always <em><strong>can</strong></em> be specified, you just have to figure out how.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2022/02/process4.drawio.png" class="kg-image" alt="What is a Process? (Operating Systems)"></figure><!--kg-card-begin: markdown--><h3 id="whichprogramtorun"><strong>Which Program to Run</strong></h3>
<p>First, we need to tell the operating system which executable file (program) we want it to run.</p>
<p>In many cases, like when running a command inside a shell, we can simply provide the name of a program, and the full file path of that program's executable file will be automatically resolved for us via the <a href="https://superuser.com/a/284351">PATH environment variable</a>.</p>
<blockquote>
<p><strong>🤔 Example:</strong> In my Linux <code>bash</code> shell, when I type <code>apt moo</code>, the shell</p>
<ol>
<li>looks into the <code>PATH</code> variable
<ul>
<li>The value of <code>PATH</code> looks something like this: <code>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</code></li>
</ul>
</li>
<li>searches each folder mentioned in the <code>PATH</code> variable
<ul>
<li>It's a list of folders separated by colons (<code>:</code>)
<ul>
<li>/usr/local/sbin</li>
<li>/usr/local/bin</li>
<li>/usr/sbin</li>
<li>/usr/bin</li>
<li>/bin</li>
</ul>
</li>
</ul>
</li>
<li>resolves to the first file called <code>apt</code> inside one of those folders
<ul>
<li>you can see this in action with the <code>which</code> command on MacOS/Linux or <code>where</code> command on Windows:
<ul>
<li><code>which apt</code> → <code>/usr/bin/apt</code></li>
<li><code>where wmic</code> → <code>C:\Windows\System32\wbem\WMIC.exe</code></li>
</ul>
</li>
</ul>
</li>
<li>Spawns a new process running the <code>/usr/bin/apt</code> executable binary file with <code>moo</code> as the first and only program argument, invoking the <a href="https://unix.stackexchange.com/questions/92185/whats-the-story-behind-super-cow-powers">&quot;Super Cow Powers&quot; apt easter egg</a>:</li>
</ol>
<pre><code>forest@thingpad:~$ apt moo
                 (__) 
                 (oo) 
           /------\/ 
          / |    ||   
         *  /\---/\ 
            ~~   ~~   
...&quot;Have you mooed today?&quot;...
</code></pre>
</blockquote>
<h3 id="programarguments"><strong>Program Arguments</strong></h3>
<p>Arguments are meant to tell the program what to do or how to run, the name &quot;argument&quot; comes from <a href="https://en.wikipedia.org/wiki/Argument_of_a_function">math terminology for functions</a>, for example, in <code>f(x, y)</code>, x and y are called the <code>arguments</code> of the function <code>f</code>.</p>
<p>In this case, <code>f</code> is our program, and <code>x</code> is the first argument, <code>y</code> is the second argument.</p>
<blockquote>
<p><strong>ℹ️ NOTE:</strong> when writing program code or scripts, the <strong>&quot;Which Program to Run&quot;</strong> and <strong>&quot;Program Arguments&quot;</strong> information is often combined into a single list or array called <code>arguments</code>, <code>args</code>, or <code>argv</code>.  So <code>args[0]</code> will be the program, and <code>args[1]</code> will be the 1st argument.</p>
<p>(<code>argv</code> is short for Argument Vector, it's called  this because in C programming, arrays are commonly called &quot;vectors&quot;)</p>
</blockquote>
<p>Arguments are strings, that means they must contain text, and any section of text is a valid argument.</p>
<p>In shells like <code>bash</code> and <code>cmd.exe</code> on Windows, space is a syntax character; arguments are automatically separated by spaces. So if you want to pass an argument that <em>CONTAINS</em> spaces, you must  wrap it in quotation marks to denote that it is a single string.</p>
<p>Consider this bash script file named <code>myscript.sh</code>:</p>
<pre><code>#!/bin/bash

echo &quot;&quot;
echo &quot;the currently running program is: $0&quot;
echo &quot;the first argument is: $1&quot;
echo &quot;the second argument is: $2&quot;
</code></pre>
<p>Here are two examples where I ran this script in order to demonstrate<br>
arguments:</p>
<pre><code>$ ./myscript.sh i like eggs

the currently running program is: ./myscript.sh
the first argument is: i
the second argument is: like
</code></pre>
<pre><code>$ ./myscript.sh &quot;i like eggs&quot;

the currently running program is: ./myscript.sh
the first argument is: i like eggs
the second argument is: 
</code></pre>
<h3 id="standardinputstream"><strong>Standard Input Stream</strong></h3>
<p>Standard Input, often called <code>stdin</code>, is a stream of data that the process can read from. No encoding is specified, so it's a stream of arbitrary bytes. It could be text, or it could be something else.</p>
<p><code>stdin</code> is usually used by multi-purpose programs like <a href="https://www.man7.org/linux/man-pages/man1/grep.1.html"><code>grep</code></a> and <a href="https://www.man7.org/linux/man-pages/man1/sed.1.html"><code>sed</code></a> which can process another program's output. This is colloquially called <a href="https://en.wikipedia.org/wiki/Pipeline_%28Unix%29">&quot;piping&quot;</a>.</p>
<h3 id="standardoutputstream"><strong>Standard Output Stream</strong></h3>
<p>Standard Output, often called <code>stdout</code>, is a stream of data that the process can write to. <code>stdout</code> is raw &quot;anything goes&quot; bytes just like <code>stdin</code>.</p>
<p><code>stdout</code> is commonly used for program output. By default, when running a program in an interactive shell, that program's standard output will be displayed in the shell.</p>
<p>The output does not have to be text and it does not have to be displayed to the screen; it could be raw binary data and it could be piped to another program or written to a file. For example on linux, the <code>gzip</code> program will output the compressed bytes directly.</p>
<p>For programs which do not have any direct output, <code>stdout</code> is commonly used for logging. Log messages may be written to either <code>stdout</code>, <code>stderr</code>, or both.</p>
<blockquote>
<p><strong>⚠️ NOTE:</strong> Many command line programs will behave differently when they detect that they are running inside an interactive shell, so they may format their output differently.</p>
<p>As an example, the <code>ls</code> (list directory) command on linux will display files separated with spaces in interactive mode, and separated with newlines otherwise:</p>
<pre><code>~/example$ ls
file1  file2  file3
~/example$ echo &quot;$(ls)&quot;
file1
file2
file3
</code></pre>
</blockquote>
<h3 id="standarderrorstream"><strong>Standard Error Stream</strong></h3>
<p>Standard Error, often called <code>stderr</code>, is a second stream of data that the process can write to. It's raw &quot;anything goes&quot; bytes just like <code>stdout</code>.</p>
<p>Unlike <code>stdout</code>, <code>stderr</code> <em><strong>should</strong></em> always output text and <em>should</em> be used exclusively for logging. By default, it is always displayed in the shell and never piped into another program's input.</p>
<h3 id="filedescriptorsstdinstdoutstderrcontinued"><strong>File Descriptors (<code>stdin</code>, <code>stdout</code>, <code>stderr</code> continued)</strong></h3>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><img style="float:right; max-width:25vw;" src="https://sequentialread.com/content/images/2022/02/280px-Pipeline.svg.png" alt="What is a Process? (Operating Systems)"><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Sometimes we might want to change the shell's default &quot;always display <code>stderr</code>, use <code>stdout</code> for piping&quot; behavior. For example when we want to write the standard error stream to a log file, or we want to process it further before it's displayed.</p>
<p>In the bourne shell <code>sh</code> and its derivatives like <code>bash</code> and <code>zsh</code>, the stream-redirection operator <code>&gt;</code> can be used to modify this behavior by specifying both the source and the destination as file descriptors.</p>
<p>Each process has its own numbered list of file descriptors, and by convention, the first three are always the standard io streams:</p>
<p>file descriptor <code>0</code>: <code>stdin</code><br>
file descriptor <code>1</code>: <code>stdout</code><br>
file descriptor <code>2</code>: <code>stderr</code></p>
<p>As a concrete example, some non-standard programs write thier <code>help</code> (usage instructions) to <code>stderr</code>, but we want to search for lines containing a specific string inside that output:</p>
<pre><code>nmcli device help | grep wifi
</code></pre>
<p>This won't work because <a href="https://www.man7.org/linux/man-pages/man1/grep.1.html"><code>grep</code></a> will only see the (empty) <code>stdout</code> from <a href="https://developer-old.gnome.org/NetworkManager/stable/nmcli.html"><code>nmcli</code> (Network Manager CLI)</a>. But if we redirect <code>nmcli</code>'s <code>stderr</code> (file descriptor <code>2</code>) into its <code>stdout</code> (file descriptor <code>1</code>), then grep can filter the output for display:</p>
<pre><code>nmcli device help 2&gt;&amp;1 | grep wifi
</code></pre>
<p>The <code>&amp;</code> shell syntax is specifying that <code>&amp;1</code> is a destination file descriptor.</p>
<p>As another example, say we wanted to write the <code>stdout</code> and <code>stderr</code> streams to two separate files, for example, we have a web server that logs its status information and errors to <code>stderr</code>, while it writes its access log to <code>stdout</code>:</p>
<pre><code>caddy run 2&gt;/var/log/caddy-status.log &gt;/var/log/caddy-access.log
</code></pre>
<h3 id="environmentvariables"><strong>Environment Variables</strong></h3>
<p>Environment variables are similar to program arguments, with two major differences:</p>
<ol>
<li>Instead of being a simple list of strings, environment variables are named; each variable has a name and a value, and you can't have two variables with the same name</li>
<li>Environment variables are always inherited from the parent process unless otherwise specified</li>
</ol>
<p>Some environment variable names like <a href="https://superuser.com/a/284351"><code>PATH</code></a> and <a href="https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html#tag_08_03"><code>HOME</code></a> are fairly standard across all operating systems, while others are specific to a single program.</p>
<blockquote>
<p><strong>⚠️ NOTE:</strong> If you run a program and then change an environment variable, your change will not affect any currently-running processes! Each process runs in its own environment, and that environment is copied from the parent when the process starts.</p>
</blockquote>
<p>On Linux, you can run the <code>env</code> command to display all currently specified environment variables. On windows (PowerShell), you would run <code>dir env:</code>.</p>
<p>Here's a trimmed-down example from my computer:</p>
<pre><code>$ env
SHELL=/bin/bash
DESKTOP_SESSION=ubuntu
HOME=/home/forest
USERNAME=forest
LANG=en_US.UTF-8
USER=forest
PATH=/home/forest/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
</code></pre>
<blockquote>
<p><strong>ℹ️ NOTE:</strong> you can set or override an evironment variable for a process you launch in a shell by including the environment variable before the command.</p>
<p>MacOS/Linux: <code>MY_VAR=example mycommand</code><br>
Windows: <code>cmd /C &quot;set MY_VAR=example &amp;&amp; mycommand&quot;</code></p>
</blockquote>
<h3 id="workingdirectory"><strong>Working Directory</strong></h3>
<p>Every process has a &quot;Working Directory&quot;, or a folder that it's running inside. If that process tries to read or write a file with a relative path, the path will be relative to the working directory.  For example if the file path specified was <code>myfile.txt</code>, and the working directory was <code>/Users/example/</code>, then the file read or written would be <code>/Users/example/myfile.txt</code>.</p>
<p>The working directory is typically inherited from the parent process which spawned it, however the working directory can be changed if desired.</p>
<h3 id="runasuser"><strong>Run as User</strong></h3>
<p>By default, any new process will run as same user who was running the parent process. If you want to run a process as a different user, you will have to use a special tool that allows this.</p>
<ul>
<li>MacOS/Linux:
<ul>
<li><a href="https://man7.org/linux/man-pages/man8/sudo.8.html"><code>sudo</code> (super-user do)</a> &amp; <a href="https://man7.org/linux/man-pages/man1/su.1.html"><code>su</code> (substitute user)</a></li>
</ul>
</li>
<li>BSD/Linux:
<ul>
<li><a href="https://man.openbsd.org/doas"><code>doas</code> (do as)</a></li>
</ul>
</li>
<li>Windows
<ul>
<li><code>Right Click → Run As Administrator...</code></li>
<li><a href="https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/cc771525(v=ws.11)">runas</a></li>
</ul>
</li>
</ul>
<p>If you are using a background process manager like <a href="https://systemd.io/">systemd</a> or <a href="https://wiki.alpinelinux.org/wiki/OpenRC">OpenRC</a> on Linux, <a href="https://support.apple.com/guide/terminal/script-management-with-launchd-apdc6c1077b-5d5d-4d35-9c19-60f2397b2369/mac">launchd</a> on MacOS, or <a href="https://docs.microsoft.com/en-us/windows/win32/services/about-services">Windows Services</a>, you'll be able to specify the user to run the process as inside your configuration file.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><hr>
<h2 id="typicalfeaturesofarunningprocess">Typical Features of a Running Process</h2>
<h3 id="processidpid"><strong>Process ID (<code>PID</code>)</strong></h3>
<p>The process ID is a unique integer ID number that the operating system assigns to each process.</p>
<p>You can access the list of processes along with thier IDs and other information with:</p>
<p>MacOS/Linux: <a href="https://www.man7.org/linux/man-pages/man1/ps.1.html"><code>ps</code> (Processes) command</a><br>
Windows: Task Manager / <a href="https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/tasklist"><code>tasklist</code> command</a></p>
<h3 id="useriduid"><strong>User ID (<code>UID</code>)</strong></h3>
<p>The user ID is a unique integer that the operating system uses to keep track of which user is running this process. There's also the <code>EUID</code> (&quot;effective user id&quot;) for binaries like <a href="https://man7.org/linux/man-pages/man8/sudo.8.html"><code>sudo</code></a> which are configured to be able to change the user they are running as.</p>
<h3 id="groupidgid"><strong>Group ID (<code>GID</code>)</strong></h3>
<p>Similar to User ID, but for group membership.</p>
<h3 id="currentlyopenfilesfiledescriptorsfilehandles"><strong>Currently Open Files (File Descriptors / File Handles)</strong></h3>
<p>The operating system keeps track of which files have been opened, and by which process. On Windows, you may not be able to delete a file or unmount a disk if a process is still accessing it.</p>
<blockquote>
<p><strong>ℹ️ NOTE:</strong> to list currently open files on your computer:</p>
<p>MacOS/Linux: <a href="https://man7.org/linux/man-pages/man8/lsof.8.html"><code>lsof</code> (List Open Files) command</a><br>
Windows:  <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer">Sysinternals Process Explorer</a> application or the <a href="https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-xp/bb490961(v=technet.10)?redirectedfrom=MSDN"><code>openfiles</code> command</a></p>
</blockquote>
<h3 id="currentlyopensocketsnetworkconnectionsorlisteningservers"><strong>Currently Open Sockets (Network Connections or Listening Servers)</strong></h3>
<p>Similar to files, the operating system also keeps track of network connections (sockets) to websites or services (local or on the internet) as well as listening sockets, aka servers, that each process has created.</p>
<p>Sockets may use either <a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol">TCP (Transmission Control Protocol)</a> or <a href="https://en.wikipedia.org/wiki/User_Datagram_Protocol">UDP (User Datagram Protocol)</a>. TCP guarantees that the connection is reliable and data is transmitted and recieved in proper order. When data cannot be transmitted or recieved, TCP will return a <code>read error</code> or <code>write error</code>. UDP offers higher efficiency and lower latency, but it doesn't guarantee proper ordering of transmitted data, and it can't provide feedback when transmission fails.</p>
<p>Besides TCP/UDP, there are two main types of sockets,</p>
<h4 id="1connectedwaitingsocketsakaclientsockets">1. &quot;Connected/Waiting&quot; sockets aka &quot;client&quot; sockets</h4>
<ul>
<li>Applications like web browsers create client sockets to connect to web servers and other types of servers on the internet (or to servers running on your computer or local network)</li>
<li>&quot;Client&quot; sockets have two port numbers:
<ul>
<li><strong>source port</strong>
<ul>
<li>a randomly generated large number like &quot;41386&quot;</li>
</ul>
</li>
<li><strong>destination port</strong>
<ul>
<li>the port number that the client connected to, for example port <code>80</code> for <code>http</code> or port <code>443</code> for <code>https</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="2listeningsocketsakaserversockets">2. &quot;Listening&quot; sockets aka &quot;server&quot; sockets</h4>
<ul>
<li>Web servers themselves create listening sockets in order to &quot;open a port&quot; for incoming client connections</li>
<li>&quot;Server&quot; sockets only have one port number, the <strong>listening port</strong></li>
</ul>
<blockquote>
<p><strong>ℹ️ NOTE:</strong> to list sockets on your computer:</p>
<p>Linux: <a href="https://www.man7.org/linux/man-pages/man8/netstat.8.html"><code>netstat</code> (Network Statistics) command</a> / <a href="https://www.man7.org/linux/man-pages/man8/ss.8.html"><code>ss</code> (Socket Statistics) command</a><br>
MacOS: <a href="https://apple.stackexchange.com/questions/117644/how-can-i-list-my-open-network-ports-with-netstat"><code>lsof -i -P -n | grep LISTEN</code> (list open files, then search for LISTEN)</a><br>
Windows: <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/tcpview">Sysinternals TCPView</a> or the <a href="https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/netstat"><code>netstat</code> command</a></p>
</blockquote>
<h3 id="signalssigintsigtermetc"><strong>Signals (<code>SIGINT</code>, <code>SIGTERM</code>, etc)</strong></h3>
<p>Processes can send <a href="https://www.man7.org/linux/man-pages/man7/signal.7.html">signals</a> to each-other. Typically this is used to shut other processes down when they are no longer wanted.</p>
<p>For example, say you ran a process in your shell, it's taking forever to complete, and you don't want it to run any more. You might press <code>Ctrl + c</code> on your keyboard, which will instruct your terminal to send a terminate signal or <code>SIGTERM</code> to the currently executing process.</p>
<p>If you've ever done this, you might already know that the <code>Ctrl + c</code> isn't guaranteed to end a process, some processes may ignore your request; perhaps they've crashed, they're in turn waiting for something else before they think they can <a href="https://www.man7.org/linux/man-pages/man3/exit.3.html"><code>exit()</code></a>, etc.</p>
<p>In this case, you may escalate to <a href="https://man7.org/linux/man-pages/man2/kill.2.html"><code>kill -SIGKILL &lt;PID&gt;</code></a> which instructs the operating system to remove the process immediately with no questions asked.</p>
<hr>
<h2 id="afteraprocessexits">After a Process Exits</h2>
<h3 id="exitcode"><strong>Exit Code</strong></h3>
<p>The exit code is an integer is commonly used to denote the reason why the program exited.</p>
<p>An exit code of <code>0</code> should always mean that the program succeeded or ran normally, while an exit code of <code>1</code> means that the program failed or crashed. Other numbers may be used and given specific meanings on a program-by-program basis.</p>
<p>If you ever see an exit code of <code>-1</code>, that may mean that the program never actually exited, perhaps it was <a href="https://unix.stackexchange.com/a/281440/182810"><code>SIGKILL</code>ed</a> by the operating system instead.</p>
<blockquote>
<p><strong>ℹ️ NOTE:</strong> You can access the exit code of the previous command in the shell. It's <code>$?</code> in <code>bash</code> (MacOS/Linux) and <code>%ErrorLevel%</code> in <code>cmd.exe</code> (Windows)</p>
</blockquote>
<hr>
<p><em>If you notice any factual innacuracies in this article, please let me know or leave a comment below!</em></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Capsul - Rumors of my Demise have been Greatly Exaggerated]]></title><description><![CDATA[..just to make 100% sure that the maintenance we were doing would succeed & maintain stability for everyone who has placed thier trust in us and voted with thier shells, investing thier time and money on virtual machines that we maintain on a volunteer basis.]]></description><link>https://sequentialread.com/capsul-rumors-my-demise-greatly-exaggerated/</link><guid isPermaLink="false">61b64ab36fa43c00012f6573</guid><category><![CDATA[rackmount]]></category><category><![CDATA[cyberia]]></category><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[operations]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Sun, 19 Dec 2021 01:41:30 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2021/12/silly-nvme-bracket2-small2.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2021/12/silly-nvme-bracket2-small2.jpg" alt="Capsul - Rumors of my Demise have been Greatly Exaggerated"><p>This is a cross-post with the <a href="https://cyberia.club/blog/20211217-capsul-maintenance-updates">cyberia computer club blog</a>.</p>
<p>Previous post in this series: <a href="https://sequentialread.com/capsul-rollin-onwards-with-a-web-application/">Capsul Rollin' Onwards with a Web Application</a></p>
<h2 id="whatisthis">What is this?</h2>
<p>If you're a wondering &quot;what is capsul?&quot;, see:</p>
<p><a href="https://capsul.org">https://capsul.org</a></p>
<p>Here's a quick summary of what's in this post:</p>
<ul>
<li>
<p>cryptocurrency payments are back</p>
</li>
<li>
<p>we visited the server in person for maintenance</p>
</li>
<li>
<p>most capsuls disks should have trim/discard support now, so you can run the fstrim command to optimize your capsul's disk. (please do this, it will save us a lot of disk space!!)</p>
</li>
<li>
<p>we updated most of our operating system images and added a new rocky linux image!</p>
</li>
<li>
<p>potential ideas for future development on capsul</p>
</li>
<li>
<p>exciting news about a new server and a new capsul fork being developed by co-op cloud / servers.coop</p>
</li>
</ul>
<hr>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="whathappenedtothecryptocurrencypaymentoption">What happened to the cryptocurrency payment option?</h2>
<p>Life happens. Cyberia Computer Club has been hustling and bustling to build out our new in-person space in Minneapolis, MN:</p>
<p><a href="https://wiki.cyberia.club/hypha/cyberia_hq/faq">https://wiki.cyberia.club/hypha/cyberia_hq/faq</a></p>
<p>Hackerspace, lab, clubhouse, we aren't sure what to call it yet, but we're extremely excited to finish with the renovations and move in!</p>
<p>In the meantime, something went wrong with the physical machine hosting our BTCPay server and we didn't have anywhere convenient to move it, nor time to replace it, so we simply disabled cryptocurrency payments temporarily in September 2021.</p>
<p>Many of yall have emailed us asking &quot;what gives??&quot;, and I'm glad to finally be able to announce that</p>
<p>&quot;the situation has been dealt with&quot;,</p>
<p>we have a brand new server and the blockchain syncing process is complete, cryptocurrency payments in bitcoin, litecoin, and monero are back online now!</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><pre style="display:inline-block; max-width:58em;">     --&gt;   <a href="https://capsul.org/payment/btcpay">https://capsul.org/payment/btcpay</a>   &lt;--   &nbsp;
</pre>
<br>
<br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Also, fun fact, the BTCPay server is currently located at my house, but I didn't have a good way to expose it to the internet, so I simply pointed the btcpay.cyberia.club domain at my <a href="https://greenhouse-alpha.server.garden/using-your-own-domain-name-with-greenhouse">greenhouse subdomain</a></p>
<pre><code>root@beet:~# nslookup btcpay.cyberia.club
Server:		127.0.0.53
Address:	127.0.0.53#53

Non-authoritative answer:
btcpay.cyberia.club	canonical name = forest.greenhouseusers.com.
Name:	forest.greenhouseusers.com
Address: 68.183.194.212
</code></pre>
<p>Then published the server to the internet thru <a href="https://greenhouse.server.garden">greenhouse</a>!</p>
<pre><code>root@beet:~# greenhouse tunnel https://btcpay.cyberia.club to tcp://localhost:3000
Now applying new tunnel configuration...
  - waiting for underlying services to start
  - creating threshold tunnels
  - testing threshold tunnels
  - configuring caddy
  - waiting for caddy to obtain https certificates from Let's Encrypt
  - final testing
Your tunnel was configured successfully!

</code></pre>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><hr>
<h2 id="thatonetimecapsulwasalmostfsyncdtodeath">That one time capsul was almost fsync()'d to death</h2>
<p>Guess what? Yall loved capsul so much, you wore our disks out. Well, almost.</p>
<p>We use redundant solid state disks + the ZFS file system for your capsul's block storage needs, and it turns out that some of our users like to write files. A lot.</p>
<p>Over time, SSDs will wear out, mostly dependent on how many writes hit the disk. Baikal, the server behind capsul.org, is a bit different from a typical desktop computer, as it hosts about 100 virtual machines, each with thier own list of application processes, for over 50 individual capsul users, each of whom may be providing services to many other individuals in turn.</p>
<p>The disk-wear-out situation was exacerbated by our geographical separation from the server; we live in Minneapolis, MN, but the server is in Georgia. We wanted to install NVME drives to expand our storage capacity ahead of growing demand, but when we would mail PCI-e to NVME adapters to CyberWurx, our datacenter colocation provider, they kept telling us the adapter didn't fit inside the 1U chassis of the server.</p>
<p>At one point, we were forced to take a risk and undo the redundancy of the disks in order to expand our storage capacity and prevent &quot;out of disk space&quot; errors from crashing your capsuls. It was a calculated risk, trading certain doom now for the potential possibility of doom later.</p>
<p>Well, time passed while we were busy with other projects, and those non-redundant disks started wearing out. According to the &quot;smartmon&quot; monitoring indicator, they reached about 25% lifespan remaining. Once the disk theoretically hit 0%, it would become read-only in order to protect itself from total data loss. So we had to replace them before that happened.</p>
<!--kg-card-end: markdown--><p></p><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2021/12/smartmon_dec2021.png" class="kg-image" alt="Capsul - Rumors of my Demise have been Greatly Exaggerated"></figure><!--kg-card-begin: markdown--><p>We were so scared of what could happen if we slept on this that we booked a flight to Atlanta for maintenance. We wanted to replace the disks in person, and ensure we could restore the ZFS disk mirroring feature.</p>
<p>We even custom 3d-printed a bracket for the tiny PCI-e NVME drive that we needed in order to restore redundancy for the disks, just to make 100% sure that the maintenance we were doing would succeed &amp; maintain stability for everyone who has placed thier trust in us and voted with thier shells, investing thier time and money on virtual machines that we maintain on a volunteer basis.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2021/12/silly-nvme-bracket2.jpg" class="kg-image" alt="Capsul - Rumors of my Demise have been Greatly Exaggerated"></figure><!--kg-card-begin: markdown--><p>Unfortunately, &quot;100% sure&quot; was still not good enough, the new NVME drive didn't work as a ZFS mirroring partner at first ⁠— the existing NVME drive was 951GB, and the one we had purchased was 931GB. It was too small and ZFS would not accept that. f0x suggested:</p>
<blockquote>
<p>[you could] start a new pool on the new disk, zfs send all the old data over, then have an equally sized partition on the old disk then add that to the mirror</p>
</blockquote>
<p>But we had no idea how to do that exactly or how long it would take &amp; we didn't want to change the plan at the last second, so instead we ended up taking the train from the datacenter to Best Buy to buy a new disk instead.</p>
<p>The actual formatted sizes of these drives are typically never printed on the packaging or even mentioned on PDF datasheets online. When I could find an actual number for a model, it was always the lower 931GB. So, we ended up buying a &quot;2TB&quot; drive as it was the only one BestBuy had which we could guarantee would work.</p>
<p>So, lesson learned the hard way. If you want to use ZFS mirroring and maybe replace a drive later, make sure to choose a fixed partition size which is slightly smaller than the typical avaliable space on the size of drive you're using, in case the replacement drive was manufactured with slightly less avaliable formatted space!!!</p>
<p>Once mirroring was restored, we made sure to test it in practice by carefully removing a disk from the server while it's running:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><video autobuffer controls preload="auto" style="max-height: 80vh;">
      <source src="https://picopublish.sequentialread.com/files/zfs_disk_replacement/zfs_disk_replacement.mp4" type="video/mp4">
      <source src="https://picopublish.sequentialread.com/files/zfs_disk_replacement/zfs_disk_replacement.webm" type="video/webm">
      <p>Your browser doesn't support HTML5 video. Here is
         a <a href="https://picopublish.sequentialread.com/files/zfs_disk_replacement/zfs_disk_replacement.mp4">link to the video</a> instead.</p>
</video><br><br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>While we could have theoretically done this maintenance remotely with the folks at CyberWurx performing the physical parts replacement per a ticket we open with them, we wanted to be sure we could meet the timeline that the disks had set for <strong>US</strong>. That's no knock on CyberWurx, moreso a knock on us for yolo-ing this server into &quot;production&quot; with tape and no test environment :D</p>
<p>The reality is we are vounteer supported. Right now the payments that the club receives from capusl users don't add up to enough to compensate (make ends meet for) your average professional software developer or sysadmin, at least if local tech labor market stats are to be believed.</p>
<p>We are all also working on other things, we can't devote all of our time to capsul. But we do care about capsul, we want our service to live, mostly because we use it ourselves, but also because the club benefits from it.</p>
<p>We want it to be easy and fun to use, while also staying easy and fun to maintain. A system that's agressively maintained will be a lot more likely to remain maintained when it's no one's job to come in every weekday for that.</p>
<p>That's why we also decided to upgrade to the latest stable Debian major version on baikal while we were there. We encountered no issues during the upgrade besides a couple of initial omissions in our package source lists. The installer also notified us of several configuration files we had modified, presenting us with a git-merge-ish interface that displayed diffs and allowed us to decide to keep our changes, replace our file with the new version, or merge the two manually.</p>
<p>I can't speak more accurately about it than that, as j3s did this part and I just watched :)</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><hr>
<h2 id="lookingtothefuture">Looking to the future</h2>
<p>We wanted to upgrade to this new Debian version because it had a new major version of QEMU, supporting <code>virtio-blk</code> storage devices that can pass-through file system discard commands to the host operating system.</p>
<p>We didn't see any benefits right away, as the vms stayed defined in libvirt as their original machine types, either <code>pc-i440fx-3.1</code> or a type from the <code>pc-q35</code> family.</p>
<p>After returning home, we noticed that when we created a new capsul, it would come up as the <code>pc-i440fx-5.2</code> machine type and the main disk on the guest would display discard support in the form of a non-zero DISC-MAX size displayed by the <code>lsblk -D</code> command:</p>
<pre><code>localhost:~# sudo lsblk -D
NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
sr0         0        0B       0B         0
vda       512      512B       2G         0
</code></pre>
<p>Most of our capsuls were <code>pc-i440fx</code> ones, and we upgraded them to <code>pc-i440fx-5.2</code>, which finally got discards working for the grand majority of capsuls.</p>
<p>If you see discard settings like that on your capsul, you should also be able to run <code>fstrim -v /</code> on your capsul which saves us disk space on baikal:</p>
<pre><code>welcome, cyberian ^(;,;)^
your machine awaits

localhost:~# sudo lsblk -D
NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
sr0         0        0B       0B         0
vda       512      512B       2G         0

localhost:~# sudo fstrim -v /
/: 15.1 GiB (16185487360 bytes) trimmed
</code></pre>
<p>^ Please do this if you are able to!</p>
<p>You might also be able to enable an fstrim service or timer which will run fstrim to clean up and optimize your disk periodically.</p>
<p>However, some of the older vms were the <code>pc-q35</code> family of QEMU machine type, and while I was able to get one of ours to upgrade to <code>pc-i440fx-5.2</code>, discard support still did not show up in the guest OS. We're not sure what's happening there yet.</p>
<p>We also improved capsul's monitoring features; we began work on proper infrastructure-as-code-style diffing functionality, so we get notified if any key aspects of your capsuls are out of whack. In the past this had been an issue, with DHCP leases expiring during maintenance downtimes and capsuls stealing each-others assigned IP addresses when we turn everything back on.</p>
<p>capsul-flask now also includes an admin panel with 1-click-fix actions built in, leveraging this data:</p>
<p><a href="https://git.cyberia.club/cyberia/capsul-flask/src/commit/b013f9c9758f2cc062f1ecefc4d7deef3aa484f2/capsulflask/admin.py#L36-L202">https://git.cyberia.club/cyberia/capsul-flask/src/commit/b013f9c9758f2cc062f1ecefc4d7deef3aa484f2/capsulflask/admin.py#L36-L202</a></p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2021/12/admin-panel.jpg" class="kg-image" alt="Capsul - Rumors of my Demise have been Greatly Exaggerated"></figure><!--kg-card-begin: markdown--><p>I acknowledge that this is a bit of a silly system, but it's an artifact of how we do what we do. Capsul is always changing and evolving, and the web app was built on the idea of simply &quot;providing a button for&quot; any manual action that would have to be taken, either by a user or by an admin.</p>
<p>At one point, back when capsul was called &quot;cvm&quot;, <em>everything</em> was done by hand over email and the commandline, so of course anything that reduced the amount of manual administration work was welcome, and we are still working on that today.</p>
<p>When we build new UIs and prototype features, we learn more about how our system works, we expand what's possible for capsul, and we come up with new ways to organize data and intelligently direct the venerable virtualization software our service is built on.</p>
<p>I think that's what the &quot;agile development&quot; buzzword from professional software development circles was supposed to be about: freedom to experiment means better designs because we get the opportunity to experience some of the consequences before we fully commit to any specific design. A touch of humility and flexibility goes a long way in my opinion.</p>
<p>We do have a lot of ideas about how to continue making capsul easier for everyone involved, things like:</p>
<ol>
<li>
<p>Metered billing w/ stripe, so you get a monthly bill with auto-pay to your credit card, and you only pay for the resources you use, similar to what service providers like Backblaze do. (Note: of course we would also allow you to pre-pay with cryptocurrency if you wish)</p>
</li>
<li>
<p>Looking into rewrite options for some parts of the system: perhaps driving QEMU from capsul-flask directly instead of going through libvirt, and perhaps rewriting the web application in golang instead of sticking with flask.</p>
</li>
<li>
<p>JSON API designed to make it easier to manage capsuls in code, scripts, or with an infrastructure-as-code tool like Terraform.</p>
</li>
<li>
<p>IO throttling your vms: As I mentioned before, the vms wear out the disks fast. We had hoped that enabling discards would help with this, but it appears that it hasn't done much to decrease the growth rate of the smartmon wearout indicator metric.  So, most likely we will have to enforce some form of limit on the amount of disk writes your capsul can perform while it's running day in and day out.  80-90% of capsul users will never see this limit, but our heaviest writers will be required to either change thier software so it writes less, or pay more money for service. In any case, we'll send you a warning email long before we throttle your capsul's disk.</p>
</li>
</ol>
<p>And last but not least, Cybera Computer Club Congress voted to use a couple thousand of the capsulbux we've recieved in payment to purchase a new server, allowing us to expand the service ahead of demand and improve our processes all the way from hardware up.</p>
<p>(No tape this time!)</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><video autobuffer controls preload="auto" style="max-width: 100%;">
      <source src="https://picopublish.sequentialread.com/files/baikal2/baikal2.mp4" type="video/mp4">
      <source src="https://picopublish.sequentialread.com/files/baikal2/baikal2.webm" type="video/webm">
      <p>Your browser doesn't support HTML5 video. Here is
         a <a href="https://picopublish.sequentialread.com/files/baikal2/baikal2.mp4">link to the video</a> instead.</p>
</video><br><br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Shown: Dell PowerEdge R640 1U server with two 10-core xeon silver 4114 processors and 256GB of RAM. (Upgradable to 768GB!!)</p>
<hr>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="canihelp">Can I help?</h2>
<p>Yes! We are not the only ones working on capsul these days. For example, another group, <a href="https://coopcloud.tech">https://coopcloud.tech</a> has forked capsul-flask and set up thier own instance at</p>
<p><a href="https://yolo.servers.coop">https://yolo.servers.coop</a></p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2021/12/serverscoop.jpg" class="kg-image" alt="Capsul - Rumors of my Demise have been Greatly Exaggerated"></figure><!--kg-card-begin: markdown--><p>Thier source code repository is here (not sure this is the right one):</p>
<p><a href="https://git.autonomic.zone/3wordchant/capsul-flask">https://git.autonomic.zone/3wordchant/capsul-flask</a></p>
<p>Having more people setting up instances of capsul-flask really helps us, whether folks are simply testing or aiming to run it in production like we do.</p>
<p>Unfortunately we don't have a direct incentive to work on making capsul-flask easier to set up until folks ask us how to do it. Autonomic helped us a lot as they made thier way through our terrible documentation and asked for better organization / clarification along the way, leading to much more expansive and organized README files.</p>
<p>They also gave a great shove in the right direction when they decided to contribute most of a basic automated testing implementation and the beginnings of a JSON API at the same time. They are building a command line tool called abra that can create capsuls upon the users request, as well as many other things like installing applications. I think it's very neat :)</p>
<p>Also, just donating or using the service helps support cyberia.club, both in terms of maintaing capsul.org and reaching out and supporting our local community.</p>
<p>We accept donations via either a credit card (stripe) or in Bitcoin, Litecoin, or Monero via our BTCPay server:</p>
<p><a href="https://cyberia.club/donate">https://cyberia.club/donate</a></p>
<p>For the capsul source code, navigate to:</p>
<p><a href="https://git.cyberia.club/cyberia/capsul-flask">https://git.cyberia.club/cyberia/capsul-flask</a></p>
<p>As always, you may contact us at:</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><p>
    <a href="mailto:support@cyberia.club">support@cyberia.club</a>
</p><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Or on matrix:</p>
<p><code>#services:cyberia.club</code></p>
<p>For information on what matrix chat is and how to use it, see <a href="https://cyberia.club/matrix">https://cyberia.club/matrix</a></p>
<p>Previous post in this series: <a href="https://sequentialread.com/capsul-rollin-onwards-with-a-web-application/">Capsul Rollin' Onwards with a Web Application</a></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[🥳 Greenhouse Enters Alpha Test Phase!! 🎉]]></title><description><![CDATA[Besides that, I don't feel like writing any more description or introduction to what you are about to witness. [...]  enjoy the demo video, and please give me feedback if you decide to try out the service!]]></description><link>https://sequentialread.com/greenhouse-alpha/</link><guid isPermaLink="false">6175f7ef850dcb0001e863cb</guid><category><![CDATA[products]]></category><category><![CDATA[backend]]></category><category><![CDATA[cloud]]></category><category><![CDATA[desktop applications]]></category><category><![CDATA[golang]]></category><category><![CDATA[FLOSS]]></category><category><![CDATA[networking]]></category><category><![CDATA[Self-Hosting]]></category><category><![CDATA[windows]]></category><category><![CDATA[linux]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Mon, 25 Oct 2021 14:37:25 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2021/10/dancing-2.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html--><img style="float:right; max-width:25vw;" src="https://sequentialread.com/content/images/2021/10/laptop.png" alt="🥳 Greenhouse Enters Alpha Test Phase!! 🎉">

<div style="display: block; margin: 0; padding: 1em; padding-left: 1em; padding-left: 2em;  border-left: 5px solid #ccc; padding-bottom: 0.5em; line-height: 2em; color: #333; margin-bottom: 1em;"> <span style="font-style: italic; font-size:1.2em;">Say that you have a computer and it has internet access</span>
</div>
<img src="https://sequentialread.com/content/images/2021/10/dancing-2.png" alt="🥳 Greenhouse Enters Alpha Test Phase!! 🎉"><p><em>That computer can browse the web, but "the web" (or, web users around the globe) typically can't browse files or web applications running on that  computer. Publishing a server on the internet takes work, costs money, and requires maintenance and upkeep. It can be especially difficult if you want to use <strong>your</strong> computer, not rent someone else's. If you live in a dormitory or if you get internet access via wifi from your neighbor or via tethering to your phone, you may not be able to perform the necessary modifications to your router's configuration even if you knew how.</em></p>
<!--kg-card-end: html--><!--kg-card-begin: markdown--><p><em>Greenhouse provides an easy way to make one or more computers into servers that anyone in the world can connect to <strong>securely &amp; reliably</strong>, regardless of where or how the servers are connected to the internet.</em></p>
<p><em>Greenhouse is being designed from the ground up as a <strong>trustless service</strong>, that is, you don't have to trust me or whoever's running the service to keep your data secure — it's designed so it can't access your data in the first place.</em></p>
<p><em>For more information about why I am building this, see <a href="https://greenhouse.server.garden/">greenhouse.server.garden</a> and <a href="https://sequentialread.com/the-pragmatic-path-4-year-update-introducing-greenhouse/">The &quot;Pragmatic Path&quot; 4-Year Update: Introducing Greenhouse!</a></em></p>
<p><em>You may also check out the source code at <a href="https://git.sequentialread.com/forest/greenhouse">git.sequentialread.com/forest/greenhouse</a></em></p>
<p><em>Previous post in this series: <a href="https://sequentialread.com/greenhouse-update-4-september/">Greenhouse Development Update 4 - September</a></em></p>
<hr>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>Last time I mentioned that my todo list for releasing the alpha version had grown from 8 items to 24. Well, by now it's grown to 38! I've conducted preliminary usability tests, a pre-alpha release, adopted a mascot, and implemented telemetry.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html-->
<div style="display:flex; align-items:center; justify-content: flex-start;"> 
<span>It's time to, as the kids say, &nbsp;</span>


    <a href="https://greenhouse-alpha.server.garden/">
        <span>ship it</span> <img src="https://sequentialread.com/content/images/2021/10/shipit.png" alt="🥳 Greenhouse Enters Alpha Test Phase!! 🎉">
    </a>
</div>
<br>
<br><!--kg-card-end: html--><!--kg-card-begin: markdown--><p>Right now the service is <strong>free for early-adopters</strong>. Not free so I can hook you on it &amp; extract capital from you later, free because I haven't gotten around to implementing payments yet 😛</p>
<p>When I do, I'm planning on billing <strong>$0.01 per gigabyte of bandwidth</strong>, which is approximately what you would pay if you went with a provider like DigitalOcean and used about half of the bandwidth they allocated to your VPS (Virtual Private Server).  However, unlike the other providers, Greenhouse will have no minimum payment, so you only pay for the bandwidth you use. I estimated that this will make it up to a hundred times cheaper than a traditional VPS, practically free, for many use cases.</p>
<p>When compared to a similar cloud service, <a href="https://pagekite.net/signup/?more=bw">PageKite</a>:</p>
<table>
<thead>
<tr>
<th>Use Case</th>
<th>Monthly Bandwidth</th>
<th>Price w/ Greenhouse</th>
<th>Price w/ DigitalOcean</th>
<th>Price w/ PageKite</th>
</tr>
</thead>
<tbody>
<tr>
<td>blog, chat, online store</td>
<td>under 3 GB</td>
<td>$0.50 per <strong>year</strong></td>
<td>$5.00 per month</td>
<td>$4.00 per month</td>
</tr>
<tr>
<td>photos, podcast, music</td>
<td>15  GB</td>
<td>$0.15 per month</td>
<td>$5.00 per month</td>
<td>$5.99 per month</td>
</tr>
<tr>
<td>vlogging, streaming, more popular sites</td>
<td>210 GB</td>
<td>$2.10 per month</td>
<td>$5.00 per month</td>
<td>$49.99 per month</td>
</tr>
<tr>
<td>email server</td>
<td>Up to 500GB</td>
<td>$7.00 per month (planned)</td>
<td>$5.00 per month</td>
<td>Not offered</td>
</tr>
</tbody>
</table>
<p>Of course, these use cases and greenhouse prices are estimates. The actual bandwidth used may depend a lot on your server configuration, how you built your site/service, and of course how popular it is. As an anecdotal example, during the last month, my server used about 80GB of bandwidth.</p>
<p>Greenhouse also offers security and data ownership by default, plus a much easier setup process which works on <strong>Mac, Windows, and Linux</strong>, affording both a point and click interface and a CLI (Command Line Interface).</p>
<p>Support for hosting email servers hasn't landed in Greenhouse yet, but it is planned for the future. Unfortunately it will have to cost more, as it requires a dedicated IPV4 address. On the bright side, it'll be bundled with quite a lot of monthly bandwidth.</p>
<p>Besides that, I don't feel like writing any more description or introduction to what you are about to witness. You can check out <a href="https://greenhouse.server.garden/">greenhouse.server.garden</a> and some of my previous blog posts if you want that. Without further ado, enjoy the demo video, and please give me feedback if you decide to try out the service!</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><blockquote>
  <b id="demo">"As a user, I want my server to be online 😀"</b>
  <br><br>
<em>7 minutes 42 seconds</em>
</blockquote>
<br>
<div style="display: flex; justify-content: center; background-color: #aaa;">
  <video autobuffer controls preload="auto" style="max-height: 90vh;" width="100%">
    <source src="https://picopublish.sequentialread.com/files/greenhouse-alpha-demo-windows.mp4" type="video/mp4">
  <source src="https://picopublish.sequentialread.com/files/greenhouse-alpha-demo-windows.webm" type="video/webm">
  <p>Your browser doesn't support HTML5 video. Here is
     a <a href="https://picopublish.sequentialread.com/files/greenhouse-alpha-demo-windows.mp4">link to the video</a> instead.</p>
  </video>
</div>


<br>
<br>
<h2>Help me make self-hosting <span style="background-color: #d9fdff; padding: 3px; color:#432; font-weight: bold; border-radius: 3px; white-space: nowrap;">✨ <i><u style="text-decoration-thickness: 2px;">radically easier</u></i> ✨</span>!!</h2>


<br>
      <blockquote>
        <span style="background: #452775; display: inline-block; border-radius: 8px; padding: 0.2em 0.6em;">
          ⚗️🧪 <a href="https://greenhouse-alpha.server.garden/" style="color: #f6ff72; font-weight:bold; text-decoration: underline; text-decoration-thickness: 2px; ">greenhouse-alpha.server.garden</a>
        </span>
      </blockquote>
<br>
<figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2021/10/dancing-2.png" class="kg-image" alt="🥳 Greenhouse Enters Alpha Test Phase!! 🎉"></figure><!--kg-card-end: html-->]]></content:encoded></item><item><title><![CDATA[Greenhouse Update 4 - September]]></title><description><![CDATA[I managed to get the `greenhouse-daemon` (called `greenhouse-background-service` on windows 😇) installing as a windows service via the NSIS installer, ...]]></description><link>https://sequentialread.com/greenhouse-update-4-september/</link><guid isPermaLink="false">615681f65faf6e0001b3f21f</guid><category><![CDATA[Security]]></category><category><![CDATA[products]]></category><category><![CDATA[windows]]></category><category><![CDATA[desktop applications]]></category><dc:creator><![CDATA[Forest Johnson]]></dc:creator><pubDate>Fri, 01 Oct 2021 04:41:15 GMT</pubDate><media:content url="https://sequentialread.com/content/images/2021/10/aaaaaaaaaaaa2.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html--><div style="display: block; margin: 0; padding: 1em; padding-left: 1em; padding-left: 2em;  border-left: 5px solid #ccc; padding-bottom: 0.5em; line-height: 2em; color: #333; margin-bottom: 1em;">
    <span style="font-size:1.4em;">🤔</span> &nbsp; <span style="font-style: italic; font-size:1.2em;">Say that you have a computer and it has internet access</span>
</div>





<!--kg-card-end: html--><!--kg-card-begin: markdown--><img src="https://sequentialread.com/content/images/2021/10/aaaaaaaaaaaa2.png" alt="Greenhouse Update 4 - September"><p><em>That computer can browse the web, but &quot;the web&quot; (or, web users around the globe) typically can't browse files or web applications running on that  computer. Publishing a server on the internet takes work, costs money, and requires maintenance and upkeep. It can be especially difficult if you want to use <strong>your</strong> computer, not rent someone else's. If you live in a dormitory or if you get internet access via wifi from your neighbor or via tethering to your phone, you may not be able to perform the necessary modifications to your router's configuration even if you knew how.</em></p>
<p><em>Greenhouse provides an easy way to make one or more computers into servers that anyone in the world can connect to <strong>securely &amp; reliably</strong>, regardless of where or how the servers are connected to the internet.</em></p>
<p><em>Greenhouse is being designed from the ground up as a <strong>trustless service</strong>, that is, you don't have to trust me or whoever's running the service to keep your data secure — it's designed so it can't access your data in the first place.</em></p>
<p><em>For more information about why I am building this, see <a href="https://greenhouse.server.garden/">greenhouse.server.garden</a> and <a href="https://sequentialread.com/the-pragmatic-path-4-year-update-introducing-greenhouse/">The &quot;Pragmatic Path&quot; 4-Year Update: Introducing Greenhouse!</a></em></p>
<p><em>You may also check out the source code at <a href="https://git.sequentialread.com/forest/greenhouse">git.sequentialread.com/forest/greenhouse</a></em></p>
<p><em>Previous post in this series: <a href="https://sequentialread.com/greenhouse-update-3-august/">Greenhouse Development Update 3 - August</a></em></p>
<p><em>Next post in this series: <a href="https://sequentialread.com/greenhouse-alpha/">🥳 Greenhouse Enters Alpha Test Phase!! 🎉</a></em></p>
<hr>
<p>September is ending. <strong>Wake up!!!11!!!!!!!!</strong></p>
<p>No, seriously, as I write this, it's about 22 minutes until midnight, that time when the calendar rolls over to October 1st.</p>
<p>I spose I've delayed writing a Greenhouse update post for September until now because I was secretly hoping I could get the alpha version released this month. Well, it's definitely not happening at this point. However, at least I can say it's not for a lack of trying.</p>
<p>This month I tried to make a short list of tasks that I'd have to complete before releasing an alpha version of greenhouse. To me, alpha means that it's relatively unstable, it probably doesn't have very many users, and it's free; payments have not been implemented yet.</p>
<p>I believe I started out with a list of about 8 items. By the time I had completed half of them, the list had grown to 17 items 😛</p>
<p>Here is my todo list today:</p>
<pre><code>- [.] alpha test
  - [X] view logs from CLI
  - [X] view logs from desktop app
  - [X] delete tunnels from desktop app
  - [X] greenhouse webapp external domain support (greenhouse-alpha.server.garden)
  - [X] greenhouse webapp scheduled tasks: external domain validation, rebalance
  - [X] greenhouse webapp auto-start cloud-side and daemon
  - [X] greenhouse webapp general cleanup
  - [X] greenhouse internal how-to pages (using-your-own-domain-name-with-greenhouse)
  - [ ] greenhouse webapp alpha test invite/auth system
  - [X] desktop app listening port chooser
  - [X] desktop app file chooser
  - [ ] greenhouse daemon / cli / desktop app development environment quickstart docs
  - [X] Desktop app build / packaging Linux
  - [.] Desktop app build / packaging Windows
  - [.] Desktop app build / packaging MacOs
  - [X] curl | sh installation method for daemon/CLI on Linux
  - [ ] Desktop app installs background service on first run on MacOS
  - [X] Some sort of installation method for daemon/cli on windows?
  - [ ] Telemetry/monitoring: greenhouse
  - [ ] Telemetry/monitoring: greenhouse-desktop
  - [ ] Telemetry/monitoring: greenhouse-cli
  - [ ] Telemetry/monitoring: greenhouse-daemon
  - [ ] Telemetry/monitoring: threshold
</code></pre>
<p>By now it looks like it's grown to 24 items, but on the bright side, only 8 of them aren't started yet.</p>
<p>I wasn't sure just how quick-and-dirty I wanted the alpha version to be. For example, does &quot;alpha test version&quot; mean it only works on Linux? Does it mean the Windows and Mac implementations are there, but super hacky?</p>
<p>I spent a lot of my time this month grappling with these decisions. Often times I would investigate what work would actually be required &amp; attempt to decide based on the technical reality I was facing, rather than a theoretical estimate.</p>
<p>I think I've been erring on the side of &quot;do it right to begin with&quot; to some extent, at least where the amount of work associated with that decision is minimal.</p>
<p>Here's a brief list of decisions I've made so far:</p>
<ul>
<li>The linux installer for the alpha version will just be a shell script, aka the <a href="https://www.arp242.net/curl-to-sh.html">world-famous <code>curl ... | sudo sh</code> </a></li>
<li>The linux shell script installer will only support systemd-based linux distributions, however there will be reasonable DIY instructions for folks  who don't use systemd.</li>
<li>There are <a href="https://picopublish.sequentialread.com/files/greenhouse-install-linux-2">two different installers for linux</a>:
<ul>
<li>The shell script which installs the <a href="https://git.sequentialread.com/forest/greenhouse-daemon/">background service</a> and <a href="https://git.sequentialread.com/forest/greenhouse-cli/">command line tool</a></li>
<li>A source repo or debian package for the <a href="https://git.sequentialread.com/forest/greenhouse-desktop">desktop application</a>.</li>
</ul>
</li>
<li>Windows and Mac are fundamentally different from linux in terms of use case: On these operating systems, the desktop application is king, it is not optional. Everything else is based on the desktop application installation.</li>
<li>For the desktop application, I will leverage the <a href="https://build-system.fman.io">fman build system (fbs) Python/QT framework</a> and its installer support. I decided on this framework early on, because I wanted an alternative to <a href="https://www.electronjs.org/">electron</a>, something which could produce a more lightweight application experience while at the same time minimizing the pain associated with cross-platform development.
<ul>
<li>On linux, it generates <code>.deb</code> and <code>.rpm</code> packages, potentially others as well. I haven't looked into &quot;PPA&quot;-style ways of hosting these yet, for now they will be direct downloads.</li>
<li>On Windows, it creates an executable <a href="https://nsis.sourceforge.io/">Nullsoft Scriptable Installation System <code>NSIS</code></a> installer. This is actually quite nice, as far as I can tell this installer gives a great user experience when implemented correctly, but it also doesn't limit you in terms of what you can do.</li>
<li>On MacOS, it creates a <code>.dmg</code> disk image containing a <code>.app</code> package. The good news is, mac users love this and they know exactly what to do with it. The bad news is it's not an installer, so we don't get an opportunity to run a script while it's being installed.  I will probably end up building the daemon and CLI installer into the desktop app on MacOS; it won't install until the first time you launch the app.</li>
</ul>
</li>
<li>You will be able to use your own domain name with the alpha version, although this feature is going to be <a href="https://picopublish.sequentialread.com/files/greenhouse-domain-2">sort of hacky</a> for now. Most of the advanced DNS-related features will have to come later.</li>
</ul>
<hr>
<p>Here is a screenshot of the work-in-progress alpha version of the web application:</p>
<p><img src="https://git.sequentialread.com/forest/greenhouse/raw/commit/3439ba66db4c3e25db9cfbe4a860a0733b782c95/readme/screenshot.webp" alt="Greenhouse Update 4 - September"></p>
<p>I think I got a lot done this month, for one thing, I finished the <a href="https://git.sequentialread.com/forest/greenhouse-cli"><code>CLI</code> (command line interface)</a>.  Here's a breif overview of what it looks like:</p>
<pre><code>$ greenhouse

Usage: greenhouse COMMAND [...]

Commands: status, register, tunnel, ls, rm, logs

To read the instructions for a specific command, you can run for example:

greenhouse help register
greenhouse tunnel -h
greenhouse ls --help

Advanced users may wish to configure this cli with environment variables. 
For instructions on how to do that, run 'greenhouse help config'

$ greenhouse status

Greenhouse Daemon:
    Server Name: thingpad
    Configured Tunnels: 1

    Caddy Server Status:
        Enabled: true
        Running: true
        PID: 411094
        Up for 3m29s
        Health Check: not implemented yet

    Threshold Tunnel Client Status:
        Enabled: true
        Running: true
        PID: 411088
        Up for 3m29s
        Health Check: not implemented yet
 
Greenhouse Account:
    Email Address: forest.n.johnson@gmail.com
    Allocated Port Range: 10000-10019

    Authorized Domains: 
        - greenhouse-alpha.server.garden
        - forest.greenhouseusers.com

    Client States:
        greenhouse_internal_node:
             Current: ClientConnected
            Previous: ClientClosed
        thingpad:
             Current: ClientConnected
            Previous: ClientClosed

$ greenhouse tunnel

Error: expected at least 3 arguments for tunnel command, but only saw 0:

Usage: greenhouse tunnel LISTEN_URL to LOCAL_URL
   OR: greenhouse tunnel --json '{...}'

  Open a new tunnel. 
  LISTEN_URL supports the protocol schemes https://, http://, tls://, and tcp://.
  LOCAL_URL supports the protocol schemes http://, tcp://, and file://
	For details on the format that --json accepts, run 'greenhouse help json'

  e.g:
  greenhouse tunnel https://www.cli-user.greenhouseusers.com to http://localhost:80
  greenhouse tunnel https://files.cli-user.greenhouseusers.com to file:///home/cli-user/Public
  greenhouse tunnel tcp://:10014 to tcp://localhost:22
  greenhouse tunnel --json '{&quot;domain&quot;: &quot;cli-user.greenhouseusers.com&quot;, &quot;public_port&quot;: 443, ...}'
		
$ greenhouse tunnel https://nginx.forest.greenhouseusers.com to http://localhost:80

Now applying new tunnel configuration...

  - waiting for underlying services to start
  - creating threshold tunnels
  - testing threshold tunnels
  - configuring caddy
  - waiting for caddy to obtain https certificates from Let's Encrypt
  - final testing

Your tunnel was configured successfully!

</code></pre>
<p>I also made serious headway on getting the software built and deployed for all platforms. At this point, windows is most of the way there. <strong>¡Bienvenido a IIS!</strong></p>
<p><img src="https://sequentialread.com/content/images/2021/10/aaaaaaaaaaaa2.png" alt="Greenhouse Update 4 - September"></p>
<p>I managed to get the <code>greenhouse-daemon</code> (called <code>greenhouse-background-service</code> on windows 😇) installing as a windows service via the NSIS installer, and it even creates a special service user for that as well. I mainly wanted to do that to isolate greenhouse for its own protection; make it a bit harder for malware running on the windows machine to swipe the encryption keys that are used for TLS, for example. I'm still fairly green on file permissions / security on Windows, so if any of yall are Windows experts, I'd love to hear your thoughts in the comments!</p>
<p>There are still quite a few bugs and issues on Windows, but just the fact that I was able to get everything going via a single <code>.exe</code> installer is exciting!</p>
<p>I also got the desktop application running on MacOS, in the picture below I am using the <a href="https://snapcraft.io/install/sosumi/ubuntu">Sosumi snap</a> to virtualize MacOS on my linux workstation.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://sequentialread.com/content/images/2021/10/greenhouse-mac.png" class="kg-image" alt="Greenhouse Update 4 - September"></figure><!--kg-card-begin: markdown--><p>Here's the roadmap as it stands today. The main changes since last time; the cross platform installer feature was moved from the beta phase to the alpha phase, the CLI was finished, and a lot of other features went from &quot;MVP implemented&quot; to &quot;ship it&quot;.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><img src="https://picopublish.sequentialread.com/files/roadmap_sept2021.webp" alt="Greenhouse Update 4 - September"></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><em>See the previous post in this series: <a href="https://sequentialread.com/greenhouse-update-3-august/">Greenhouse Development Update 3 - August</a></em></p>
<p><em>Next post in this series: <a href="https://sequentialread.com/greenhouse-alpha/">🥳 Greenhouse Enters Alpha Test Phase!! 🎉</a></em></p>
<p><em>For more information about why I am building this, see <a href="https://greenhouse.server.garden/">greenhouse.server.garden</a> and <a href="https://sequentialread.com/the-pragmatic-path-4-year-update-introducing-greenhouse/">The &quot;Pragmatic Path&quot; 4-Year Update: Introducing Greenhouse!</a></em></p>
<p><em>You may also check out the source code at <a href="https://git.sequentialread.com/forest/greenhouse">git.sequentialread.com/forest/greenhouse</a></em></p>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>