Changelog

What we’ve shipped.

Every feature we ship — often because a host asked. Vote on what’s next on the Build Board.

14 entriesLatest · 2026-09-06
  1. The Founding Race was hardcoded, not data2026-09-06

    and the guard could not see it (branch `Affiliate-level2`)

  2. One person, one row2026-09-06

    on both sides (branch `Affiliate-level2`)

  3. Eight places, not nine2026-09-04

    and the card that hid the eight it had (branch `main`)

    The bug the verification found: a card that showed nothing it had not fetched itself
    `NearbyExperiencesCard` held its list in `useState<NearbyPlace[] | null>(null)` and learned it ONLY from its own action's return value. Every page load rendered _"Find nearby places"_ and an empty card — with eight places saved in the profile and rendering on the live Experiences page. Its own doc comment, _"the host reviews the list here"_, stopped being true the moment the host reloaded. 🚨 **Not merely cosmetic.** The only route back to the list was to press the button, which spends a real Overpass round-trip — a free, rate-limited, frequently-overloaded community service — to redisplay rows the database already held, **and overwrites the saved selection while doing it**. 🔑 **Found by disagreement, not by reading the component.** While verifying the cap, the card said _"Find nearby places"_ while the database said `nearby=9`. The empty card on its own looked like a perfectly ordinary empty state; only the two sources contradicting each other made it a bug. It also nearly cost the cap its verification — the blank card read as "my change broke the fetch." - `loadOverviewData` already read `content_profile` for the Rewrite card, so the profile is parsed ONCE and both cards read it — no extra query for the fix. - The `saved` prop is **required on purpose**. Optional would type-check at a mount that forgot it, which is exactly how `turnstileSitekey` was threaded through five layers and passed at none (savepoint §2.6). - Verified as a render, both directions, on the real dashboard: 8 saved → _"Refresh"_ plus all eight chips; cleared → the CTA returns with no phantom list; restored → the list is back. The eight objects were snapshotted to a file before the control cleared them — real host data, and a negative control is not worth losing it to. - Corrected in passing: a comment naming `refreshNearbyPlacesAction`, which does not exist.
    ⚠️ Correcting the SEO-title advice from the entry below
    That entry says to compare the **SEO title** across wizard runs as the canary for "the AI actually ran". **That is wrong, and the fifth run disproved it.** Page SEO is never written by the wizard — `regeneratePageSeoAction` says so outright (_"THIS IS NOT HOW A PAGE GETS SEO"_): every page is written once, free, by `/api/seo-ai-worker`, driven by a `*/10 * * * *` pg_cron job that lives in the live cloud database and posts to production, so it sweeps rows a LOCAL wizard creates. Measured: site built `12:55:08Z`, `seo_ai = {}` at `12:59`, written at `13:00:15Z` — the sweep, to the second. So run four's AI titles and run five's stock titles were the same behaviour at different points in a ten-minute cycle. 🚨 **And it is worse than merely timing-dependent.** Run five's home title came back _character-for-character identical_ to run four's, because the generation is hash-bounded off the same business facts. Following that advice would have compared two identical strings and concluded the AI was serving from a cache — **a false negative on the exact question the canary exists to answer.** The canary is the BODY copy, and several slots at once rather than the headline alone.
    Runs five and six, and the purge behind them
    **Run five** (`d50e6171…`) is the one that caught the SEO-title mistake above; it is otherwise unremarkable — 8 rows, 15 pages, both new pages in draft and published, 9 places (it predates the cap). **Run six** (`9200aa27-028e-470a-995d-5e7da5ecb3d4`) is the first run that exercises **both** of today's changes from a clean start, and that is why it was worth doing: - 🔑 **The cap on a genuinely FIRST fetch.** The earlier proof was a _refresh_ over an existing nine, which only shows the cap applied to a re-fetch. Run six started at `nearby: 0`, the card offered _"Find nearby places"_, and one press produced **8** — stored 8, rendered 8, Albasini ruins absent, 8 Directions links, no sample note. The category filters are still all four groups, so the round-robin spans the set at the lower cap rather than collapsing toward restaurants. - 🔑 **The card fix on a real fresh load.** Navigating away and back shows _"Refresh"_ and _"8 places saved"_ with the chips. Before today the identical navigation showed the empty CTA with eight places sitting in the database. - Sixth distinct headline: _"Where the Fence Used to Be"_ — differing in structure, not only wording, from the five before it. (Body copy, not the SEO title — see the retraction above.) **The purge.** `d50e6171…` (15 pages) and `b5f952cd…` (15 pages), both hard-deleted, **not recoverable** — wizard-built verification artefacts carrying nothing hand-written. Dry run first, per the rule added after the previous purge: both listed with page counts and deletion timestamps, `must survive: 9200aa27` printed, and the apply pass named the same two ids. Each delete re-read `deleted_at` immediately beforehand rather than trusting the listing it had just printed. Verified: **zero** orphaned pages, `9200aa27…` the only site left, and the negative control — `/experiences` still 8 places and 8 Directions links, `/gallery` still 11 images, the card still _"8 places saved"_. ⚠️ All three sites were `mana-bush-lodge`, so **the subdomain identifies nothing** — the id is the only thing telling them apart, which is why every line of a purge is scoped to one id.
    Run seven — the filters, not just the count
    `1dceb388-5ea2-4efb-8991-294542aada8e`. Same result as run six on every axis already listed, so only the new check is worth recording: 🔑 **A cap can starve a category.** The count being 8 says nothing about WHICH eight. `shop` has exactly one member in this area, so dropping 9 → 8 could have cut it and left a "Shop" filter that leads to an empty list — a control implying a capability the page no longer has. Measured on the live page against the database, group by group: | | stored | rendered | | ------ | ------ | -------- | | All | 8 | 8 | | Shop | 1 | 1 | | Nature | 2 | 2 | Full spread `eat 3 · see 2 · nature 2 · shop 1` — the round-robin still reaches every group at the lower cap. **Assert the DISTRIBUTION a cap produces, not only its size.** **The purge.** `9200aa27…` (15 pages), dry run first, `must survive: 1dceb388` printed, zero orphans, survivor still rendering 8 places and 11 gallery images. 📌 **Runs three through seven have now returned the same result five times.** The wizard path is settled; each run since has earned its keep only by checking something the previous one did not (run five: the SEO retraction · run six: the cap on a FIRST fetch · run seven: the distribution). An eighth identical walkthrough would confirm nothing new — **give it a new question or spend the time elsewhere.** ---
  4. Seeded by default2026-09-03

    and the wizard had to be able to build the page at all (branch `main`)

  5. The whole wizard, start to finish, on the real dashboard2026-09-03

    and a purge (branch `main`, no code change)

  6. "This business already has a website"2026-08-20

    deleting a site locked the business out forever (branch `feature/website-cms-10min-wizard`)

  7. External Reviews as a standalone subscription-gated feature2026-08-10

    Phase 1 (branch `external-reviews-integration`).

  8. Fix: booking-field polish2026-07-21

    date card + guests select + sabela footer CTA.

  9. Theme differentiation Phase B: Safari fully forked (Home + Rooms + Room-detail), built from scratch2026-07-20

    Phase B COMPLETE (`d651e36`).

  10. Theme differentiation: Safari forked site-wide2026-07-20

    all remaining shared subpages bespoke (`179c2a7`).

  11. Theme differentiation: Royal forked site-wide2026-07-20

    all remaining shared subpages bespoke (`20b0acc`).

  12. Wizard→website Phase 2: go-live UX2026-07-20

    publish from the wizard + lower the wall (`51d8e78`).

  13. Theme differentiation Phase B: Royal Rooms + Room-detail forked2026-07-19

    Royal fully decoupled (`c24882d`).

  14. The last three lifecycle docs2026-07-16

    which found six background jobs silently dead.