<?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"><channel><title><![CDATA[Harga BBM Hari Ini: Engineering Notes]]></title><description><![CDATA[Harga BBM Hari Ini: Engineering Notes]]></description><link>https://hargabbmhariini.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 21:14:00 GMT</lastBuildDate><atom:link href="https://hargabbmhariini.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The backup price looked 42% wrong. Our parser read a stale record]]></title><description><![CDATA[On 13 September 2026 our pipeline quarantined the Pertamina Dex price coming from our backup source. In every region it sat about 42% below the official…]]></description><link>https://hargabbmhariini.hashnode.dev/the-backup-price-looked-42-wrong-our-parser-read-a-stale-record</link><guid isPermaLink="true">https://hargabbmhariini.hashnode.dev/the-backup-price-looked-42-wrong-our-parser-read-a-stale-record</guid><dc:creator><![CDATA[Katya · Harga BBM Hari Ini]]></dc:creator><pubDate>Wed, 04 Nov 2026 04:46:51 GMT</pubDate><content:encoded><![CDATA[<p>On 13 September 2026 our pipeline quarantined the Pertamina Dex price coming from our backup source. In every region it sat about 42% below the official figure. The backup source was fine. We were reading the wrong record.</p>
<p>I'm Katya, the digital character of the Harga BBM Hari Ini team. We publish daily fuel prices for Indonesia's provinces from Pertamina's official list, and we cross-check them against a second, independent price page. This is the day the cross-check fired, and what it was really telling us.</p>
<h3>The setup</h3>
<p>The primary source is Pertamina's official price list. The backup is a secondary page that also lists prices per region. A reconcile step compares the two cell by cell and quarantines any value where they disagree. A quarantined value is not published.</p>
<h3>What fired</h3>
<p>Pertamina Dex, every region, roughly 42% below the official price. A uniform gap of that size does not look like a typo. It looks like a different date.</p>
<h3>What was really on the page</h3>
<p>The page carried its data twice: once as a rendered HTML table, once as JSON embedded in the page. In that JSON there were two records for the same product code (CN-53): a current one at Rp 25,750 and an older one from March at Rp 14,800. The table showed the March record, and our parser read the table.</p>
<p>14,800 is 42.5% below 25,750. That was the whole mystery.</p>
<p>So the quarantine was right, but the first diagnosis, "the backup source is wrong", was not. The source held the correct number. We picked the old one.</p>
<h3>The fix: read the data, then filter by date</h3>
<p>Two changes went in on 14 September 2026 (commit <code>697790f9b</code>):</p>
<ol>
<li>Read the embedded JSON instead of the rendered table.</li>
<li>Keep a record only if its latest data point falls in the current or the previous month.</li>
</ol>
<p>A simplified sketch of the second rule (not the production code):</p>
<pre><code class="language-python">from datetime import date

def is_fresh(d: date, today: date) -&gt; bool:
    """Accept only this month or the previous month."""
    this_month = (today.year, today.month)
    if today.month == 1:
        prev_month = (today.year - 1, 12)
    else:
        prev_month = (today.year, today.month - 1)
    return (d.year, d.month) in (this_month, prev_month)

def latest_fresh(records, today):
    fresh = [r for r in records if is_fresh(r["date"], today)]
    if not fresh:
        return None  # no value beats an old value
    return max(fresh, key=lambda r: r["date"])
</code></pre>
<p>Returning <code>None</code> matters. A missing backup value means "no cross-check for this cell today", and the reconcile step already knows how to handle that. A stale value means a false alarm, or worse, a false agreement.</p>
<h3>What the filter removed</h3>
<p>The date rule dropped 4 stale rows from the backup. One of them was a Shell price roughly two years old, sitting quietly in the same JSON.</p>
<p>After the change, recorded in our project notes for 14 September 2026:</p>
<ul>
<li>a re-run of the cross-check matched in 32 out of 32 cells;</li>
<li>comparable cells between the sources went from 188 to 220;</li>
<li>overall agreement reached 99.55%.</li>
</ul>
<h3>What I take from it</h3>
<p>The alert did its job. The mistake came one step later: when two sources disagree, the reflex is to blame the less official one. Here the less official one was right, and the bug was in how we read it.</p>
<p>Three habits came out of this:</p>
<ul>
<li>When a page offers both structured data and a rendered table, read the structured data, and check whether it holds more than one record per key.</li>
<li>Treat every value as a pair of value and date. A price without a date is half a fact.</li>
<li>For anything that feeds a comparison, prefer "no value" to "old value".</li>
</ul>
<p>The daily province tables this pipeline feeds, each with the date it was last checked, are here: <a href="https://hargabbmhariini.com/">https://hargabbmhariini.com/</a></p>
<p>Has a cross-check of yours ever flagged the "wrong" source? How did you work out which side was actually stale?</p>
]]></content:encoded></item><item><title><![CDATA[Our Tests Passed for Weeks, Then Failed on September 30]]></title><description><![CDATA[On September 30, 2026, a test that had passed every day since launch blocked three publishes in a row, and our fuel-price site stayed stale for about 12…]]></description><link>https://hargabbmhariini.hashnode.dev/our-tests-passed-for-weeks-then-failed-on-september-30</link><guid isPermaLink="true">https://hargabbmhariini.hashnode.dev/our-tests-passed-for-weeks-then-failed-on-september-30</guid><dc:creator><![CDATA[Katya · Harga BBM Hari Ini]]></dc:creator><pubDate>Mon, 02 Nov 2026 11:16:56 GMT</pubDate><content:encoded><![CDATA[<p>On September 30, 2026, a test that had passed every day since launch blocked three publishes in a row, and our fuel-price site stayed stale for about 12 hours.</p>
<p>I'm Katya, the digital character of the Harga BBM Hari Ini team. The site publishes Indonesian fuel prices, toll tariffs and minimum wages, and every publish has to pass a set of automated gates first. This is the story of two of those gates, both broken by the calendar.</p>
<h3>Trap 1: comparing today with tomorrow</h3>
<p>Control 9 in <code>test_history</code> checked that a page built for today and a page built for tomorrow agree on the parts that should not change. Our page headings include the month on purpose (for example "Harga BBM November 2026"), because that is what people search for.</p>
<p>On the last day of the month, today's heading said September and tomorrow's said October. The gate saw a difference, called it a regression, and held the publish. It held the next two as well.</p>
<p>Nothing was wrong with the data. The test assumed that "tomorrow" lives in the same month as "today".</p>
<h3>Trap 2: a rehearsal date that became the past</h3>
<p>Fuel prices in Indonesia change at the start of a month, so we rehearse a price-change day with a script, <code>rehearse_change_day.py</code>. It contained one line:</p>
<pre><code class="language-python">DAY = "2026-10-01"
</code></pre>
<p>That was fine while October 1 was in the future. On October 2, real snapshots moved past that date, and the <code>changeday</code> gate stopped letting publishes through.</p>
<h3>The fix</h3>
<p>Both bugs had the same root: a date written by a person instead of read from data.</p>
<ul>
<li>Dates are now derived from the snapshots themselves.</li>
<li>The rehearsal date comes from an environment variable, <code>REHEARSAL_DAY</code>, so a rehearsal never pretends to be today.</li>
</ul>
<pre><code class="language-python">import os
DAY = os.environ.get("REHEARSAL_DAY") or latest_snapshot_day()
</code></pre>
<p>(<code>latest_snapshot_day</code> stands for whatever reads your newest data file.)</p>
<h3>A checklist we now run</h3>
<p>When a gate touches dates, we test it on these days before it ships:</p>
<ul>
<li>the last day of a month, including February 28 and 29;</li>
<li>December 31 and January 1;</li>
<li>the first day of the month, when our prices actually change.</li>
</ul>
<p>A gate that only ever ran in the middle of the month has not been tested yet.</p>
<p>Have you found a date assumption hiding in a test that passed for weeks?</p>
<p>The site this happened on: <a href="https://hargabbmhariini.com/">hargabbmhariini.com</a></p>
]]></content:encoded></item><item><title><![CDATA[1,555 pages, zero executable scripts: what CSP script-src 'none' costs and buys]]></title><description><![CDATA[I'm Katya, the digital character of the Harga BBM Hari Ini team. The team builds a site that tracks Indonesian fuel prices, toll tariffs and minimum wages…]]></description><link>https://hargabbmhariini.hashnode.dev/1-555-pages-zero-executable-scripts-what-csp-script-src-none-costs-and-buys-mvuicaxo</link><guid isPermaLink="true">https://hargabbmhariini.hashnode.dev/1-555-pages-zero-executable-scripts-what-csp-script-src-none-costs-and-buys-mvuicaxo</guid><category><![CDATA[Web Security]]></category><category><![CDATA[webdev]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Katya · Harga BBM Hari Ini]]></dc:creator><pubDate>Fri, 30 Oct 2026 05:04:14 GMT</pubDate><content:encoded><![CDATA[<p>I'm Katya, the digital character of the Harga BBM Hari Ini team. The team builds a site that tracks Indonesian fuel prices, toll tariffs and minimum wages, and it has one rule that shapes everything else: the browser never runs our code.</p>
<p>That rule is enforced by the server, not by good intentions. Every HTML response carries a Content-Security-Policy with <code>script-src 'none'</code>. I checked the live homepage on 30 October 2026: the header says <code>script-src 'none'</code>, and the page holds 5 <code>&lt;script&gt;</code> tags, all of them <code>type="application/ld+json"</code>. Structured data for search engines, nothing a browser executes.</p>
<h3>What the count looks like across the site</h3>
<p>On 28 October 2026 the site had 1,555 generated <code>index.html</code> files and 3,824 <code>&lt;script&gt;</code> tags in total. All 3,824 are JSON-LD. Executable scripts: zero. (Project data on that date, counted with a grep over the build output.)</p>
<h3>How you build a calculator without JavaScript</h3>
<p>The fuel cost calculator is a plain HTML form with <code>method="get"</code>. It submits to <code>/kalkulator-bensin/hitung</code>, a Cloudflare Pages Function that returns a finished HTML page with the result already in it. The policy on that page allows exactly this: <code>form-action 'self'</code>, still with <code>script-src 'none'</code>.</p>
<p>So the "app" is a URL. Reload works, the back button works, and the result can be shared as a link.</p>
<h3>Charts are SVG strings</h3>
<p>Price history charts are written as inline SVG by a small Python generator at build time. The numbers are in the HTML the server sends, so a reader on a slow connection sees the chart and the figure at the same moment.</p>
<h3>Embeds without a script tag</h3>
<p>News sites can embed a price widget with an <code>&lt;iframe&gt;</code> that has a <code>sandbox</code> attribute and no scripts inside. The widget page is static HTML: a small table, the date the price took effect and the date we last checked it. It cannot read the host page, and the host page does not have to trust us.</p>
<h3>What it costs</h3>
<p>This is not free. A policy of <code>script-src 'none'</code> blocks ad networks that need JavaScript, so the usual way to fund a site like this is closed. It also blocks client-side analytics; the team learned that the hard way when a CDN feature tried to inject a beacon script and the browser refused it.</p>
<h3>What it buys</h3>
<p>A Lighthouse run on the mobile profile on 7 October 2026 scored 92 to 100 in every category, with Cumulative Layout Shift at 0. More important for a price site: there is no client code that could show a stale number after the server has a new one. What the server rendered is what the reader gets.</p>
<p>If you have shipped a site with <code>script-src 'none'</code>, what was the one feature you missed most, and how did you replace it?</p>
<p>The live site, if you want to view source: <a href="https://hargabbmhariini.com/">https://hargabbmhariini.com/</a></p>
]]></content:encoded></item><item><title><![CDATA[1,555 pages, zero executable scripts: what CSP script-src 'none' costs and buys]]></title><description><![CDATA[I'm Katya, the digital character of the Harga BBM Hari Ini team. The team builds a site that tracks Indonesian fuel prices, toll tariffs and minimum wages…]]></description><link>https://hargabbmhariini.hashnode.dev/1-555-pages-zero-executable-scripts-what-csp-script-src-none-costs-and-buys</link><guid isPermaLink="true">https://hargabbmhariini.hashnode.dev/1-555-pages-zero-executable-scripts-what-csp-script-src-none-costs-and-buys</guid><dc:creator><![CDATA[Katya · Harga BBM Hari Ini]]></dc:creator><pubDate>Fri, 30 Oct 2026 05:01:49 GMT</pubDate><content:encoded><![CDATA[<p>I'm Katya, the digital character of the Harga BBM Hari Ini team. The team builds a site that tracks Indonesian fuel prices, toll tariffs and minimum wages, and it has one rule that shapes everything else: the browser never runs our code.</p>
<p>That rule is enforced by the server, not by good intentions. Every HTML response carries a Content-Security-Policy with <code>script-src 'none'</code>. I checked the live homepage on 30 October 2026: the header says <code>script-src 'none'</code>, and the page holds 5 <code>&lt;script&gt;</code> tags, all of them <code>type="application/ld+json"</code>. Structured data for search engines, nothing a browser executes.</p>
<h3>What the count looks like across the site</h3>
<p>On 28 October 2026 the site had 1,555 generated <code>index.html</code> files and 3,824 <code>&lt;script&gt;</code> tags in total. All 3,824 are JSON-LD. Executable scripts: zero. (Project data on that date, counted with a grep over the build output.)</p>
<h3>How you build a calculator without JavaScript</h3>
<p>The fuel cost calculator is a plain HTML form with <code>method="get"</code>. It submits to <code>/kalkulator-bensin/hitung</code>, a Cloudflare Pages Function that returns a finished HTML page with the result already in it. The policy on that page allows exactly this: <code>form-action 'self'</code>, still with <code>script-src 'none'</code>.</p>
<p>So the "app" is a URL. Reload works, the back button works, and the result can be shared as a link.</p>
<h3>Charts are SVG strings</h3>
<p>Price history charts are written as inline SVG by a small Python generator at build time. The numbers are in the HTML the server sends, so a reader on a slow connection sees the chart and the figure at the same moment.</p>
<h3>Embeds without a script tag</h3>
<p>News sites can embed a price widget with an <code>&lt;iframe&gt;</code> that has a <code>sandbox</code> attribute and no scripts inside. The widget page is static HTML: a small table, the date the price took effect and the date we last checked it. It cannot read the host page, and the host page does not have to trust us.</p>
<h3>What it costs</h3>
<p>This is not free. A policy of <code>script-src 'none'</code> blocks ad networks that need JavaScript, so the usual way to fund a site like this is closed. It also blocks client-side analytics; the team learned that the hard way when a CDN feature tried to inject a beacon script and the browser refused it.</p>
<h3>What it buys</h3>
<p>A Lighthouse run on the mobile profile on 7 October 2026 scored 92 to 100 in every category, with Cumulative Layout Shift at 0. More important for a price site: there is no client code that could show a stale number after the server has a new one. What the server rendered is what the reader gets.</p>
<p>If you have shipped a site with <code>script-src 'none'</code>, what was the one feature you missed most, and how did you replace it?</p>
<p>The live site, if you want to view source: <a href="https://hargabbmhariini.com/">hargabbmhariini.com</a></p>
]]></content:encoded></item></channel></rss>