Services site for Hongdam, Chiang Rai. The front page lists every service with its price,
English at / and Thai at /th/: data/services.json → python3 tools/build_services.py
→ docs/index.html, docs/th/index.html and the services block of docs/llms.txt. A new
service is one more item in the JSON. GitHub, PHP fix and bot prices come from /api/github,
/api/php and /api/bots (at build time, and again in the page when it loads); --offline
uses data/services.live.json. The build stops when a price disagrees with its task-graph
node. /services redirects to / (docs/_redirects). Orders go to /api/contact, which
takes an email address or a LINE ID. The portfolio is at /portfolio/.
The pages outside the shop (your own builder, red team, market, bots, GitHub, translation, the task board and
graph, events, portfolio, stats, invoices, 404) wear the shop's frame and look: docs/assets/pages.css on top of
shop.css, and tools/pages.py for the head, top bar, heading with its dingbats, and footer. data/pages.json names
each page's line colour, dingbats and link card; data/pages/ holds the words of the pages kept by hand;
node tools/share_cards_pages.mjs <name> draws a card, and python3 tools/build_task_cards.py --all the task cards.
Prices walk monthly by Shoup's rule (her word 2026-10-15; monthly walk asked 2026-10-23): an item with
baht (or pct, for sourcing) and its floor, ceiling and slots a month is written by the build into
functions/_services_data.js; functions/_services.js walks it on the 1st, 00:00 Bangkok (over 80% of
slots ordered last month, one step up; under 60%, one down), first check 1 December 2026. Orders are
rows in svc_order (D1 hongdam-tasks, migrations/0006_services.sql): the page's form through
/api/contact, and team orders by POST /api/services/order {svc, note} with Bearer TASKS_ADMIN.
GET /api/services gives every price and its walk. Grab rows (walk: "desk") are walked by the
Hongdam Desk bot (hongdam-desk/grab.py), which posts its price to /api/services/price.
node tests/services.test.mjs; node tools/services_sim.mjs runs a year under four demands.
Baht, dollars or euros (her ask 2026-10-24): docs/assets/cur.js adds a ฿ / $ / € switch to the top bar and shows every .amt price in the reader's currency, rounded up to a whole unit, at the day's rate from /api/fx (European Central Bank reference rates through frankfurter.dev, open.er-api.com as the spare; functions/_fx.js). A first visit opens in baht in Thailand and its neighbours, euros in Europe, dollars elsewhere. /api/buy takes pay (THB, USD or EUR) and asks for the same converted amount; the column svc_buy.pay (migrations/0011_pay.sql) keeps it and the baht for PromptPay. Card or PayPal on PayPal's own page (functions/_paypal.js, POST /api/buy/card) is on once PAYPAL_CLIENT_ID and PAYPAL_SECRET are set on the Pages project; a payment counts only when PayPal reports it COMPLETED for the order's ref, amount and currency. node tests/fx.test.mjs, node tests/card.test.mjs.
Bitcoin by Lightning (her ask 2026-10-24): on while PAY.lightning in functions/_shop.js holds a Lightning Address (empty turns it off). The Buy page asks POST /api/buy/ln for a bill from that address's wallet (LNURL-pay) for the amount due, in sats at the bitcoin price from Kraken (Coinbase as the spare) and the day's exchange rate; the bill is kept on the order in pay.ln. When the wallet gives a verify address (LUD-21), POST /api/buy/ln/check and the order page mark the order paid once the wallet reports the bill settled with a preimage that hashes to its payment hash; the buyer can also press "I paid by Lightning", and the team checks the wallet. A bill is kept until it runs out. With an address set, tools/build_shop.py names Lightning in the pay line of the footer and the Buy form (rebuild the pages). The footer links offrampt.net, crypto to baht in Thailand, and so does the Lightning box. node tests/ln.test.mjs.
Her ask 2026-10-24: a Hongdam Studio minisite on black swans and black swan predictions, with toys and prediction storage, very shareable, in few words with each one explained, for second-language readers and short attention. hongdam.net/studio/black-swan/ (and /th/, /my/, /zh/, /de/): her photo of a bronze hongsa (CC BY 4.0, credit Nan Peacock; the licence is also in the files' EXIF and XMP and in the page's ImageObject JSON-LD), four toys (count white swans until a black one comes; a swan meter, likely × big; 100 days with one huge day; black swan or not, with everyone's votes), a form that saves a prediction with its odds, size and check date, and the list of predictions with votes (will it happen, then after the check month, did it happen). Each prediction has a short page, hongdam.net/swan/ (functions/swan/[id].js), whose link preview carries the prediction as its title and an odds picture (docs/og/swan/o0–o4.jpg); the page also makes a 1080 × 1350 picture of the prediction to share or save.
- Words:
data/blackswan.json→python3 tools/build_blackswan.py(pages, the /swan/ template atstudio/black-swan/one/, sitemap and llms.txt blocks); the Studio front pages carry a link band (tools/build_studio.pyswan_band). - Toys, form, list, picture:
docs/assets/blackswan.js,blackswan.css. Dingbats: group p-blackswan,DINGBATS_SRC=<doodle-dingbats>/src tools/blackswan_dingbats.sh(that group only). - Link-preview pictures:
node tools/blackswan_cards.mjs. - Storage:
functions/api/swan/(D1 tables inmigrations/0012_swan.sql). A prediction naming the Thai royal family, or one Llama Guard flags, waits for the team (a note in #help-desk);POST /api/swan/<id>/state {"state":"ok"}withBearer TASKS_ADMINlists it,GET /api/swan/heldshows the waiting ones. Three reports from three addresses take one off the list. Six new predictions a day from one address, 400 for the site.node tests/swan.test.mjs.
Her ask 2026-10-26: a Hongdam Studio minisite for DJ, her advisor, as a portfolio and introduction to the company, in few words. hongdam.net/studio/dj/ and /th/studio/dj/, noindex and out of the sitemap and llms.txt. Four names (Mot Dang, Hongdam, Chiang Mai Visa Desk, Hongdam Studio), each with an Isotype row: one picture for a fixed number, the last one cut to what is left, counted at build (Mot Dang pages and public repositories from data/studio/counts.json, shop services from data/shop.json, Visa Desk services from chiangmaivisadesk.com with data/studio/dj.counts.json as the fallback). Then the work and the papers as thumbnails of each site's own link-preview picture (docs/studio/dj/img/), and the crew.
- Words:
data/studio/dj.json→python3 tools/build_dj.py. Look:docs/assets/dj.css,dj.js(rows fill in as they come into view). - Spares (her word 2026-10-26, DJ asked about a single point of failure): each part of the company with its copies and its monthly cost.
eval "$(~/.claude/bin/cfenv)"; python3 tools/dj_spares.pycounts them (org admins and repositories from GitHub, R2 storage and requests from Cloudflare, open-model jobs from ~/.claude/state/ow.tsv, the server, box and Workers prices as bought) intodata/studio/dj.spares.json; then rebuild. - Dingbats: group p-dj,
DINGBATS_SRC=<doodle-dingbats>/src tools/group_dingbats.sh p-dj(that group only). - Link-preview pictures:
node tools/dj_card.mjs.
Live at https://hongdam.net (Cloudflare Pages project hongdam). A merge to main
in github.com/motdang/hongdam-site goes live within about five minutes:
motdang-server runs /srv/site-deploy/deploy_on_merge.sh every five minutes and
deploys docs/ and functions/. Ledger: /srv/state/site-deploy.tsv on the server.
Roll back by reverting the commit on main, or in the Cloudflare dashboard.
Bindings and secrets (AI, RT_LIMIT, Resend) live on the Pages project, not here.
Her word 2026-10-22: every page under docs/ meets WCAG AA contrast (4.5 for body
text, 3 for large text) at phone and desktop width, in light and dark, in English and
Thai, at the top and scrolled, and on each tab. node tools/contrast/run.mjs checks
them all in about 80 seconds; node tools/contrast/run.mjs for/cca checks pages whose
path contains for/cca. Node 22+ and Chrome; nothing to install. Pages that read /api/... get their answers
from live hongdam.net (GET only), so the gate sees the content people see; a failed read
or a page that does not load fails the run.
The GitHub check "gate: contrast" (.github/workflows/gate-contrast.yml) runs it on
every pull request and every commit to main. pr-push waits for it and leaves the PR
open when it is red. The server's deploy holds a commit on main until it is green
(held in /srv/state/site-deploy.tsv).
What it measures: each visible run of text against the colours behind it. Over a
photograph, against the overlay gradient laid on the lightest tenth of the part of the
photo behind the text. Faded text counts at its faded strength. Gradient-filled text
counts at its weakest colour. A see-through fixed bar counts against what scrolls
under it. Text not shown yet (a fade-in that has not run), hidden text, and switched-off
controls (disabled, aria-disabled="true") are left out, as WCAG leaves them out.
- Real screenshots, not mockups. 12 pages were captured live with headless
Chrome (
docs/assets/screens/) — motdang.net, wichaa.net, poplucky.net, defiant.to, hakfarang.net, nanobotco.github.io/offramp, nanobotco.github.io, nanobotco.github.io/skipdjt, and three wichaa.net instrument pages (jovilabe, divination, moon — moon/redspot kept as parallax background art only, not listed as separate repos since they're pages inside the Lanna repo, not repos of their own). Each sits in a fake browser-chrome frame (dots + URL bar) so it reads as a genuine screenshot, not a photo. - All 38 public NaNoBotCo repos are represented — verified live via
gh repo liston 2026-07-30, cross-checked against theproject_git_reposmemory for which ones are deliberately private. The 8–10 with a real live URL got full screenshot treatment; the other ~28 (APIs, CLIs, crawlers, a text game, SMAPI mod, etc. — genuinely no "sight" to screenshot) got a designed icon-tile grid instead, grouped by category, each linking straight to its GitHub repo. Nothing fake-screenshotted. - Excluded on purpose:
spec-hud,coucal-clock,catalog-pipeline,acr-hud,manuscript-crawler(all private on GitHub already), plus anything never pushed at all —khwan,tel,michael-go-court/legal material,photo-mine,rusty-chandelier(client-owned). None of these appear anywhere on the page. - On the photo roll: I didn't browse it.
photo-mine's ownBOTS.md/RABBIT_HOLES.mddocument real passport, bank-card, financial, and medical hits mixed into that library — not something to page through for a business site's hero art. If there are specific photos you want used, export them (or point me to a folder) and I'll drop them in; otherwise wichaa.net's existing graphics are covering the "visual" ask safely.
- Top progress bar fills across the viewport width as you scroll the page.
- Continuous parallax inside each screenshot frame — the captured page drifts slightly within its browser-chrome window as it passes through view (not just an entrance effect; keeps moving the whole time the card is on screen).
- Staggered reveal — cards/tiles fade + rise in with a slight cascade by column position, instead of all popping in at once.
- Slow-rotating clock glyph in the "offices overlooking the clocktower" card.
- All of it is one rAF-throttled scroll listener; geometry is measured once on
load/resize, never inside the scroll handler itself (an earlier draft read
getBoundingClientRect()per element on every scroll frame — real layout- thrashing risk, fixed before shipping). - Tried native CSS
animation-timeline: view()/scroll()first (the actual "scroll-driven animations" browser API) — pulled it back in favor of the plain JS version since it's more broadly predictable across engines, and the JS version already reuses a scroll-handler pattern proven earlier in this build. Visually the outcome is the same. - The
.revealhidden state (opacity:0) is now gated behind anhtml.reveal-readyclass JS adds right before wiring up the IntersectionObserver, plus a 1.5s force-reveal safety net — so content can never get stuck invisible if JS is slow, errors, or a browser lands straight on a scrolled anchor before the observer's first callback fires.
For people:
- Bilingual title/description (
<title>and meta description now carry Thai too). - Favicon (
docs/assets/favicon.svg) matching the nav mark. - A permanent thin gold "gilt-edge" hairline across the very top of the page — a premium-print touch, not just the scroll-progress fill.
- A subtle grain-texture overlay (
feTurbulencedata-URI,mix-blend-mode:overlay, ~3.5% opacity) for a tactile, less flat-vector feel. - Ornamental section dividers using ๛ (khomut) — the traditional Thai manuscript end-of-section mark. Chosen deliberately over generic decoration: it's real Thai typographic heritage, and it ties back to the Lanna-manuscript roots this whole body of work actually grew from.
- Footer closes with "ขอให้กิจการเจริญรุ่งเรือง" (a genuine, common Thai business-opening blessing — "may the business prosper") next to a small gold ๙ seal — เก้า/nine being a well-known auspicious number in Thai culture.
- Card/button hover states warmed from teal to gold, richer layered shadows.
For robots:
- JSON-LD structured data (
ProfessionalService+ anItemListof the live portfolio) — both validated as parseable JSON. - Full Open Graph / Twitter Card meta, canonical URL.
docs/robots.txtexplicitly welcomes named AI crawlers (GPTBot, ClaudeBot, anthropic-ai, Google-Extended, CCBot, PerplexityBot) — same stance Mot Dang and wichaa.net already take: crawlers are guests, not pests.docs/llms.txt— a short plain-language summary of what Hongdam is and does, for AI systems that read that convention.- Footer explicitly links
/llms.txtso human visitors can see the site means it.
- Contact info — email / LINE / phone. The contact section currently shows a visible "coming soon" placeholder instead of fake details.
- Friend's name — for the "Built in partnership" credit line, currently generic.
- Confirm company name/spelling — using "Hongdam" (EN) / ฮ่องดำ (rough Thai) as placeholders; correct if there's an official Thai name or logo.
- Repo + hosting — per Nan's call, this will live under NaNoBotCo on GitHub for
now (transfer to Hongdam's own account later). Needs:
git init, push to a newNaNoBotCo/hongdam-site(or similar) repo- GitHub Pages enabled on
docs/, custom domainhongdam.netset in repo settings - a CNAME record (or A/ALIAS records per GitHub Pages docs) added at whichever registrar holds hongdam.net, pointing at NaNoBotCo's GitHub Pages
- Once live, run the Mot Dang ad entry (already drafted, see below) through
build.pyand push, so the sponsor card goes live on motdang.net.
Added to mot-dang/data/ads.json locally:
{
"id": "hongdam",
"th": "Hongdam — บริษัทเว็บดีไซน์เชียงราย เว็บสองภาษา แอปไลน์ ระบบข้อมูลจริง",
"en": "Hongdam — Chiang Rai web studio: bilingual platforms, LINE-native apps, real data",
"url": "https://hongdam.net"
}This won't appear on the live site until mot-dang's build.py is re-run and the
result is pushed — hold off until hongdam.net actually resolves, so the ad doesn't
link to a dead domain.
data/packs/*.json→python3 tools/build_market.py→docs/market/,functions/api/_packs.js.docs/redteam/callsfunctions/api/redteam.js, which needs the Workers AI bindingAIon the Pages project (optional KVRT_LIMITfor a per-address daily cap).- Local:
.claude/rt-dev.sh→ http://localhost:8791/redteam (AI calls are remote and billed).
A board for work done by people or bots, answered by Mot Dang, Hongdam and Chiang Mai
Visa Desk. Pages and the API are Pages Functions (functions/tasks/, functions/api/tasks/,
shared code in functions/_tasks.js) over D1 hongdam-tasks (binding TASKS,
schema migrations/0001_tasks.sql). Secrets on the Pages project: TASKS_ADMIN
(also ~/.tokens/HONGDAM_TASKS_ADMIN), DISCORD_TASKS (#help-desk forum).
- Kinds of work:
data/tasks/graph.json, one line per kind.python3 tools/build_tasks.pychecks it and writesfunctions/api/_taskgraph.js,docs/tasks/graph.jsonand the drawn graph atdocs/tasks/graph/.--newlists kinds posters named that the graph lacks. - Our postings and the asks from other sites:
data/tasks/seed.json→seed.sql;--seedloads new keys into D1 (existing keys are left alone). - Shoup:
functions/_tasks.jsshoup()andcatBaht();node tests/shoup.test.mjs. - Answer as one of ours:
POST /api/tasks/<id>/admin {"do":"answer","org":"hongdam","note","baht"}withAuthorization: Bearer $TASKS_ADMIN.
Bookings for PHP work after PHP2Shell (CVE-2026-109618, CVE-2026-109619), priced by Shoup the same way as /github. Page docs/php/index.html, prices and the weekly walk in functions/_php.js (KINDS, OPENED, relate), bookings and slips in functions/api/php/[[path]].js, table php_order in D1 hongdam-tasks (migrations/0005_php.sql, applied remote 2026-10-22). node tests/php.test.mjs.
The four kinds are Hosted site, Server, Upgrade and Five servers. After the walk a hosted site is priced at most as high as a server, an upgrade at least as high, and five servers at least the rung above a server. The US$ price is the baht at 33 to the dollar rounded to US$5 (usd() in _php.js). A booking counts toward slots and the walk when it is paid or done, or booked less than 48 hours ago. An Upgrade booking with PHP older than 7.4 is answered with a LINE message and no order.
The form takes a site name and a contact; server access is arranged on LINE after payment. GET /api/php answers any origin; POST answers hongdam.net, hongdam.pages.dev and localhost. The ip column of php_order is written null. nanobotco.github.io/php-patch links here and reads /api/php.
Upgrade scope (her pick 2026-10-22, option D): flat for WordPress, Drupal and Joomla; custom code gets 8 hours in the price, then a quote at ฿1,500 an hour, and the buyer can stop there with nothing more to pay.
Hongdam Studio, the tech side (her ask 2026-10-24), is a site inside the shop: /studio/ in five languages and the
notes under /studio/notes/, with Atom and JSON feeds at /studio/feed.xml and /studio/feed.json. Words, practices
and work: data/studio/studio.json; a practice sells shop cards at the shop's prices. Notes:
data/studio/notes/<date>-<slug>.md, written to data/studio/brief.md, checked by tools/studio_check.py.
python3 tools/build_studio.py builds the pages, feeds, sitemap block and llms.txt section; node tools/studio_cards.mjs
draws the link pictures; tools/studio_publish.sh <note> does all of it and publishes through pr-push. A scheduled task,
studio-notes, writes a note each morning in a clean clone, ../studio-desk.