<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Debugging on blog.taikiito.com</title><link>https://blog.taikiito.com/en/tags/debugging/</link><description>Recent content in Debugging on blog.taikiito.com</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Sat, 22 Aug 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.taikiito.com/en/tags/debugging/index.xml" rel="self" type="application/rss+xml"/><item><title>I Built This Blog a Hatena-Style Calendar, and Locked My Own JSON In a Box</title><link>https://blog.taikiito.com/en/posts/2026-08-22-sidebar-calendar/</link><pubDate>Sat, 22 Aug 2026 09:00:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-22-sidebar-calendar/</guid><description>A record of building a Hatena-Blog-style calendar, tag cloud, and monthly jump buttons into this blog&amp;rsquo;s own sidebar - including tracking down why the calendar silently failed to render, caused by Hugo&amp;rsquo;s html/template auto-escaping mangling an inline JSON blob.</description><content:encoded><![CDATA[<p>Last time, I wrote about shipping a Singlish glossary site alongside this blog. Back to the blog itself this time - a record of building its sidebar.</p>
<h2 id="the-post-list-and-archive-page-werent-quite-enough">The post list and archive page weren&rsquo;t quite enough</h2>
<p>This blog runs on Hugo + PaperMod. At first the header just had two menu items: &ldquo;Posts&rdquo; and &ldquo;Archive.&rdquo; As the post count grows, being able to browse by date or tag helps. PaperMod is a single-column theme though - there&rsquo;s no concept of a sidebar built in. The content column is fixed at a max width (<code>--nav-width: 1024px</code>) and centered, so on a wide screen there&rsquo;s blank space on either side. I figured something could go there.</p>
<h2 id="first-pass-just-two-buttons">First pass: just two buttons</h2>
<p>PaperMod ships an empty hook partial called <code>extend_footer.html</code> - override it and you can inject arbitrary HTML at the end of every page. There&rsquo;s also a convention where any CSS file dropped in <code>assets/css/extended/*.css</code> gets auto-concatenated into the theme&rsquo;s stylesheet. Using both, I added two fixed pill buttons in the right margin: &ldquo;📅 By date&rdquo; linking to <code>/archives/</code>, and &ldquo;🏷️ By tag&rdquo; linking to <code>/tags/</code>. Just shortcuts, nothing more.</p>
<h2 id="looking-at-hatena-blog-i-decided-to-rebuild-it">Looking at Hatena Blog, I decided to rebuild it</h2>
<p>Plain buttons felt thin. Hatena Blog&rsquo;s sidebar has an actual month-grid calendar with post days highlighted, and a tag cloud with names and counts sitting right there. I wanted that instead.</p>
<p>The tag cloud was easy - PaperMod&rsquo;s own <code>/tags/</code> page (<code>taxonomy.html</code>) already does exactly this, so I borrowed its logic directly:</p>
<pre tabindex="0"><code class="language-gotemplate" data-lang="gotemplate">{{ range $tagsPage.Data.Terms.Alphabetical }}
  {{ with site.GetPage (printf &#34;/tags/%s&#34; .Name) }}
    &lt;a href=&#34;{{ .Permalink }}&#34;&gt;{{ .LinkTitle }} ({{ $count }})&lt;/a&gt;
  {{ end }}
{{ end }}
</code></pre><p>The calendar took more thought since this is a static site generator. At build time, I embed each language&rsquo;s post dates, titles, and URLs as JSON inside the page (<code>{{ $calItems | jsonify }}</code>), then render the month grid with plain vanilla JS. No external library.</p>
<h2 id="the-calendar-rendered-nothing">The calendar rendered nothing</h2>
<p>I shipped it, and the tag cloud showed up fine - but the calendar area was just blank. The prev/next (‹ ›) buttons appeared; no day grid, no month title.</p>
<p>Checking the actual rendered HTML in devtools, I found this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;application/json&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;quick-cal-data&#34;</span><span class="p">&gt;</span><span class="s2">&#34;[{\&#34;date\&#34;:\&#34;2026-08-21\&#34;,\&#34;...\&#34;}]&#34;</span><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>The whole array had been escaped and wrapped in quotes <strong>as a string</strong>. Feed that into <code>JSON.parse()</code> and you get back a single string, not an array. The <code>.forEach()</code> right after it obviously doesn&rsquo;t exist on a string, so it threw and halted the rest of the script.</p>
<p>The cause was Hugo&rsquo;s template engine (<code>html/template</code>). When you interpolate a value inside a <code>&lt;script&gt;</code> tag, Hugo automatically treats it as untrusted and escapes it as a JS string literal. <code>jsonify</code> returns a plain <code>string</code>, so Hugo wrapped the whole thing in quotes rather than trusting it as raw JS.</p>
<p>The fix was one line - pipe it through <code>safeJS</code> to tell Hugo explicitly that this JS is safe to pass through as-is:</p>
<pre tabindex="0"><code class="language-gotemplate" data-lang="gotemplate">{{ $calItems | jsonify | safeJS }}
</code></pre><p>With that, the JSON landed as a real array again, and the calendar started working.</p>
<h2 id="adding-monthly-jump-buttons">Adding monthly jump buttons</h2>
<p>Once the calendar worked, I wanted a way to jump straight to a given month instead of stepping through prev/next one month at a time - which gets tedious as posts accumulate. I added a list of every year-month with at least one post, newest first; clicking one jumps the calendar there in place, no page reload, reusing the same state the prev/next buttons already manage.</p>
<h2 id="where-things-stand">Where things stand</h2>
<p>This post ends up being a record of the blog fixing itself while I was building it. Calendar, monthly buttons, and tag cloud are all in the sidebar now - next up is actually writing more posts.</p>
<p>To be continued.</p>
]]></content:encoded></item><item><title>After Making MulmoClaude Run 24/7, Its Background Automation Started Looping Forever</title><link>https://blog.taikiito.com/en/posts/2026-08-17-journal-loop-bug/</link><pubDate>Mon, 17 Aug 2026 09:00:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-17-journal-loop-bug/</guid><description>The day after moving to an always-on server, a background automation task (the journal feature) started endlessly retrying the same sessions. The root cause: a millisecond-precision mismatch between Python and Node.js timestamps.</description><content:encoded><![CDATA[<p>Last time I wrote about moving MulmoClaude to an always-on AWS Lightsail server. The migration itself went fine - but the next day, I noticed something was off with the automation running behind the scenes.</p>
<h2 id="what-was-happening">What was happening</h2>
<p>MulmoClaude has a background feature called <code>journal</code> that auto-generates daily summaries from chat logs. It started taking an abnormally long time every time it ran hourly - some runs took 25 to 90 seconds. It felt like something was retrying over and over, and token usage was climbing noticeably.</p>
<h2 id="tracking-it-down">Tracking it down</h2>
<p>Digging in, I found that a large batch of sessions (625 of them) generated during the migration work (see last post) had been left unprocessed. The hourly job kept trying to process the entire backlog every single run, and kept failing to finish - so the next hour it would try the exact same backlog again. Close to an infinite loop.</p>
<p>Digging further, there was a deeper root cause underneath. The logic that checks whether a session has &ldquo;already been processed&rdquo; compares timestamps, and the Python side and the Node.js side handled milliseconds slightly differently - one truncating, the other rounding. That mismatch meant sessions that should have counted as &ldquo;already processed&rdquo; kept looking &ldquo;unprocessed&rdquo; every single time. A few attempted fixes didn&rsquo;t stick, because none of them addressed the mismatch itself.</p>
<h2 id="stopgap-then-the-real-fix">Stopgap, then the real fix</h2>
<p>First I turned the <code>journal</code> feature off entirely to stop the bleeding. Then I fixed the timestamp handling so both languages treat precision the same way. After that, I confirmed <code>daysSkipped: 0</code> and the hourly check dropped to 2-3 milliseconds.</p>
<p>I also revisited the schedule: <code>journal</code> is now pinned to run once a day (<code>dailyIntervalHours: 24</code>), and the hourly scheduler check itself stays lightweight.</p>
<h2 id="what-i-took-from-this">What I took from this</h2>
<p>&ldquo;Always-on&rdquo; turned out to mean more than just the app staying up - the quiet automation running behind it has to not break either. The most expensive thing was happening somewhere completely invisible from the front end.</p>
<p>To be continued.</p>
]]></content:encoded></item><item><title>Moving to a Server That Never Turns Off</title><link>https://blog.taikiito.com/en/posts/2026-08-16-moving-to-a-server/</link><pubDate>Sun, 16 Aug 2026 09:00:00 +0800</pubDate><guid>https://blog.taikiito.com/en/posts/2026-08-16-moving-to-a-server/</guid><description>Migrating MulmoClaude to AWS Lightsail. A silent CSRF trusted-origin failure, a mystery &amp;lsquo;Network error calling &amp;hellip;&amp;rsquo; traced to a Docker Desktop vs Docker Engine networking difference, and a memory-plan saga.</description><content:encoded><![CDATA[<p>Continuing from before. I&rsquo;d been running MulmoClaude on my local MacBook, which meant it stopped working whenever I closed the lid or took it with me. I increasingly wanted proper web UI access while out and about, away from home. The Telegram bot couldn&rsquo;t handle multiple threads well and its Markdown rendering was weak, so I was starting to feel its limits.</p>
<h2 id="weighing-the-options">Weighing the options</h2>
<p>Leaving my laptop on 24/7 was ruled out on electricity and portability grounds. Connecting directly via SSH from my own machine was ruled out early too - corporate networks commonly block outbound port 22 (SSH). Instead I went with HTTPS over a domain, fronted by an authentication gate (Cloudflare Access). Login is via Google account, plus the app&rsquo;s own shared secret (<code>MULMOCLAUDE_AUTH_TOKEN</code>) as a second layer.</p>
<p>I went back and forth with Sakura Cloud, but chose AWS Lightsail for the lower latency from its Singapore region. Started on the $7/mo bundle (1GB RAM, 2 vCPUs, 40GB SSD), planning to upgrade later if it wasn&rsquo;t enough.</p>
<h2 id="a-silent-failure-the-csrf-trusted-origin-gap">A silent failure: the CSRF trusted-origin gap</h2>
<p>The migration itself went through, but sending a message from the browser just silently failed - no error, no response.</p>
<p>The cause: the app&rsquo;s built-in CSRF guard checks the <code>Origin</code> header, and the new domain hadn&rsquo;t been added to <code>MULMOCLAUDE_TRUSTED_ORIGINS</code> in <code>.env</code>. Adding <code>https://mulmoclaude-taiki.com</code> there and running <code>systemctl restart mulmoclaude.service</code> fixed it. If you&rsquo;re self-hosting MulmoClaude and sends silently fail only from your public domain, check this first.</p>
<h2 id="chasing-down-network-error-calling-">Chasing down &ldquo;Network error calling &hellip;&rdquo;</h2>
<p>A few days later, internal MCP-bridge calls like <code>presentForm</code> and <code>manageCollection</code> started intermittently failing with &ldquo;Network error calling &hellip;: fetch failed.&rdquo;</p>
<p>My first suspicion was memory pressure. <code>/proc/meminfo</code> did show low <code>MemAvailable</code>, with swap mostly used. I installed <code>zram-tools</code> for compressed swap (<code>/dev/zram0</code>) and dropped <code>vm.swappiness</code> to 10 - no change. I even bumped the Lightsail plan from $7/mo (1GB) to $12/mo (2GB) just in case, and the error still reproduced 100% of the time.</p>
<p>The real cause turned up when I read the app&rsquo;s source (<code>server/index.ts</code>): it&rsquo;s designed to <strong>bind only to loopback</strong> - <code>app.listen(port, &quot;127.0.0.1&quot;, ...)</code>. The sandboxed agent reaches the host via <code>host.docker.internal</code>. On <strong>Docker Desktop (Mac)</strong>, that resolves through a hypervisor proxy that does reach loopback. On <strong>Docker Engine (Linux)</strong>, it just resolves to the real IP of the <code>docker0</code> bridge (<code>172.17.0.1</code> in this case) - a genuinely different interface, which a loopback-only bind will never accept a connection from. A latent bug that never surfaced on the Mac, exposed for the first time on Linux.</p>
<p>The fix was a single <code>socat</code> relay bridging the docker-bridge address to loopback:</p>
<pre tabindex="0"><code>ExecStart=/usr/bin/socat TCP-LISTEN:3001,bind=172.17.0.1,fork,reuseaddr TCP:127.0.0.1:3001
</code></pre><p>Registered as a systemd unit, and <code>curl http://host.docker.internal:3001/</code> immediately started returning 200, with <code>presentForm</code>/<code>manageCollection</code> working right away. The memory/zram work wasn&rsquo;t entirely wasted (headroom genuinely improved), but it was never the actual cause of this error.</p>
<h2 id="why-i-ended-back-on-12mo-2gb">Why I ended back on $12/mo (2GB)</h2>
<p>Once the real cause was fixed, I tried downgrading back to $7/mo (1GB) to save money. Lightsail, though, <strong>doesn&rsquo;t support resizing an instance down via snapshot</strong> - it meant rebuilding a 1GB instance from scratch. That rebuilt instance repeatedly hung under load (like building the Docker sandbox image), with SSH and even Lightsail&rsquo;s own browser console becoming unresponsive, more than once.</p>
<p>I concluded 1GB can&rsquo;t reliably sustain this workload (MulmoClaude itself, the Docker sandbox, and the Telegram bridge together) and settled on $12/mo (2GB) as the final plan. Stability won over saving a few dollars.</p>
<h2 id="running-247">Running 24/7</h2>
<p>A few stumbles along the way, but this server has been running around the clock ever since. I can use it anytime, from anywhere, regardless of what state my own laptop is in.</p>
<p>To be continued.</p>
]]></content:encoded></item></channel></rss>