# Destina Holidays – Full Project Context

Last updated: Oct 20, 2026.

This file is the hand-over for the Destina Holidays static-pages rebuild, the header/footer port, the destination pipeline, and the related changes in `hotel-services` and `homepage-ssr-app`.

---

## 1. Repos, paths and servers

| What | Local path | Deployed to |
|---|---|---|
| Static pages (plain PHP 7.4+, Bootstrap 5.3, jQuery 3.7, Font Awesome kit `80f3ee4f21`, Outfit font) | `/Applications/MAMP/htdocs/github-global-api/destinaholidays-homepage` (moved from `destinaholidays-static-pages` on Oct 21, 2026) | staging `/var/www/flightomart.com/destinaholidays-static-pages`; production `d1.flightomart.com` + `d2.flightomart.com` at `/var/www/destinaholidays.com` (GitHub Actions) |
| Hotel / activity APIs (own git repo) | `/Applications/MAMP/htdocs/github-global-api/hotel-services` | staging `/var/www/flightomart.com/hotel-services`; production backend servers |
| Angular 12 Universal SSR homepage (branch `main`) | `/Applications/MAMP/htdocs/homepage-ssr-app` | `node-home` upstream, port 4000 |
| Old Angular header/footer library (source of the PHP port) | `/Applications/MAMP/htdocs/common-components-github/projects/common-components/src/lib/header` | – |
| Legacy flight search | `/Applications/MAMP/htdocs/github-global-api/search-flights` | backend servers |
| Global API (booking, insurance) | `book-flights/process.php`, `api-stage` in the same parent repo | backend servers |

- Staging URL: `http://stage.flightomart.com/destinaholidays-static-pages/` (use `http://`; HTTPS fails on staging).
- The static-pages folder is meant to become the **root of www.destinaholidays.com**. Production-facing config (e.g. Angular `templateBase`) must point at `https://www.destinaholidays.com`, not staging.
- Production load balancer: host `lb` (`root@flightomart.com`), nginx. Backends `10.116.0.3` and `10.116.0.8`. MySQL `global-api` on `10.116.0.5` (staging and production share the same DB). Mongo reachable externally at `159.223.173.24` with the same credentials.

## 2. Access and secrets

- SSH (staging and LB): `ssh -i ~/.ssh/digitalocean.key -o BatchMode=yes root@stage.flightomart.com` / `root@flightomart.com`.
- Mongo / MySQL / Hotelbeds (Apitude) credentials: `includes/constants.php` in static pages and `hotel-services/include/database.php`. Read them in code, never print or copy them.
- Image keys (Spaces, Unsplash): `/root/.dh-images.env` on staging and `d1` (root-only, `600`); update both after rotating. Keys used by `pipeline/destinations/images.php`: `SPACES_KEY`, `SPACES_SECRET`, `UNSPLASH_KEY`.
- GitHub Actions secret: `SSH_PRIVATE_KEY`. Optional repo variables: `PIPELINE_SERVER`, `PIPELINE_MIRROR_SERVER`, `PIPELINE_SSH_PORT`, `PIPELINE_APP_PATH`, `DEST_CONTENT_API`.
- **Security note:** a DigitalOcean Spaces access key/secret was pasted into chat during this project. Rotate it in DigitalOcean and update `/root/.dh-images.env` on staging and `d1`.

## 3. Working rules for this codebase

- Colours: palette CSS variables only (`--navy`, `--indigo`, `--blue`, `--violet`, `--primary`, `--primary-dark`, `--ac-soft`, `--ac-line`, `--bg`, `--bg-2`, `--border`, `--muted`, `--ink`, `--ink-soft`, `--success`, `--star`, `--danger`, `-rgb` variants, `--band`, `--cta-hover`, `--shadow`, `--shadow-lg`, `--r-card`, `--r-pill`, `--animate`). `#fff` is allowed.
- No code comments in final code.
- Keep changes concise; match the surrounding code.
- Palette is hard-coded in `includes/palette.php` (never read from the DB).
- Every page lives under `pages/` (e.g. `pages/terms-and-conditions.php` → `/terms-and-conditions`); the homepage is `index.php` at the folder root.
- Rails-cakehr repo only (Windows/WSL): never run commands, give them to the user. All other repos: run commands directly.

## 4. Deploying and testing

### Staging
- The SFTP auto-upload on save (`.vscode/sftp.json`) **stopped working** during this session. Upload manually:

```bash
cd /Applications/MAMP/htdocs/github-global-api/destinaholidays-homepage
scp -i ~/.ssh/digitalocean.key <file> root@stage.flightomart.com:/var/www/flightomart.com/destinaholidays-static-pages/<file>
```

- At ~10pm IST on Oct 20 2026 staging stopped accepting SSH (`kex_exchange_identification: Connection reset by peer`) and HTTP took ~20s. Possibly overload or rate limiting from repeated connections.

### Local
- MAMP may not be running. Use PHP's server: `php -S localhost:8791 router.php` from `destinaholidays-homepage`.

### Production (GitHub Actions, `.github/workflows/`)
| Workflow | Trigger | What it does |
|---|---|---|
| `deploy.yml` | push to `main`/`master`, manual | esbuild-minifies `assets/css/*.css` and `assets/js/*.js`, backs up `.htaccess`, `robots.txt`, `sitemap.xml`, rsyncs to `d1` and `d2` at `/var/www/destinaholidays.com`, fixes ownership. Pipeline output (`assets/data/destinations/`, `assets/data/destination-images.json`, `sitemap-destinations.xml`) is excluded and only seeded with `--ignore-existing` |
| `refresh-homepage-data.yml` | every 6h at :15, manual (`all/flights/hotels/vacations`) | SSH to server, `php pipeline/refresh.php <job>` |
| `build-destination-pages.yml` | daily 02:30 UTC, manual (stage, force, probe, only, searches) | SSH to `d1`, starts `pipeline/run-destinations.sh` detached (`setsid nohup`, PID file `/tmp/destination-pages.pid`, log `/tmp/destination-pages-*.log` + `.exit`) and exits after ~20s, like the hotel sync. The script runs `destinations.php`, then `d1` rsyncs the generated files to `d2` (`PIPELINE_MIRROR_SERVER`, default `10.116.0.8`) with key `/root/.ssh/id_ed25519_mirror` (authorised on `d2` only `from="10.116.0.3"`) |

`refresh.php` and `StaticHotels.php` write only to the shared MySQL/Mongo, so running them on `d1` alone serves both servers.

### Browser verification tips
- Use CDP `Runtime.evaluate`. jQuery `trigger('focus')` is needed when the automation tab lacks window focus (native focus events and `:focus` don't fire).
- Use `Emulation.setDeviceMetricsOverride` for mobile, then clear it.
- Force hover with `CSS.forcePseudoState`.

---

## 5. Static pages architecture

### Structure
```
index.php                 homepage (root)
router.php                routing for php -S (mirrors .htaccess)
.htaccess                 clean URLs, legacy redirects, blocks includes/ and pipeline/, caching headers
robots.txt, sitemap.xml, sitemap-destinations.xml
includes/
  palette.php             brand, fonts, colours, gradients, tokens, contact, offices, currencies, services, social, badges
  functions.php           e(), site_services(), base_path(), rewrite_root_urls(), absolute_urls(), palette_css(),
                          asset() (?v=filemtime), minify_html(), send_cache_headers(), send_template_headers()
  header.php              page defaults, cache headers, preconnect, <head>; requires header-data.php and header-markup.php
  header-data.php         $palette, $brand, $contact, $services, $navMenus, $deals, $usefulLinks, $officeAddress, $headerConfig
  header-markup.php       <header id="site-header"> (desktop nav with dropdowns, mobile header, off-canvas menu, sidebars, geo toast)
  footer.php              requires footer-markup.php + scripts
  footer-markup.php       <footer id="site-footer">
  search-box.php          homepage/destination search box (tabs, panes, SVG tab icons)
  faq*.php                FAQ data, search, item, topics, shared page template
  support-request.php     Zendesk support request form
  account.php             account layout for /support/user-* pages
  insurance-form.php      Generali form partial
  travel-feed.php         cached hotel-services feeds (6h, stale-on-error)
  homepage-data.php, destinations.php, db.php, constants.php
pages/
  about-us.php, about-us/nadeem-qureshi.php, pay-later.php, visa-requirements.php, franchise.php, press-release.php,
  media-kit.php, privacy-policy.php, terms-and-conditions.php, uae-terms-and-conditions.php,
  support.php, support/{support-request,ticket,user-home,user-flights,user-activities,user-search-history,user-tickets,user-profile}.php,
  faq/index.php + 14 topic pages (baggage, book-and-hold, cancellations-refunds, changes, children, contact, e-tickets,
  payments, policies, seats-check-in, tours-activities, travel-credits, travel-insurance, vat-tax-refunds),
  flight-status.php, flight-booking-rating.php, reset-password.php, travel-insurance.php,
  destinations/{index,country,guide}.php, header-template.php, footer-template.php
assets/css/   styles, header, footer, template, home, search-box, support, flight-status, travel-insurance, destinations
assets/js/    header, main, search, support, flight-status, travel-insurance, destinations
assets/data/  airports.json, destinations/{slug}.json, destinations/index.json, home.json
assets/img/home/perk-{hold,package,tours,experts}.jpg
pipeline/     refresh.php, destinations.php (+ destinations/{guides,activities,stays,flights,images,home,render,lib}.php),
              FlightAggregator.php, HotelAggregator.php, VacationBuilder.php, ApitudeClient.php, Airports.php, Writer.php,
              schema.sql, places.json, holidays.json, activity-destinations.json
```

### URLs, SEO and caching
- Extension-less URLs at site root. `.htaccess` maps `/x` → `pages/x.php`, `/x` folder → `pages/x/index.php`, `/faq/{topic}/{question}` → topic page, `/destinations/{country}/{city}` → `pages/destinations/guide.php`.
- 301s: `/static/...`, `/pages/...`, direct `.php`, trailing slashes, old support-ssr URLs (`/support/faq/...` → `/faq?...`, `/support/home` → `/support/user-home`, `/support/ticket/{id}` → `/support/ticket?id=`, `/support/itinerary` → `/flight-status`, `/support/user-actions/reset-password` → `/reset-password`, `/support/flight-booking-rating` → `/flight-booking-rating`), `/corporate-travel` → skybooker.com.
- HTML minified on output; CSS/JS minified by esbuild on deploy; gzip via mod_deflate.
- Public pages: `Cache-Control: public, max-age=600, stale-while-revalidate=86400` + ETag. `noindex` pages with query params: `private, no-store`. Assets: 1 year immutable (versioned by `asset()`).
- Each page sets `$page` (title, description, canonical, og, JSON-LD, robots, styles, scripts). FAQ pages emit `FAQPage` JSON-LD; destination guides too.
- Sitemaps: `sitemap.xml` (static pages) and `sitemap-destinations.xml` (written by the pipeline). `robots.txt` also covers the shared docroot (checkout/API paths, `flights/sitemap.xml`).
- Base path auto-detected so the site works at root or in a sub-folder (staging).

### Palette (current: purple)
Taken from the live activities page (`/search-activities/info/...`, `--cta`/`--violet` `#7B5CF5`). History: started purple → changed to homepage-ssr blue (`#4151B3`, navy `#091A55`, indigo `#1C1F7A`) → now purple again.

| Key | Value |
|---|---|
| `blue`, `violet`, `primary`, `alternate` | `#7B5CF5` |
| `navy`, `gray`, `heading-color`, `sub-heading` | `#24135F` |
| `indigo`, `secondary` | `#3F25A6` |
| `ac-soft` / `ac-line` | `#E9E7F7` / `#C9C4E6` |
| `bg` / `bg-2` / `border` | `#EDEBF5` / `#E4E1F2` / `#E3E1F0` |
| `muted` | `#6C74A3` |
| `ink` / `ink-soft` / `body-color` | `#150B33` / `#463D6B` / `#150B33` |
| `band` | `linear-gradient(120deg, #24135F 0%, #3F25A6 55%, #6A4BE8 100%)` |
| `cta-hover` | `linear-gradient(90deg, #F26A3A 0%, #E23A4A 100%)` |
| `shadow` / `shadow-lg` | `rgba(36, 19, 95, 0.08)` / `0.16` |
| success / danger / loader / star | `#0F8A5F` / `#C62839` / `#ED5E3C` / `#F5A623` |

`palette_css()` emits every colour plus `-rgb` variants, derived `--primary-dark` / `--alternate-dark`, and Bootstrap overrides. Favicon: `https://www.destinaholidays.com/favicon.ico`.

### Header
- Ported from the Angular `common-components` header to PHP + jQuery (`assets/js/header.js`, `assets/css/header.css`).
- Desktop nav: Flights, Hotels, Activities dropdowns (popular destinations linking to destination pages: flights = as-is list, hotels = top-rated hotel cities, activities = top-rated activity cities), More (Flight + Hotel, Travel Insurance, Golf, Corporate Travel, Visa Services, Medical Tourism, Travel Guide, Destinations, …), currency, Help & Support, Sign in.
- On pages with `side_menu` (homepage) the ☰ toggle sits next to the logo.
- Mobile header: logo + `.mobile-actions` (currency flag button, guest sign-in button / user link to `/support/user-home` shown as a filled primary circle with the user's first initial, `[data-user-initial]` set in `renderUser()` from `firstName` or `email`). Call Us icon removed.
- Sidebars: login `[data-login]`, currency `[data-currency-sidebar]`, contact `[data-contact-sidebar]`. `openPanel(name)` / `closePanel(name)`; close adds `.is-closing` (slide-out + overlay fade, 300ms) then sets `hidden`. Opening the login sidebar focuses the email input.
- Geo toast on first visit: shows detected location and default currency, with a Change button that opens the currency sidebar; auto-hides after ~8s.
- `window.DH`: `focusFirst`, `openLogin`, `openCurrency`, `currentUser`, `store`, …; jQuery event `dh:user`. Shared `localStorage` keys with Angular: `currency`, `user_details`, `auth_header`, `geo_location`.
- Sign-in: email (`signIn`) and Google button (token client → userinfo → `quickLogin` on `user.php`) save `user_details` + `auth_header` then `location.reload()` so the page (incl. Angular) re-reads the user. `currentUser()` needs both keys.
- `DH_HEADER.carrierBase` (`{cdn}/carriers/`, Spaces) is used for airline logos in `support.js` (My flights), falling back to `MC.png`; the old `skybooker-static` S3 bucket returns 404.
- Checkout focus mode: `DH_HEADER.focusPaths` (`header-data.php`: `/search-hotels/checkout`, `/search-activities/checkout`, `/flight-checkout`) — `header.js` `applyFocusMode()` adds `.focus-mode` to `#site-header` when `location.pathname` matches (also on Angular navigation by wrapping `history.pushState`/`replaceState` + `popstate`); CSS hides `.nav-menu` (Flights/Hotels/Activities/More) and the mobile ☰ toggle. Angular checkouts focus the holder first-name input via `appAutofocus` (`src/app/includes/autofocus.directive.ts`).
- Angular `src/index.html` has a white header placeholder (66px mobile, 83px ≥992px) inside `<app-root>` before boot and on `site-template[name="header"]:empty` until the template is inserted.
- Visibility uses the `hidden` attribute; `header.css` starts with `#site-header [hidden], #site-footer [hidden] { display: none !important }` because Angular pages load Bootstrap, whose `.d-block` etc. (`!important`) otherwise override `hidden` (the Sign in button stayed visible after login).
- `asset()` versions files by `filemtime`. When hot-patching servers, give `d1` and `d2` the same mtime (`touch -d @TS`) or Cloudflare can cache old content under the new `?v=` (served immutable for 1 year). The deploy minifies CSS/JS with `esbuild@0.25.10 --minify`.
- `focusFirst(root)` focuses the first empty, enabled, visible text field; `main.js` calls it on `shown.bs.modal` / `shown.bs.offcanvas`, and toggles (e.g. insurance "Yes, I have a booking") use it too.

### Footer
- `includes/footer-markup.php`, `assets/css/footer.css`. Style inspired by trip.com.
- `.footer-top`: brand, tagline, region/currency button (opens currency sidebar), social icons.
- Contact Us: no "Email us"; phone cards highlighted for US `214-227-6700`, Canada `416-907-9899`, China `+86 400-160-6700`.
- No PCI DSS line.

### Header/footer templates for Angular
- `/header-template` (`pages/header-template.php`) and `/footer-template` (`pages/footer-template.php`) return HTML fragments with CORS `*` (sent by Apache, not PHP), 5-minute cache, minified, absolute URLs. Header template includes fonts, palette CSS, header/footer/template CSS, `window.DH_HEADER`, jQuery and `header.js`.
- `assets/css/template.css` sets font/colour for `#site-header` and `#site-footer` inside Angular. No top margin on `#site-footer` (removed Oct 21, 2026): it exposed the light body background as a band between tinted Angular pages (e.g. `.hotels-page`) and the footer.

### Homepage (`index.php`)
- Left services sidebar (desktop): Flights, Hotels, Tours & Activities, Flight + Hotel | Travel Insurance, Golf, Corporate Travel, Visa Services, Medical Tourism | Find my trip, Book & Hold, Help Center.
  - Collapses to a 68px icon strip; opens briefly on load (`is-side-peek`, removed 1.2s after `load`); expands as an overlay (no layout shift) on hover or keyboard focus; ☰ button pins it open (`localStorage` `dh_side_pinned`).
- Hero: full-width purple band, curved bottom edge, "Travel Made Simple".
- Search box (see below).
- Trust strip (expert support, best price guarantee).
- Four perk cards (Book & Hold, Flight + Hotel, Flexible experiences, Real travel experts): AI-edited photos with abstract navy/blue artwork (`assets/img/home/perk-*.jpg`, 800×600), white card with text below, hover lift.
- Popular Holiday Destinations with theme filter pills, Top Rated Holiday Stays, flight deals, vacations, recently searched (XHR; image cards: saved cheapest-result photo → `DH.destinationImage(code)` (`cfg.destinationBase` = `{cdn}/destinations/`) → icon tile, via `DH.thumb()` with a capture-phase `img[data-thumb]` error fallback; "from {price}/night|person" badge via `DH.money()`; flights use `search_history.quoted_per_adt` + `currency`), customer support band with Support Center and FAQ links (no phone numbers). Content and thumbnails come from `assets/data/home.json` (pipeline `home` stage) with fallbacks.
- `airports.json` is loaded asynchronously after page load (homepage and destinations).

### Search box
- Files: `includes/search-box.php`, `assets/js/search.js`, `assets/css/search-box.css`.
- Tabs (DOM order): Stays (hotels), Flights, Flight + Hotel (packages), Tours & Activities. "Stays" label is search-box only.
- Tab icons: inline SVGs in `$sbIcons` (Expedia pictograms `light__bed`, `light__flight`, `light__ticket`; packages = flight plane scaled 0.7 + suitcase scaled 1.15). Classes: `sb-ic-a` currentColor, `sb-ic-b` 35% blue, `sb-ic-c` 60% blue, `sb-ic-d` navy. Size 2.25rem desktop / 1.85rem mobile. Active underline 3px.
- Mobile: Flights tab visually first (`order: -1`).
- Default tab: URL hash → `localStorage` `dh_tab` (saved only on user click) → `flights` on mobile / `hotels` on desktop.
- Desktop tab click focuses the first field (Destination / From); arrow keys keep focus on tabs.
- Seamless flow: origin → destination dropdown → depart → return → travellers (same for hotels and activities). Previous searches per tab from search-history endpoints.
- Calendar: range picker; on mobile it closes once the range is complete. Check-in/check-out equal halves on mobile (grid).
- Autosuggest overlay/backdrop when the dropdown is visible.
- Sources: `airportItems` (airports.json), `hotelItems` (`?suggestDestinations=true`, hotels at 3+ chars), `tourItems` (`?activitiesAutosuggest=true`, **destinations only** for 2+ chars, popular list below 2).
- Builds `/compare-flights`, `/search-hotels`, `/search-activities` URLs on the main domain; no `username=default-rp` in flight URLs.
- `hotelApi` = `cfg.hotelApiStage` on stage hosts, else `cfg.hotelApi`.

### FAQ and support
- FAQs built from the `faq` and `faq_tours_home` SQL dumps, cleaned (HTML, skybooker links, duplicated `https://`), grouped into 13 flight topics + Tours & Activities. One page per topic and per question. COVID references removed (topic renamed "Travel Credits", question removed/renamed with redirects).
- `/support`: FAQ search, sign in prompt, support request, phone support, Zendesk chat, popular FAQs, topics, "Find my trip" tile (replaced chat-with-agent).
- Account pages under `/support/user-*` with guest / inactive / ready states.

### Travel insurance (`/travel-insurance`)
- Generali Global Assistance branding and logo.
- Step 1: "Did you book your trip with us?" Yes → booking reference/PNR + email to fetch trip details; No → flying from/to (airport autosuggest), departure/return dates (custom calendar), trip cost, travellers.
- Quote then purchase via `book-flights/process.php` (`purchaseInsurance`), card + billing details.
- Files: `pages/travel-insurance.php`, `includes/insurance-form.php`, `assets/js/travel-insurance.js`, `assets/css/travel-insurance.css`.

### Destinations
- `/destinations` index (popular by experiences, things to do by type, discover panel, all destinations by region and country); `/destinations/{country}` country page (with FAQs); `/destinations/{country}/{city}` guide.
- Guide sections: overview, flight prices (from USA/Canada focus), places to visit, things to do by type, top experiences, where to stay, climate, best time, 7-day forecast, know before you go, airports, getting around, FAQ. Search box for flights/hotels/activities/flight+hotel on every page. Source credits removed from pages.
- Destination list in MySQL `hp_destination_pages` (`code`, `city`, `country`, `image_code`, `activity_code`, `active`). Insert a row and run the workflow to add a page.
- Hero images in Spaces bucket `destinaholidays-static`, folder `destinations/{code}.jpg` (filled via Unsplash/Pexels; AWS/CloudFront removed, DigitalOcean Spaces only).

### Naming
- "Tours & Sightseeing" → "Tours & Activities" everywhere (`includes/functions.php` service label, FAQ text, README, `search-flights/index.php`, `search-flights/tours.html`).

---

## 6. Data pipelines

### Homepage tables (`pipeline/refresh.php`)
| Step | Source | Output table |
|---|---|---|
| flights | Mongo `search_analytics.flight_search` (last 7 days, future departures) | `hp_trending_destinations`, `hp_flight_deals` |
| hotels | Mongo `hotels.destinations_mapped`, `hotels.apitude_properties`, `hotels.google_reviews`, optional live Apitude | `hp_hotel_deals` |
| vacations | cheapest round trip + 3-night hotel | `hp_vacations` |
Runs logged in `hp_pipeline_runs`. City names from MySQL `airports_sorted`.

### Destination pages (`pipeline/destinations.php`)
Mongo database `destination_pages` (override `DEST_MONGO_DB`), one document per city code per stage:

| Stage | Collection | Content |
|---|---|---|
| `guides` | `guides` | Wikipedia overview, Wikidata airports/coords, Wikivoyage get-around, Open-Meteo climate, places (from `places.json`), forecast, country facts, exchange rate, holidays |
| `activities` | `activities` | from Mongo `activities.portfolio` + `factsheets_tmp`: counts by category, durations, services |
| `stays` | `stays` | 12 hotels + 12 activities via `hotel-services/process.php?destinationContent=1` (also cached in `cache.destination_content`) |
| `flights` | `flights` | `search_analytics.flight_search` (30 days, USD), cheapest fares, per-origin, per-month, airlines; live searches for missing fares (`--searches`) |
| `images` | – | uploads missing hero images to Spaces |
| `home` | – | writes `assets/data/destinations/home.json` for the homepage (≈100 destinations) |
| `render` | – | writes `assets/data/destinations/{slug}.json`, `index.json`, `sitemap-destinations.xml` |

### Legacy `/flights/*` links (Oct 21, 2026)
Old Google-indexed URLs (`/flights/uae.php`, `/flights/delhi-to-dubai.php`, `/flights/cheap-flight-tickets-to-sydney`, …):
- `.htaccess`: `/flights` → 301 `https://…/destinations`; `/flights/{link}` → `pages/destinations/legacy-flights.php?link=…` (before the `.php`-strip rule).
- `destination_link_term()` (`includes/destinations.php`) takes the last path segment, keeps the part after the last `-to-`, strips `cheap-/flight(s)-/ticket(s)-/to-/business-class-/first-class-/premium-economy-/last-minute-/one-way-/direct-/…` prefixes and `-flights/-tickets/-deals` suffixes, a trailing 3-letter airport code (`gold-coast-ool` → `OOL`), and aliases `uae/usa/us/uk/…` to country slugs. `destination_link_url()` resolves to a country page, a city guide, or an index entry by city slug/code.
- Found → 301. Not found → 302 to `/destinations` and the link is recorded in MySQL `hp_flight_links` (`pending`, hit counter; shared by `d1`/`d2`).
- `pipeline/flight-links.php` (runs at the start of `run-destinations.sh`, so the daily build picks new cities up): marks resolvable links `done`; otherwise maps the term to a city from `assets/data/airports.json` (city slug, else country → most popular city, else city/airport code), inserts it into `hp_destination_pages` and marks it `queued`; unknown terms → `unresolved`. Options: `--add="link1,link2"` seed links, `--retry` re-check `unresolved`, `--build` immediately runs `run-destinations.sh --stage=images,guides,activities,stays,flights,render --only=CODES` (skips `home`, which ignores `--only`). Immediate build on `d1`: `cd /var/www/destinaholidays.com && MIRROR_SERVER=10.116.0.8 php pipeline/flight-links.php --build`.
- Verified: `/flights/uae.php`, `/flights/delhi-to-dubai.php`, gold-coast/sydney/adelaide/philippines/australia links 301 to existing pages; `/flights/delhi-to-hyderabad.php` was recorded, built (HYD, ~10s, mirrored to `d2`) and now 301s to `/destinations/india/hyderabad`.

Commands:
```bash
php pipeline/destinations.php
php pipeline/destinations.php --stage=flights,render
php pipeline/destinations.php --stage=guides --force --only=chi,lon
php pipeline/destinations.php --probe
```

Market focus: fares in USD/CAD, routes from/to USA and Canada (USA ↔ Europe/Asia etc.), not Asia↔Asia.

---

## 7. hotel-services changes

- `process.php`: `?destinationContent=1&hotel={code}&activity={code}` (cached in Mongo `cache.destination_content`); `suggestDestinations` uses `hotels.destinations_mapped` (`code`, `name`, `country`, `countryCode`, `apitude_properties`).
- Visitor ↔ email linking (Oct 21, 2026): `process.php` `visitorIdentify` → `HotelsUser::link()` (stamps the email on `hotels.{search_history,wishlist,viewed}` rows of that `visitor_id` that have no email) + `ActivitiesUser::identify()` (upserts `activities.visitors` {visitor_id, email, names, source, visits, last_seen_at} and stamps `activities.*`). `header.js` `renderUser()` calls it for any logged-in user (all login methods, and existing sessions on next page view) with the shared `act_vid` cookie, once per visitor+email (`localStorage.dh_identified`). Cross-device: `ActivitiesUser::email()` falls back to the email linked to the `visitor_id` in `activities.visitors` (so saves from a linked browser are stamped even when logged out), and search-history `list` uses `owner()` = rows of every visitor id linked to the email OR rows with that email. `/support/user-search-history` now renders hotel + activity history (20 each, newest first, "x ago") plus the last flight search from `localStorage`, via `support.js` `loadSearches()` (`[data-searches]`, `.history-*` styles in `support.css`); `DH.visitorId()` is exposed by `header.js`. Note: `list` trusts the client-supplied email (no token check). For targeted emails query `hotels.search_history` / `activities.search_history` by `email` (rows expire after 15 days via TTL; flight searches are only in `localStorage.search_history`, not stored server-side).
- Search history cheapest result (Oct 21, 2026): `ActivitiesUser::searchHistory()` (shared by hotels) also stores `price` (> 0, rounded 2dp) and `image` (https only, ≤500 chars) from `search` when sent; `action: 'result'` updates the same row without incrementing `count`. homepage-ssr-app: activity results send the cheapest activity's `_ui.price`/`currency`/first image (and wait for the static destinations list so `destinationName` is the name, not the code like `BAI`); hotel results call `HotelUserService.recordResult()` once polling completes with the cheapest property's nightly price and `photos.hotelbeds.com/giata/bigger/` image. Account Search history rows show the same thumbnail + price.
- `activities-static.php`: `ActivitiesStatic::autosuggest()` (`?activitiesAutosuggest=true&q=&language=en&limit=8`).
  - Destinations from `activityDestinations()`: names from `hotels.destinations_mapped` filtered to distinct `activities.portfolio.destination` codes (cached as `['items' => …]` under `act-destination-codes`), sorted by `apitude_properties`, ranked prefix-first with popularity tie-break (`rank($items, $q, false)`).
  - Cache key prefix `act-suggest-v3-`.
  - Example: `rome` → Rome, Italy (`ROE`). London (`LON`) has no portfolio activities so it doesn't appear.
- Both files are modified and uncommitted in the `hotel-services` repo.

## 7a. Flight price monitor (Oct 21, 2026)

- **Naming (Oct 24):** customers see it as "Price alert" everywhere (popup, emails, manage page, checkout). Internal names (`priceMonitor*` actions, `flights.price_monitors`, `/price-monitor` URL) are unchanged.
- **Flight results banners:** `.alert-banner` with the illustration at `{cdn}/price-alert.svg` (Spaces, not in the repo) shows inside the result list on page 1 only, after the 3rd and 6th result (or after the last result when there are fewer than 3). The logic is in `showAlertBanner(i, last)` and `alertRoute()` in `flight-results.component.ts`.
- **Sign-in required (Oct 26):** `DH.openPriceMonitor(trip)` opens the centred sign-in modal `[data-alert-gate]` for guests. The modal shows the logo, an email field and Continue with Google, plus a destination photo with a bell and a fare card. The photo comes from `/assets/data/airport-images.json`, which maps airport and city codes to destination images (for example DPS to `hb-bai.jpg`). The render stage writes this map, matching each destination's own airports first and then guide airports within 60 km; `run-destinations.sh` mirrors it to d2. If no photo is found, the modal uses `destinations/default.jpg` in Spaces (a plane-wing photo, hosted only in Spaces). Continue opens the login sidebar with the email prefilled on sign-in and register. While `pendingAlert` is set, sign-in, Google and register call `afterLogin()`, which opens the alert panel without reloading the page. The alert email is locked (readonly) to the account email. reCAPTCHA stays, because the API has no server-side session to verify the login.
- **Desktop sidebar:** a "Get price alerts" button (`.alert-side`) sits above the filters on desktop flight results.
- **Flow:** the "Set price alert" banner on flight results or "Not ready to book?" in the checkout review/book summary. On mobile the summary stacks at the bottom of the page, so the checkout's sticky `.m-pricebar` also has a bell button (`.m-monitor`, `styles.scss`) next to Continue / Confirm & book.
  - Calls `DH.openPriceMonitor(trip)`, which opens the header sidebar `[data-monitor]` (email, check every 3/6/12/24 hours, reCAPTCHA v2).
  - It then calls `DH.api({action: 'priceMonitorCreate'})` on `api-stage/include/user.php`.
- **Backend:** `api-stage/include/PriceMonitor.php`, Mongo `flights.price_monitors`.
  - **Limits:**
    - captcha required;
    - 10 monitors per IP per day;
    - 3 active monitors per email;
    - a duplicate active monitor (same email, route, dates, passengers and cabin) returns `exists`;
    - the first departure must be more than one day away.
  - **Checks:** each monitor gets at most 5 checks. The cron calls `scan()`, which searches the flight API directly (credentials read from `hotel-services/include/constants.php`) and takes the cheapest converted `TotalFare`.
  - **Emails:** sent through Mandrill using `priceMonitorEmail.html`:
    - a confirmation when monitoring starts;
    - one email per check, sent as `drop` when the price is below the lowest seen so far, otherwise `update`;
    - the 5th check, or the last check before departure, says monitoring has ended (status `completed`);
    - status `ended` when departure is less than one day away.
  - **Failures:** a failed search is retried after 1 hour; after 3 failures the status is `failed`.
- **Stop links:** `/price-monitor?stop={token}` (`pages/price-monitor.php` + `support.js` `initPriceMonitor()`) calls `priceMonitorStop`.
- **Cron:** `api-stage/.github/workflows/price-monitor.yml` runs hourly at :20 and runs `php /var/www/flightomart.com/api/include/PriceMonitor.php` on `d1`.
- **Deploy:** hot-deployed to `d1`/`d2` (api-stage `PriceMonitor.php`, `priceMonitorEmail.html`, `user.php`; site `header-markup.php`, `palette.php`, `header.js`, `support.js`, `header.css`, `pages/price-monitor.php`).
- **Testing:** the full 5-check cycle was tested on `d1` with stubbed captcha and mail; the test document was deleted.
- **Logo:** `destina_logos/destina_logo_360.png` returns 403 on Spaces. `palette.php` `logo_png` and the monitor's default logo now use `destinaholidays_color_flat.png`. Both Angular apps' `common-data.service.ts` `logoPng` now use the same flat PNG, which fixes the logo in booking emails (`book-flights/process.php`).

## 7b. Unguessable hotel IDs (Oct 24, 2026)

- **Why:** a crawler on rotating residential proxies loaded ~15K `/search-hotels/hotel-info/{code}` pages a day by walking numeric Hotelbeds codes. Each page triggers a live Hotelbeds content call.
- **IDs:** `$session->encodeHotelId($code)` / `decodeHotelId($guid)` in both `hotel-services/include/session.php` and `api-stage/include/session.php`.
  - The code is encrypted with AES-128 (one block: an 8-byte `DHHOTEL\0` marker plus the 8-byte code) and written as a UUID.
  - Deterministic: the same hotel always gets the same guid. Decoding a made-up UUID fails the marker check and returns 0.
  - The key is `/etc/destinaholidays/hotel-id.key` on `d1`/`d2` (32 hex characters, `root:www-data` 640, not in git, never printed); `HOTEL_ID_KEY_FILE` env overrides the path. Without the key, encode returns '' and the static-hotel sync aborts.
  - The old `$session->encode()` (key in `constants.php`) is a weak per-character cipher and must not be used for this.
- **DB:** every `hotels.apitude_properties` doc has `guid` (backfilled, 355,731), with a non-unique index `guid` (the sync's `_tmp` collection has duplicate codes). `StaticHotels.php` `loadProperties()` sets `guid` on insert, so weekly syncs keep it.
- **API (`hotel-services`):**
  - `propertyInfo` returns `guid` and, in the final code, accepts only a guid (404 otherwise).
  - `findPropertyAvailability` and `getRelatedProperties` accept a guid or a code (`propertyCode()` helper).
  - `guid` was added to the results `$map`, `slimListProperty`, `mapViewItem`, `searchPropertyAutoSuggest`, `homeProperties`, `queryHotels` (suggest), related items, `getPopularHotels`, `destinations.php` featured hotels, and `HotelsUser` saved/viewed items (`listItems()` hook in `ActivitiesUser`).
  - Booking info adds `emailData.propertyGuid`. Cache keys were bumped (`DEST_SUGGEST_V5_`, `HOTELS_HOME_V7_`), and the `hotels:*`/`home:*` docs in `cache.destination_content` were purged.
- **Rollout:**
  - **Phase 1 (live):** the deployed `process.php` still lets `propertyInfo` accept numeric codes (`$this->propertyCode()`), because the live Angular build sends codes.
  - **Phase 2 (live since the Oct 24 push):** `propertyInfo` is guid-only (`decodeHotelId`), so numeric codes return 404.
  - Destination pages: guids were backfilled into `destination_pages.stays` from `hotels.apitude_properties`, then the pages were re-rendered with `--stage=render` and mirrored to `d2`. No numeric `hotel-info/` links remain in the generated data.
- **Clients:** every Angular hotel link uses `.guid`, and `hotel-info` saves items with `propertyInfo.code` plus `guid`. On the PHP site, `search.js`, `guide.php` (falls back to `code` until the stays data has guids), `stays.php` and `HotelAggregator.php` use the guid.
- **Cloudflare:** Super Bot Fight Mode is on (Definitely automated → Managed Challenge, JavaScript detections on). It also challenges our own server-to-server calls through `www.destinaholidays.com`: the destination pipeline (`stays` stage) and `api-stage/portal/process.php` `hotelServiceBase`/`activitiesServiceBase`. Fixed by custom rule #1 `ip.src in {206.189.177.106 159.89.226.76 161.35.54.246}` (`d1`, `d2`, `lb`). Its action is Skip: remaining custom rules, rate limiting, managed rules and Super Bot Fight Mode. Don't also tick "remaining custom rules in this phase", because Cloudflare rejects that combination. After it was added, the `stays,render` re-run refreshed 529 of 532 destinations with guids.

## 8. homepage-ssr-app changes (uncommitted, branch `main`)

- New `src/app/site-template/site-template.service.ts`: `load(name)` GETs `${environment.templateBase}/${name}-template` as text (`catchError` → `''`, `shareReplay(1)`); `attach()` re-creates scripts in order once header and footer are attached; `bridge()` syncs login (`throwLoginEvent`, `dh:user`) and handles `LOGIN_MODAL` / `CURRENCY_MODAL` messages via `DH.openLogin()` / `DH.openCurrency()`.
- New `site-template.component.ts` (selector `site-template`, input `name`), declared in `app.module.ts`.
- No flash of unstyled header/footer: `SiteTemplateService.ready(name)` moves the header template's `<link rel="stylesheet">` tags into `<head>` and waits for them to load (3s cap) before the component inserts the markup (links stripped from it). Outfit font is also linked in `src/index.html` so the app body doesn't swap fonts.
- `app.component.html`: `<site-template name="header"></site-template><router-outlet></router-outlet><site-template name="footer"></site-template>`.
- `environment.ts` / `environment.prod.ts`: `templateBase: 'https://www.destinaholidays.com'`.
- `<common-header>` / `<common-footer>` removed from 29 templates; `src/app/static` module and route deleted; checkout links now `/pay-later` and `/terms-and-conditions`.
- **No SSR any more (Oct 21, 2026).** Built as a browser-only SPA: `npm run build` (Node 16.19.0, ~50s) → `dist/browser`. `deploy.yml` builds the browser app, rsyncs `dist/` to `/var/www/homepage/dist` on `d1`/`d2` (`--delete-after`), purges Cloudflare; no pm2.
- Served on port 80 by the same Apache vhosts as this repo (`www.destinaholidays.com` / `.ca` in `000-default.conf` on `d1`/`d2`, backups `/root/000-default.conf.*`), via `Include /etc/apache2/homepage-spa-routes.conf`: the Angular routes regex rewrites to `/var/www/homepage/dist/browser/index.html`; `/assets/*` and root-level bundle files are served from `dist/browser` only when they don't exist in `/var/www/destinaholidays.com` (PHP repo wins); cache headers there (`no-cache` index, 1-year immutable hashed bundles). The old `:8090` vhost (`homepage-spa.conf`) is disabled and `src/.htaccess` removed. pm2 `homepage` (36 SSR workers per server) deleted and `pm2 save`d; ~4 GB RAM freed per server. pm2 `support` also deleted; pm2 runs nothing.
- **`@skybooker/common-components` removed (Oct 21, 2026).** Dropped from `package.json`/lock along with `@angular/elements` and `build:elements`; `CommonComponentsModule` removed from `includes.module.ts`; `src/app/elements`, `src/main.elements.ts`, `scripts/concat-elements.js`, `src/assets/common-elements.{js,css}`, `elements-example.html` deleted. Replaced by local `src/app/services/common-data.service.ts` (`getDomainInfo()` returns Destina Holidays settings, `getLocation()`, login `EventEmitter`, `sendMessage`/`getMessage`, `removeParam`) imported by 14 files.
- Templates are fetched cross-origin (`.ca` and local dev load from `.com`). Apache sets `Access-Control-Allow-Origin *` globally on all servers, so PHP must not send it again (a duplicate header makes browsers reject the response); removed from `send_template_headers()` in `includes/functions.php`.
- Templates are served as `Content-Type: text/plain` (`send_template_headers()`), so Cloudflare does not rewrite them. As `text/html` it obfuscated the `mailto:` emails and injected `/cdn-cgi/scripts/.../email-decode.min.js` plus the bot-detection `/cdn-cgi/challenge-platform` script; those relative URLs hit the Angular dev server on `127.0.0.1:4321`, which returned `index.html` (Sentry HOMEPAGE-SSR-34S, "Unexpected token '<'").
- Sentry HOMEPAGE-SSR-34V (Oct 24): `process.php?propertyInfo={code}` returns `{checkInTime, checkOutTime, description, facilities}` with no `name` when the code isn't in `hotels.apitude_properties`, and `hotel-info` rendered it and crashed on `propertyInfo.name.content`. `loadPropertyInfo()` now shows the invalid-property state when `name.content` is missing, and the page container requires `propertyInfo?.name?.content`. HOMEPAGE-SSR-34T (7.8K events, same crash) comes from a Chrome 145/Mac crawler on Singapore cloud IPs (~15K hotel-info loads a day); server logs show no nameless responses and every Mongo property has `name.content`, so the template guard covers whatever the crawler's browser does to the response.
- **Activity SEO (Oct 26):** `robots.txt` allows all of `/search-activities/`. `activity-info` sets `<link rel="canonical">` and `og:url` to `/search-activities/info/{code}` without query parameters, and removes the canonical on destroy. The page works without query parameters, because dates default to today +7 and +8.
- **Hotel SEO (Oct 26):** `robots.txt` allows all of `/search-hotels/`. `hotel-info` sets `<link rel="canonical">` to `/search-hotels/hotel-info/{guid}` without query parameters and removes it on destroy. Without query parameters, dates default to +30 and +32 days.
- **Rental cars removed (Oct 26):** `cars-page-component`, the `car-rentals` route, the Cars tab and the rentalcars loaders are deleted from homepage-ssr-app. `car-rentals` was removed from the Apache SPA regex in `/etc/apache2/homepage-spa-routes.conf` on d1/d2 (backups are in `/root/homepage-spa-routes.conf.*`), so `/car-rentals` now returns 404.
- `server.ts`, `tsconfig.server.json`, `build:ssr` scripts and the `server` target in `angular.json` are unused and can be removed later.
- Currency bridge: `header.js` `initCurrency()` triggers `dh:currency` after auto-detecting the region currency; `SiteTemplateService.bridge()` forwards it as `sendMessage('CURRENCY_LOADED')` so pages waiting on currency don't hit their timeout.

## 8a. ng-checkout-github (`/Applications/MAMP/htdocs/ng-checkout-github`, Angular 11, served at `/flight-checkout/`) — uncommitted

- **`@skybooker/common-components` removed (Oct 21, 2026)**: `npm uninstall`; `CommonComponentsModule` out of `app.module.ts`; new local `src/app/common-data.service.ts` (homepage version minus `getLocation`/`removeParam`, plus `isMobileDevice()`) imported by `data.service.ts`, `flight.book.component.ts`, `soldout-desktop.component.ts`, `review.component.ts`. `deploy.yml`: `repository_dispatch` trigger, npm registry auth and forced common-components install removed.
- PHP header/footer: `src/app/site-template/*` copied from homepage-ssr-app; `app.component.html` wraps `<router-outlet>` with `<site-template name="header|footer">`; `templateBase` in both environments; `brand.scss` excludes `site-template`, `styles.scss` hides `site-template` while the sheet closes.
- `index.html`: Outfit font, fixed white `.dh-header-placeholder` (66px / 83px ≥992px) as first child of `<app-root>` plus `site-template[name="header"]:empty` sizing, so no flicker before the header loads.
- `/flight-checkout` is in `DH_HEADER.focusPaths` (deployed to `d1`/`d2`), so Flights/Hotels/Activities/More and the mobile ☰ are hidden there.
- Build: Node 16.19.0, `npm run build` (~60s) passes.
- Sentry fixes: CHECKOUT-V2-2BJ (`flightResult[0]` undefined) — `book-desktop`/`review` content blocks now require `fareData?.ref && fareData.flightResult` (ref comes from the URL before `validateRef` returns; never filled on cache expiry). CHECKOUT-V2-2BM (`defaultCurrency.country_name` in the old common-components currency modal) — gone with the library; "Change currency" sends `CURRENCY_MODAL` → `DH.openCurrency()`, whose `renderCurrency()` ignores a missing currency. CHECKOUT-V2-2BN (`RangeError: Maximum call stack size exceeded`, Chrome Mobile iOS only, frames point at the page HTML with impossible line/columns) is browser-injected script noise; `main.ts` `beforeSend` drops `RangeError` events with no `.js` frame.

---

## 9. Production nginx (load balancer `lb`)

**Changed Oct 21, 2026** (backup `/root/nginx-backup/default.20261021105341` on `lb`; live copy in `api-stage/nginx.conf`). In both `www.destinaholidays.com` and `.ca`:
- No Node upstreams any more (backup `/root/nginx-backup/default.20261021054546`): `upstream node-home`, `node-support`, `node-tours` and every location using them (`/tours/`, Angular routes regex, `/assets/` split, root-level bundle regex) removed from `.com` and `.ca`.
- `location /` goes to `backend` (Apache on `d1`/`d2`) for everything: PHP homepage/static pages/templates from `/var/www/destinaholidays.com` (this repo) and the Angular browser build (see section 8). `/tours/` now falls through to Apache (301 → `/tours`).
- `location /support/` blocks removed earlier (backup `/root/nginx-backup/default.20261021053440`): `/support/*` served by PHP; old Angular support URLs 301 to PHP equivalents. pm2 runs nothing on `d1`/`d2`.
- `/blog`, `/travel-guide`, `/helpdesk/` and the backend regex are unchanged.
- **Oct 24, 2026:** a temporary Chrome 145 client-IP log (`dh-client.log`) was added and then removed; the config was restored from `/root/nginx-backup/default.20261024061223`, and no Cloudflare rule was created.
  - It tracked a hotel crawler (Chrome 145 on Mac) that loads about 15K hotel-info pages a day through rotating residential proxies, one IP per page; its requests are 2 adults, dates ~30 days out, locations POZ/HKT/BKK/AMI/FUE.
  - IP blocking doesn't work against it; handle it with a Cloudflare challenge rule instead.
- `location /` in `.com` and `.ca` sends `Cross-Origin-Opener-Policy: same-origin-allow-popups` (was `same-origin`, which cut the Google sign-in popup off from the page so the token callback never fired). Backup `/root/nginx-backup/default.20261021060732`.

Before the change:

- File `/etc/nginx/sites-available/default` (nginx 1.26.3, Lua module used for a `/hotels` lowercase redirect). Config test passes.
- Upstreams: `backend` (10.116.0.3, 10.116.0.8, least_conn), `node-home` (:4000 on both), `node-support` (:4001 on .3), `node-tours` (:4003 on .3), `u2-blog` (.3).
- `destinaholidays.com` / `.in` → 301 to `www`. HTTP → HTTPS. Cert covers apex + www for .com and .in, expires **Nov 30 2026**. `.ca` uses its own near-identical block.
- `www.destinaholidays.com` routing:
  - `/blog`, `/travel-guide` → `u2-blog`
  - regex `^/(trips|hotels|flights|search-flights|flight-checkout|book-flights|destinations|travel|events|static_pages_images|static_pages_includes|package-deals|hotel-services)` → `backend` (no trailing boundary, so `/travel-insurance`, `/destinations/...` match)
  - `/support/` → `node-support`; `/tours/` → `node-tours`; `/helpdesk/` → Zendesk
  - everything else → `node-home`
- Blocks `/(admin|config|includes?|lib|logs?|tmp|backup|cache|vendor|node_modules|api-stage|memcache)/`, dotfiles, backup files.
- Security headers duplicated three times per location; location-level `add_header` overrides server-level CORS. `.ca` has a dead `/flight-checkout/` block.
- Resolved on Oct 21: templates, `/` and `/support/*` route to PHP, `/assets/` split by sub-folder.

**traveli.com (onboarded Oct 26, 2026):**
- DNS is at DNS Made Easy, not Cloudflare. `traveli.com` and `www` have A records pointing straight to `lb` (161.35.54.246).
- nginx uses its own file, `/etc/nginx/sites-available/traveli.com.conf` (symlinked into `sites-enabled`), copied from the clevelandcare.org pattern:
  - Port 80 serves the ACME webroot `/var/www/html/traveli.com` and sends everything else to `https://www.traveli.com`.
  - On 443, the apex 301s to `www`, and `www` proxies to `backend` with the standard deny rules and security headers.
- The Let's Encrypt cert (webroot authenticator) covers `traveli.com` and `www.traveli.com`, expires Jan 24 2027, and auto-renews; the renewal dry run passed.
- Apache on `d1`/`d2` already had the vhost `www.traveli.com` (alias `traveli.com`) with docroot `/var/www/traveli.com`, now owned by `www-data`.
- The docroot holds a static homepage, "Traveli for Teams", positioned as corporate travel for golf and sports (styled after kayak.com/business): `index.html` plus `assets/` (`logo.svg`, `logo-white.svg` and the pin `favicon.svg`). Its source is the `traveli/` repo (github.com/dnishant1/traveli); the old 2022 Angular 12 SSR white-label app was removed from that repo on Oct 26, and git history still has it. It has `noindex, nofollow`; remove that at launch. The "Request a proposal" lead form is front-end only and sends nothing yet.
- The old, empty `/var/www/traveli` folders on `d1`/`d2` were removed.
- Deploys run on push to `master` (or manually) via `traveli/.github/workflows/deploy.yml`, which mirrors the destinaholidays-homepage workflow:
  - It needs the `SSH_PRIVATE_KEY` repository secret.
  - It runs `rsync --delete` to `/var/www/traveli.com` on d1/d2 (excluding `.git`, `.github`, `README.md` and `.gitignore`), then sets `www-data` ownership.
  - It verifies that the md5 of `index.html` matches on both servers and that `Host: www.traveli.com` returns 200.

---

## 10. Request history (chronological)

1. Scan static pages; redo header and footer, trip.com-style, starting with footer.
2. Port Angular common-components header to PHP/jQuery for all static pages.
3. Use the destinaholidays.com activities font (Outfit) everywhere.
4. Use homepage-ssr colour palette as a hard rule; hard-code it in `includes/palette.php`; favicon from destinaholidays.com.
5. Move all static pages into `pages/`.
6. Build FAQs from SQL dumps; remove COVID references; SEO-friendly, cacheable FAQ pages; bigger responsive fonts like trip.com.
7. Support page with login prompt, FAQs, support request, phone, Zendesk chat; Find my trip tile instead of chat tile.
8. Focus email input when the login sidebar opens.
9. Build a trip.com-like homepage with services sidebar and search box (flights, hotels, tours, flight+hotel); tours = search-activities; other services as links.
10. Content-heavy homepage (makemytrip content/images, licensed).
11. Port homepage-ssr date picker and autosuggest (mobile and desktop); wider search box; recently searched items via XHR; seamless field-to-field flow; fix from/to flicker.
12. Switch theme to homepage-ssr blue; remove inner rounded corners in the search bar.
13. Destination pages generated dynamically via GitHub Actions; collapsible sidebar; group destinations by country; cache hotel and activity data in Mongo; search box on destination pages; destinations table in MySQL.
14. Aggregate flight prices, hotels, activities into Mongo daily via GitHub Actions; Mongo external IP access.
15. Remove source credits text from destination pages; flight price click → flight results.
16. Improve `/destinations` like Viator blog; cover all Viator/Apitude destinations.
17. Travel insurance purchase page with Generali APIs (PNR+email or manual trip).
18. Load airports.json async; fix search box width; Generali logo; FAQs for country pages.
19. Production deploy workflow for d1 and d2; SEO (sitemap, robots), HTML/CSS/JS minification.
20. Homepage content for ~100 destinations via pipelines; live flight searches when no fare data; focus on USA/Canada routes.
21. First-visit toast with default currency and location; remove PCI DSS text.
22. Header with Flights/Hotels/Activities dropdowns (different popular lists), Travel Insurance under More.
23. Destination images: Freepik → Pexels → Unsplash into DigitalOcean Spaces (no AWS/CloudFront); remove all destination credits; homepage at folder root.
24. Homepage hero background (tried image + overlay, Stripe-style gradient; final: full-width blue band, curved bottom); relevant images for tiles; customer support image/section with FAQ and Support Center links, no phone numbers; ☰ next to logo; better cards with overlay gradient; more destinations (aggregate 500).
25. Fix insurance autosuggest and date picker.
26. Footer redesign with region/currency button; remove Email us; highlight US/Canada/China phones.
27. Smaller header fonts on mobile; focus inputs after toggles; mobile header with sign-in icon and currency flag (no Call us); mobile check-in/check-out alignment; close mobile calendar after range.
28. Render header/footer as PHP templates for homepage-ssr-app instead of the Angular library; use production URL; remove Angular static module.
29. Activities autosuggest: show destinations only (Rome fixed).
30. Mobile: Flights first and default tab.
31. Rename Tours & Sightseeing → Tours & Activities everywhere.
32. Inspect production nginx (read-only).
33. Expedia pictogram tab icons (flight, bed, package → plane + suitcase, ticket); Hotels → Stays; 3px active underline; bigger icons.
34. AI-redesigned perk card images with text below.
35. Sidebar peek on load, open on hover.
36. Desktop tab click focuses first field.
37. Purple palette (from the activities page) across all pages.
38. Animate sidebar close.
39. Write this context file.

---

## 11. Pending / open items

- **Upload to staging** once SSH recovers: `assets/js/header.js`, `assets/css/header.css` (sidebar close animation).
- Fix or restart the SFTP auto-upload watcher (the moved folder has no `.vscode/sftp.json`).
- Hotel search outage (Oct 21): `hotels.apitude_properties` was empty after an interrupted `StaticHotels.php` swap; restored from `apitude_properties_tmp` (354,883 properties). `_tmp` has 2,000 duplicate `code`s and the swap deletes live data before copying, so the Sunday 02:00 UTC sync on `d1` (`api-stage/.github/workflows/sync-static-hotels.yml`) can cause it again until the swap is fixed.
- **Rotate the Spaces key** that was pasted in chat.
- Production deploy: static pages (incl. `pages/header-template.php`, `pages/footer-template.php`, `includes/header-data.php`, `includes/header-markup.php`, `includes/footer-markup.php`, `assets/css/footer.css`, `assets/css/template.css`), `hotel-services/activities-static.php`, `hotel-services/process.php`, `search-flights/index.php`, `search-flights/tours.html`.
- nginx cutover done (section 9); homepage-ssr-app changes can now be deployed.
- The support SSR app's own deploy workflow (if it still runs `pm2 restart`) would bring `support` back; disable it.
- PHP redirects (e.g. `/support/` → `/support`) return `http://` URLs, adding a second hop through the load balancer's HTTP→HTTPS redirect.
- Commit `hotel-services`, `homepage-ssr-app` and `ng-checkout-github` changes (ng-checkout `deploy.yml` builds `/flight-checkout/`; verify the header there after deploy).
- Commit the price monitor (section 7a) in api-stage, destinaholidays-homepage, homepage-ssr-app and ng-checkout-github. The cron starts once `price-monitor.yml` is on the default branch.
- Optional: regenerate homepage perk artwork in purple to match the palette.
- Activities autosuggest "No matches" for Bali/Phuket/London/Paris (Oct 21): the Oct 20 sync stored only 196 activity destinations (Hotelbeds `/countries` now embeds partial destination lists, which `syncAuxiliary()` trusted), so `syncPortfolio()` skipped most destinations (portfolio 7,925). `api-stage/include/StaticActivities.php` now always fetches `/destinations/{lang}/{country}` (falls back to the embedded list); hot-patched on `d1` (`/var/www/flightomart.com/api/include`, backup `/root/StaticActivities.php.bak`), `aux` re-run → 1,091 destinations incl. `BAI`; `portfolio` + `content` re-run started Oct 21 07:08 UTC (log `/tmp/static-activities-20261021-070851.log`). After it finishes, hit `process.php?activitiesAutosuggest=true&q=bali&refresh=1` a few times (both servers) to refresh `act-destination-codes`. Commit the api-stage change. Hotelbeds activities key/secret are hard-coded fallbacks in that file — move to env and rotate.
- Older backlog: image stage follow-up (rebuild guides, render home on staging, copy `home.json`); Popular Flight Routes ("From India") question; PCI DSS/UPI card question.
