How the Board
Was Built
Thirty-seven days. 135 commits. One human author, half a dozen AI coding agents, and a bot that bakes fallback snapshots at 06:15 every morning. Also one rule, broken by nearly every one of them at least once: never let a guess pass for a reading.
01How to read this page
A dashboard is easy. An honest one is not. This page tells the story of the second kind, in order.
Each era answers three questions. What did it add? What did it break? What did it teach? The lessons are not decoration. Every one became a written rule in the project's instructions, a test that fails if the mistake returns, or both. Commit links go to the exact change, so nothing here has to be taken on trust.
One vocabulary runs through the whole history. An observed value is something a station reported. A modelled value is something a weather or air-quality model estimated. A reference value is a dated dataset, such as the 2020 census. A derived value is arithmetic on the others. Most of the bugs below are a value wearing the wrong one of these four labels.
02Ten eras
I · Day one: a live national twin 23 September
Added. The board went live on its first day: national map, live feeds, three languages from the first commit (232faa7). Relays moved behind Cloudflare Pages Functions the same day (663184c). By evening, version 2 had seven lenses from nation to kampung, reach analysis, sea and air traffic, and satellites (cc55622). The daily snapshot bot started the next morning.
Broke. A folder of temporary build files was committed in hour one and removed in hour two (7a262e3). Hygiene was a theme from the start.
II · The design wars 24–25 September
Added. Version 3 rebuilt the board as five views derived from a user manual, on keyless base maps (85fbff3). Local briefings arrived (551a9bb), as did Himawari-9, vegetation and lightning layers, and the first research page (069ec09).
Taught. In one day the typeface changed from IBM Plex to Plus Jakarta Sans to a Newsreader serif to the Axiom design core. Every pass subtracted something: a drop shadow, an inset, a radius, a brand mark. Taste converges by removal. The commit titles say it best: "kill the last drop-shadows."
III · From cards to joins 26–28 September
Added. An ASEAN clock and market ticker. An insights panel. Then the first real idea: a ranked flood action queue that weighs converging evidence, remembers how fast water climbs, and names the districts no gauge is watching (c4e2b08). Version 4 added a states × layers matrix and cross-layer findings with forest loss (156d19a).
Broke. A boot crash, and browsers holding stale code. The fix was a version number appended to every import and bumped by hand: 12, then 14, then 18. Hand-bumped tokens would take three more weeks to kill.
IV · Kuala Lumpur in three dimensions 29 September – 3 October
Added. A 3D Kuala Lumpur with MapLibre building extrusions and a 1:1 Minecraft world built with Arnis (b4aeda5), then 2,100 buildings on real terrain (67d898a). A civic dashboard design (0dd7eb2). Paid data relays went behind operator authorisation and a rate limiter (f0d4ccd).
Taught. The first credibility audit (0dcf006) wrote down the principle the rest of the history enforces: keep observation age, lineage and limits visible.
V · The quiet week 4–10 October
Seven commits, all by the bot. The board ran, the snapshots refreshed, and nobody touched the code. By the standards of what came next, the most stable week in the project's life was the one nobody wrote anything.
VI · The haze 11–14 October
Added. Malaysia was under haze, and the board turned to face it.
- Air quality: observed Department of Environment (DOE) stations replaced modelled air as the headline (70653b3).
- Haze response: who is breathing it, which official school or outdoor line each district has crossed, and a sitrep a duty officer can forward on WhatsApp (28e8989).
- Three brainchildren: SLIC city scores, a carbon footprint and a news digest that corroborates across outlets (1d9f329).
- The KL operations centre (0c3722a), with building massing ported from the Bangkok atlas (7d03be3).
- Official statistics: income, prices, labour and public finance, each with its date (0f9e30d).
Broke. Two things, both on the KL page.
- Typed-in readings. The page shipped values typed into the code and shown as live: "DOE CAQM: 52", "JPS RIVERS: NORMAL FLOW", river gauges frozen in alert. Its live wiring read the wrong field names, so the calm placeholders never changed.
- An invented city. 4,171 computer-generated rectangles, laid out on a grid as corridor filler, were drawn over satellite imagery as if they were buildings. The owner looked at the screenshot and asked whether it was Peter Eisenman's Holocaust memorial in Berlin. It was not Kuala Lumpur, which was the problem.
Both were fixed the same day: readings now come only from live relays (cec6992), and only real footprints are drawn (a4b9405).
Taught. A screen does not get to say "now" unless a relay said it, and recently. It also produced a decision recorded for good: no physics simulator belongs on this board (5ce22ce). A two-day-old simulation next to a live reading is a lie by adjacency.
VII · One nation, two views 15 October
Added.
- Automatic cache token: the hand-bumped cache tokens died. One release id is now hashed from the code itself (d76c35f).
- National 3D:
/klbecame a 3D view of the whole nation, and the operations centre moved to/kl-ioc(b2c836a). George Town streams in beside Kuala Lumpur (2c52934). - Two views, one instrument: 3D became the default, and the 2D and 3D maps now share one instrument (f45d142). Switching views keeps your place, your language and the briefing.
- Dated earth observations: surface heat, soil moisture, rainfall and others (03b0f93).
VIII · Hardening 16–17 October
Added. A full-system audit by four parallel reviewers (65491fe):
- Live river-station names were being written into the KL page unescaped; they are now escaped.
- Every page got a content security policy, and every external script an integrity hash.
- Data relays accept only the parameters they use, so nobody can force them to re-download upstream data on every request.
- A dead hosting configuration that would have exposed the relays without their access gate was deleted.
A runtime follow-up found that phones threw 75 map errors per lens change, and that a city score's access pillar had been blank for all 21 cities (b683fdd). Deploys gained a guard that refuses a stale or dirty copy of the code (bfe5e1e).
Broke, and taught. When a relay failed, it answered with an honest error, and Cloudflare replaced every 5xx answer with its own blank page. For weeks, every honest error the board wrote was undeliverable. Relays now answer 424, which arrives intact (3f3f181). Being honest is not enough. The honesty also has to reach the reader.
IX · antiSHism: the map takes the screen back 19–23 October
The owner gave the problem a name. Spatial hallucination is when an interface invents a reason for things to occupy space. The test: why is this here, why this size, and why this far from that?
- Map space: nine national readings moved from a permanent band above the map into the briefing rail. The map at 1280×720 grew from 484 to 548 pixels (e345bb4, c7811e1). The map key, lens label and rail sections were measured and re-placed so nothing hides behind a scrollbar (2070896).
- Derived estimates: how fast a river is rising, how hot it feels, and how many people face flood and haze at once, each labelled as arithmetic on readings (9b210c7). The same change fixed findings that misstated their own numbers: "29 districts worst on all six layers" was true of 3.
- Rain units: rainfall turned out to be a 15-minute total, not an hourly rate. For months the board had understated it fourfold (c7d26cd).
- Visible release id: the release id became visible in the Menu and footer (3a2111b).
- Shipability: deploys now ship only reviewed files and check that every file the pages request exists. They then wait until the live site actually serves the new release before reporting success (f34c306, 616d6e7, aae2906).
X · Now 24–29 October
Added. Evidence screenshots must be the file type their names claim, checked byte by byte (5917025). The KL page gained a clock and market strip (bf63a6e). This page.
Broke. Two things, caught before release.
- Market strip. It painted every quote green whatever its age. Quotes now show their age and turn green only when fresh.
- The live-release check. One of its tests was quietly cancelled, so the full test run failed and the deploy guard refused to ship
main. The check now uses a timer that cannot be dropped, and the guard passes again.
03What the board can do now
Every capability carries its evidence tier. If a row says "model", the screen says "model" too.
| Capability | Since | Evidence tier |
|---|---|---|
| River gauges: counted as current only within 45 minutes; old ones go grey, never green | 25 Sep | observed |
| Flood action queue, climb memory, districts no gauge watches | 27 Sep | observed × reference |
| Reach analysis: people more than 20 km from a hospital, by age group | 23 Sep | reference (dated) |
| States × layers matrix and cross-layer findings, each labelled by tier | 28 Sep · 19 Oct | reference · observed · model |
| KL in 3D with real footprints, setback massing and an Arnis world | 29 Sep · 14 Oct | reference; massing is a stylisation |
| DOE air-quality stations, worst current value with coverage | 13 Oct | observed index (not PM2.5) |
| Haze response: official lines met by district, forwardable sitrep in three languages | 13–14 Oct | observed × reference |
| Fine-particle (PM2.5) and rain estimates from air-quality and weather models | 23 Sep | model, labelled, ≤2 h old |
| SLIC city slice, state carbon stock, corroborated news digest | 14 Oct | mixed, each row labelled |
| Official income, prices, labour and public finance | 14 Oct | published statistics, dated |
| National 3D scene with George Town; one shared 2D/3D instrument | 15 Oct | reference geometry × observed stations |
| Dated satellite products, which say so when they fail | 15–16 Oct | satellite products |
| River rise rate, heat index, people under two hazards at once | 21 Oct | derived, with method and limits |
| One release id from the code, guarded deploy, live proof of delivery | 15–23 Oct | infrastructure |
04How it comes back
Every system breaks. This one is built to say so, and to recover without lying in between.
| When… | The board… |
|---|---|
| a feed fails | turns it grey and says "feed unavailable". It never shows a calm default or a last-known number as current. Relays answer 424 so the explanation survives Cloudflare. |
| a feed is blocked for a day | falls back to the 06:15 MYT snapshot, labelled "snapshot", with its age. |
| a reading is old | applies a currency rule (45 min for rivers, 3 h for DOE, 2 h for models) and stops counting it. Missing data is never an all-clear. |
| inputs are too thin | refuses to estimate. A derived number with too few readings becomes "not estimable", not a guess. |
| the code changes | derives one release id from the code. Browsers fetch the new version and the Menu shows which one you are running. |
| someone deploys | refuses dirty trees, stale copies, mismatched ids and failing tests. It ships only reviewed files, then waits until the live site proves it serves the new release. |
| an agent is wrong | catches it with a test written after the last time, and a rule in the project's instructions that names the mistake. |
05The scars
Each row is a real failure, and each one left a mark that stops it coming back.
| What broke | What it taught | Where it is pinned |
|---|---|---|
| "DOE CAQM: 52" typed into the page as live | Only a relay, recently, may say "now". | tests/kl-live.test.mjs |
| 4,171 generated boxes drawn as Kuala Lumpur | Never promote a stylisation to a survey. | tests/kl-fabric.test.mjs |
| Honest relay errors replaced by Cloudflare's page | Honesty has to arrive, not just be sent. | tests/relay-status.test.mjs |
| Four hand-bumped cache tokens in flight at once | The version is a function of the code, not a memory. | tests/release-id.test.mjs |
| A deploy from a stale checkout reverted a merged fix | A deploy proves only the copy of the code it was given, so check that copy first. | scripts/deploy.mjs |
| A deploy "succeeded" while serving nothing | The upload is not the delivery. | scripts/live-check.mjs |
| River climb measured against the poll clock | Time is observation time, not when we asked. | tests/insights.test.mjs |
| Rain shown as mm/h; it was mm per 15 minutes | Name the window the source names. | tests/rain.test.mjs |
| SLIC Access blank in production, green in tests | Test against the shipped data, not a convenient fixture. | tests/learn.test.mjs |
| "29 districts worst on all six"; it was 3 | A sentence must match the numbers it carries. | tests/joins.test.mjs |
| A map key cut off behind a hidden scrollbar | A key you cannot read implies a completeness it lacks. | tests/msh-geometry.test.mjs |
| JPEG screenshots saved under .png names, three times | Evidence must be what its name says. | tests/evidence-files.test.mjs |
The pattern is always the same. Something that looks finished, a green tick, a passing test, a successful upload, a clean screenshot, stands in for something that is true. Most of this history is the work of telling those apart.
06Who built it
Dr Non Arkaraprasertkul is the author of record for 106 of the 135 commits and set every rule above, usually by looking at a screenshot and asking a question nobody wanted to answer. The board is his gift to depa (Digital Economy Promotion Agency of Thailand) and the ASEAN Smart Cities Network 2026.
The code was written with AI coding agents working under his direction, sometimes in parallel and often reviewing each other's work. Commit trailers name Claude Code, Codex, MiniMax, Antigravity and Cursor, among others. The trailers undercount: not every agent signed its work. One more author, god-mode-bot, made 12 commits, each a daily refresh of the fallback snapshots.
The division of labour settled into a pattern. One agent builds. Another audits it against the rules. A third verifies the live site, not the local copy. The owner reads the evidence and decides. Nobody, human or machine, gets to mark their own work as done.
Counts from the public git history of main on 29 October 2026. Earlier history is summarised; every claim links to the commit that made it.