# Deploying the Bangsar South Area Guide to production

**Written for:** whoever is running the deploy — not necessarily the person who wrote the content.

Everything below was built and verified on a developer machine against a READ-ONLY copy of the
shared catalogue. Production is where it gets written, and this is the order.

---

## Already deployed once? Read this instead

**The full runbook below is for the FIRST deploy.** Once production has the content, a change to
the words or the cameras is three commands, and steps 1–4 are not repeated.

### What is in the bundle right now (2026-09-20)

| | Was on production | Now in the bundle |
|---|---|---|
| **Chapter 6 (the closing chapter)** | quoted a monthly rent and a psf trend | **no figures at all** — it explains where the demand comes from |
| Chapter cameras | opened at zoom 14.2, looking down | **open at zoom 16.2, tilted 55°** — the towers, not half of KL |
| The area's own zoom | 15.3 | 16.2, so opening the area is one clean descent — **carried by the bundle since format 2**; before that it stayed behind and the map landed wide, then shoved itself in |

⚠️ **The rent figure is the reason this update exists.** A rent, a psf or a yield in the copy goes
stale, differs per unit and gets quoted back at us — the same reason the area chatbot's prompt
forbids them. Do not put one back.

### The three commands

```bash
# 1 · On your machine — rebuild the bundle from whatever you just edited
php artisan area-guide:export-area bangsar-south

# 2 · Copy storage/app/area-guide-export/bangsar-south/ to the server, then:
php artisan area-guide:import-area /path/to/bangsar-south            # dry run, writes nothing
php artisan area-guide:import-area /path/to/bangsar-south --apply

# 3 · ⚠️ RE-RENDER THE VOICE, or it keeps reading the old words.
#     Name the chapters you actually edited — each one is a paid provider call.
php artisan area-guide:narrate --area=bangsar-south --chapter=6
#     Everything, when the whole story moved:
php artisan area-guide:narrate --area=bangsar-south --force
```

⚠️ **Step 3 is the one that gets forgotten.** The narration is rendered audio, not text read at
runtime — the bundle does not carry it, and the import does not touch it. Skip this and the page
shows the corrected chapter while the voice recites the sentence you just removed, with nothing
failing anywhere.

**`--chapter` takes ONE-BASED numbers** (`6`, or `2,6`), matching every line the command prints.
It needs `--area`, because areas have different numbers of chapters and the same number is a
different paragraph in each; a number that area has no chapter for is an **error**, not a quiet
no-op. It **implies `--force`** — naming a chapter is already the decision to replace its audio.
`--force` on its own re-renders all 12 files (6 chapters × EN/中文), which is what you want after
a whole-story rewrite and a waste after a one-sentence fix.

**No code deploy is needed for a content-only change** — the content lives in the catalogue
database, not in git.

### ⚠️ The bundle format is checked, and a mismatch is refused

The import compares the bundle's `version` against the one this site's exporter writes and
**refuses anything else outright**, naming both numbers. That is deliberate: a bundle one format
behind looks completely normal in a dry run — it simply has nothing to say about whatever the
newer format added, and that part quietly does not travel. **Format 2 (2026-09-20)** added
`area.map`, the area's own lat / lng / zoom. If the import refuses a bundle, re-export it from
the machine that wrote it; if it refuses a *fresh* bundle, this server's code is older than that
machine's and needs the deploy first.

---

## What is actually changing

The panoramas and the 3D building models are **already on production** (7 panoramas, 4 models) and
nothing here touches them. What is new is the authored content around them:

| | Production today | After this |
|---|---|---|
| Bangsar South story | 2 paragraphs | **6 chapters**, EN + 中文 |
| Chapter cameras | none | 6 — this is what makes the map move as you read |
| Chapter pictures | none | 4 |
| Traced boundary | none | 8 points |
| `profile.video` (retired) | **still present** | removed |
| Avatar library | **table does not exist** | 5 avatars |
| The Vertical / The Horizon / VE Hotel | 3 rows, **empty** | summaries + 14 tabs |
| Location video | none | 1, playing from a Vimeo link |
| Narration | none | 12 files (6 chapters × EN/中文) |

**~4.5 MB of images travel.** No panorama and no model is uploaded, re-uploaded or moved.

---

## Before you start

- [ ] The content bundle: `storage/app/area-guide-export/bangsar-south/` (4.4 MB). Copy it to the
      server — it is NOT in git, because it carries image files.
- [ ] **`ffmpeg` on the server** and **a queue worker** — ⚠️ **NOT needed for this deploy.** The
      Bangsar South video is a pasted Vimeo link, so nothing is converted. They are needed the
      first time someone UPLOADS a video file; without them it sits on "Waiting to convert"
      for ever. Worth doing, but it does not block today.
- [ ] **No `.env` changes are required.** Every new setting
      (`AREA_GUIDE_SOON_AREAS`, `AREA_GUIDE_VIDEO_SOURCE_MAX_KB`, `AREA_GUIDE_FFMPEG`,
      `AREA_GUIDE_VIDEO_HEIGHT`, `AREA_GUIDE_VIDEO_CRF`) has a working default.
- [ ] ⚠️ **`AREA_GUIDE_LOCKED=true` keeps MEMBERS on the coming-soon notice.** Admins see the
      real guide. Leave it until the content has been read; set it to `false` to open it.
- [ ] A Gemini API key on **Manage → AI Providers** (not `.env` — see step 6).

---

## The steps

### 1 · Deploy the code

```bash
sudo bash scripts/deploy-update.sh
```

⚠️ **Do not `git pull` first** — the deploy script does it.

It also runs `composer`, `npm run build`, **`migrate --force`**, `config:cache`, `view:cache`, and
restarts php-fpm / Horizon / SSR. So steps that would otherwise be yours are already done —
**except the catalogue migrations below**, because `migrate` only reads the DEFAULT migration
directory.

### 2 · Run the catalogue migrations

These add the avatar library and the two new asset columns. They are on the **catalogue**
connection, which is a separate migration directory.

```bash
php artisan migrate --path=database/migrations/catalogue --database=catalogue --force
```

Expect four: `create_area_guide_avatars`, `add_avatar_id_to_area_guide_assets`,
`add_transcode_to_area_guide_assets`, `add_video_link_to_area_guide_assets`.

### 3 · The config cache

**The deploy script already did this** (`config:cache` at the end of step 1), so there is nothing
to run here. It is called out because it matters, and because anyone editing
`config/area_guide_content.php` LATER — or `.env` — has to run it by hand:

```bash
php artisan config:cache
```

⚠️ **This is not optional when you edit config.** `config/area_guide_content.php` gained
`video_source_max_kb`, `ffmpeg`, `video_height`, `video_crf`. On a stale cache
`video_source_max_kb` reads NULL, `(int) NULL` is `0`, and `max:0` **refuses every video upload**
with a size message that is true of no file anyone owns. (A floor was added so this can no longer
be silent, but the real ceiling still needs the rebuild.)

### 4 · Clear the retired keys

```bash
php artisan area-guide:strip-area-copy --apply
```

Removes `tagline`, `facts`, `video` and `video_media` from stored areas. Without it, an area that
still holds one **cannot be saved from its own editor** — Laravel's `array:` rule fails the whole
attribute on a single unlisted key.

### 5 · Import the content

**Dry run first. It writes nothing and prints exactly what it would change.**

```bash
php artisan area-guide:import-area /path/to/bangsar-south
```

Read the plan. Then:

```bash
php artisan area-guide:import-area /path/to/bangsar-south --apply
```

What it does: uploads the 5 avatar images, replaces the area's story / chapters / boundary,
fills the three buildings and their tabs, and creates the location video. It is **re-runnable** —
everything is matched by name and updated in place.

It is also **safe about ids**: a chapter's pictures and a tab's `<img>` both address an avatar by
uuid, and production mints its own. The bundle carries `{{avatar:…}}` tokens instead, and the
import substitutes production's real ids *and* the `?v=` cache buster. (Carrying ours would 404
every tab picture on production, silently.)

### 6 · Render the narration

```bash
php artisan area-guide:narrate --area=bangsar-south --force
```

12 files, ~1,400 characters, billed by Google. `--force` matters: the old whole-area audio reads
a script that opened with the `tagline` the guide no longer has, so it is deliberately invisible
until re-rendered.

⚠️ **The key comes from Manage → AI Providers**, not `.env`. This was broken until 2026-09-19 —
the TTS client read the env only, so a verified key on that page produced an empty key and an HTTP
error from Google with nothing naming the cause. Fixed, but confirm the key is saved there.

### 7 · The rollout switches — already set

Nothing to do if production already carries these; they are **temporary decisions**, not defaults:

```env
AREA_GUIDE_PINNED_AREA=bangsar-south   # the landing list offers this one area
AREA_GUIDE_CHAT_ENABLED=false          # the area chatbot stays off
AREA_GUIDE_LOCKED=true                 # members still see coming-soon
```

⚠️ **The pin no longer opens the area.** Since 2026-09-19 the guide lands on a short list —
Bangsar South pressable, three more named as coming — and the pinned area opens from it. The
coming names default to `Maluri / Cochrane | Bukit Bintang | Old Klang Road`; override with
`AREA_GUIDE_SOON_AREAS`, pipe-separated, then `config:cache`.

---

## Check it worked

1. Open `/property/academy?tab=area-guide` as an admin.
2. The story is **6 chapters** with pictures, and a **play button** under "THE STORY".
3. Press play — the voice reads, and at the end it scrolls to the next chapter by itself.
4. **Back / Next** under the story turn the page, and each chapter **moves the map**.
5. The area has a **dimmed boundary** around it.
6. A **round avatar** marker waves on the map. Pressing it opens the video **at the top** and the
   page scrolls to it.
7. Open a building marker — The Vertical, The Horizon or VE Hotel — and its tabs have content.

---

## If something is wrong

| Symptom | Cause |
|---|---|
| Import stops naming missing tables/columns | Step 2 did not run. It checks the schema before writing anything. |
| No play button under the story | No narration rendered for that chapter — step 6. The control is absent by design, never broken. |
| Story shows but the map does not move | The chapters have no cameras. Check the import applied `profile.chapters`. |
| A tab picture is missing | The `?v=` did not match. Re-run the import; do not hand-edit the HTML. |
| Every video upload refused on size | Stale config cache — step 3. |
| An uploaded video sits on "Waiting to convert" | No queue worker, or no `ffmpeg`. |
| An area cannot be saved from the editor | Step 4 did not run. |

---

## Rolling back

The import only **adds and updates**; it deletes nothing except an avatar image it has just
replaced. There is no "undo" command — restore from a database backup if the content itself needs
reverting. The code side rolls back with the normal deploy rollback, but ⚠️ **leave the catalogue
migrations in place**: the columns are nullable and harmless to older code, and dropping them
would take the content with them.
