Changelog

What we’ve shipped.

Every slice, in order. Pulled directly from the project’s CHANGELOG.md — no separate marketing version.

217 entriesLatest · 2026-06-25
  1. Fix (re-applied): menu styling class2026-06-25

    array join

  2. Fix: header preview2026-06-25

    logo style + minimal alignment

  3. Live QA pass (Step 1)2026-06-22

    first browser drive-through

  4. Website CMS: theme + brand wiring2026-06-21

    new-tab previews + "Aria" flagship default theme

    Changed — previews open in a new tab (A)
    - Theme gallery "Preview" (hover + footer) now opens the live site in a **new tab** (`/site?site=<sub>&preview=1&theme=<slug>`) so the host sees exactly how their site looks/functions under that theme on their own data, before activating. The old full-screen iframe modal is replaced by a small, reversible **Activate** confirmation (`ThemeActivateModal`, with its own "Preview in new tab" link). Editor header + Brand Studio already opened previews in a new tab. Commit `22d376b`. +3 en i18n keys.
    Added — "Aria", the new flagship default theme (B)
    - Migration `20260621000000_theme_aria_default.sql` (data-only; **pushed to the linked DB**) adds the **Aria** theme and makes it the **sole default** (demotes warm/coastal): modern editorial-luxe `base` — warm paper `#F6F4EF`, near-black ink, deep-eucalyptus accent `#2F5D4F`, `elegant` serif-display + sans, `lg` radius — plus a polished **7-page `page_templates` blueprint** (home / about / rooms / contact / blog / checkout / thank-you, 23 sections) whose sections auto-populate from the host's rooms / reviews / gallery / location / blog. The home page folds in the curated **`trust`** section (from the Phase 6A slice). New sites seed Aria automatically; existing sites can activate it.
    Verified (C)
    - `scripts/verify-theme-aria.mjs` (read-only, service role) — 22/22 green against the live DB: Aria present, active, sole default; base font/radius/accent correct; 7 pages with all expected kinds; home carries the Trust + auto sections; all 23 sections have unique UUID ids + the props the schema requires. Migration list confirms the prior theme migrations were already applied remotely (gap C). - No app-schema change ⇒ no type regen. tsc + lint green on the changed code. **Note:** the `elegant` font stack falls back to Georgia/system unless the Cormorant/Inter web fonts are loaded site-wide — an optional fidelity follow-up.
    Compatibility — themes work with everything we've built (D)
    - `scripts/verify-themes-compat.mjs` (read-only) runs **every active theme's** `page_templates` through the **actual current `sections.schema`** (`parseSectionsLoose` + per-section `safeParse`, via Node TS type-stripping): all three themes (aria/warm/coastal) validate with **zero sections dropped**, and every section type they use exists in `SECTION_TYPES`. So activating any theme never silently drops a section, and the Phase-4 **Forms** work already flows through every theme (each Contact page's `contact_form` routes to the host inbox). - Migration `20260621001000_themes_add_trust.sql` (data-only, pushed, idempotent) adds the curated **`trust`** section to the **warm + coastal** home pages too (Aria already had it), so every theme carries the recent work. Re-verified green.
  5. Website CMS Phase 6 on-site checkout (save-point c)2026-06-21

    PHASE 6 COMPLETE

    Added — shared booking core (one money SSOT)
    - **`lib/bookings/createBooking.ts`** `createBookingCore(input, actor, ctx)` — extracted verbatim from `createBookingAction` (validate → re-price via the canonical `priceStay` → availability RPCs → `persistBookingAndPay`). Both checkout surfaces now run it: the app's authenticated checkout and the new session-less on-site one. `createBookingAction` is now a thin auth wrapper — behaviour preserved (same returnTo, same redirect).
    Added — session-less on-site checkout
    - **`lib/website/siteCheckout.ts`** — `siteBookingQuote` (live price + availability via `computeStayPricing` + the RPCs) and `createSiteBooking` (find-or-create a passwordless guest via `findOrCreateLeadIdentity`, then run the shared core). Both membership-gated (property must be a VISIBLE channel member, via the exported `resolveSiteProperty`) and run on the service-role admin client. Routes **`/api/site-booking-quote`** + **`/api/site-booking`**. - **`app/[locale]/site/book/page.tsx`** — themed checkout page (membership-gated loader: property + rooms + the host's payment rails [Paystack when connected, EFT fallback] + cancellation note). **`SiteCheckoutForm.tsx`** (client, `--site-*` themed): dates, whole-place/rooms, party, contact, payment method, policy ack, a live server-recalculated total, submit → redirect to the host's Paystack page (card) or the on-site thank-you (EFT). - **`app/[locale]/site/book/thank-you/page.tsx`** — confirms a card payment with the host's key via the existing `confirmHostCardPaymentByReference` (webhook is the backstop), or shows the awaiting-transfer state + the host's banking details for EFT. Anti-tamper: booking must belong to a sellable property.
    Changed — on-site booking links + a tenant-host routing fix
    - Booking deep-links from the site (rooms, rate table, booking-search, availability-calendar) now point at the **on-site** `/book` route via a new `siteBookHref(ctx, …)` (relative on a tenant host; `/[locale]/site` + `&site=` when rendered via the app-domain `?site=` preview). Threaded `siteParam → ctx.bookBasePath` through `loadSiteContext` / `SitePageView` / the site routes. - **middleware fix:** on a tenant host, global route-handler trees (`/api`, auth, ical, …) now pass through to their real handlers (tagged with `x-wielo-site-host`) instead of being rewritten into `/<locale>/site/*` where they'd 404. This also fixes the existing `/api/website-*` form/funnel endpoints for real tenant domains (previously only exercised via the app-domain `?site=` path, so the bug was latent). App-host routing unchanged (host tests 10/10).
    Notes
    - tsc + lint (changed files) green; `verify-themes-compat.mjs` 🎉. - **Deferred (founder):** add-ons + coupons in the on-site UI (the core already supports them — UI only); Turnstile/bot-hardening on the checkout; the on-site flow needs `NEXT_PUBLIC_ROOT_DOMAIN` + wildcard DNS to run on real tenant domains (same W5 OPS TODO) — until then test via the app-domain `?site=`. ---
  6. Website CMS Phase 7 save-point (c): image pipeline + lightbox + media + perf score2026-06-21

    PHASE 7 COMPLETE

    Added — image transform pipeline
    - **`lib/site/image.ts`** — `siteImageUrl` rewrites a public `website-assets` object URL to its Supabase `/render/image/...` variant (resized + WebP/AVIF via the Accept header); `siteImageSrcSet` builds a responsive `srcset`. No-op for non-project URLs and SVGs (passthrough). Verified live: a 2.5 MB PNG → 56 KB WebP at w=480, 133 KB at w=1280. - **`components/site/SiteImg.tsx`** — the one `<img>` for the public site: responsive `srcset`/`sizes`, lazy by default (eager + `fetchpriority=high` when `priority`), graceful fallback for non-transformable sources. Pure presentational (no directive) so it renders in both server sections and the client lightbox. Chosen over `next/image` deliberately — works on tenant custom domains with no `/_next/image` dependency or Vercel optimizer cost. - Converted every public image to `SiteImg`/`siteImageUrl`: gallery, host bio, rooms preview, blog preview, logos strip, specials preview, hero backgrounds (CSS `background-image` resized at fixed widths), chrome logo, and the blog index / tag-archive / post-detail covers + author avatar + related covers.
    Added — lightbox
    - **`components/site/GalleryLightbox.tsx`** (client) — grid (grid/list/carousel) + swipeable fullscreen overlay: prev/next, ArrowLeft/Right + Esc, touch-swipe, counter, caption, scroll-lock. `GallerySection` now delegates to it.
    Added — media library (fresh editor uploads)
    - `RichTextEditor.onImageUpload` now returns `{ url, alt }`. `PostEditor`'s `uploadBodyImage` switched to the media path (`createWebsiteMediaUploadUrl` + `registerWebsiteMediaAction`): prompts for alt text, captures intrinsic dimensions, registers into `website_media` — so a fresh body image is reusable in the library and alt/CLS-ready. +1 i18n key (`imageAltPrompt`).
    Added — performance score (dashboard Overview)
    - **`lib/website/perfAnalyzer.ts`** — pure `analyzeSitePerformance` over the media library (responsive ✓ always, alt coverage, known-dimensions/CLS) → 0–100 score + graded checks (mirrors the seo/a11y coach pattern). A lab/readiness signal, not field CWV. - `loadOverviewData` counts `website_media` (alt/dims) and returns `performance`; the Overview page renders an "Image performance" card (score + bar + checks). +13 `website` i18n keys (`perf*`).
    Deferred (founder)
    - Real field **Core Web Vitals** (RUM beacon + aggregation) — the dashboard score is lab/readiness for now. - Media library: replace-in-place, folders. (Reusable picker, search, alt, dimensions already shipped.) - Optimising user-inserted `<img>` inside sanitised blog `body_html`. ---
  7. Website CMS: Phase 4 form builder2026-06-20

    slice 1 (Forms tab + builder UI)

    Added
    - **Forms tab** in the website editor (`[websiteId]/forms`) — a master-detail builder: a forms list on the left, a curated builder on the right. - **Curated form builder** — name + type (contact/custom/newsletter), an ordered field list (add from a fixed catalogue: text/paragraph/email/phone/dropdown/ checkbox/date; edit label/placeholder/required + dropdown choices; reorder + delete), and form settings (button label, success message, send-to-inbox). - **SSOT field schema** `lib/website/forms.schema.ts` (`FORM_FIELD_TYPES`, `formFieldSchema`, `formSettingsSchema`) — shared with the public render + submit route built in slice 2. - **Owner + feature-gated actions** `createWebsiteFormAction` / `saveWebsiteFormAction` / `deleteWebsiteFormAction` (soft-delete so existing responses keep their parent), `loadFormsEditor` loader (parses stored jsonb through the SSOT schema + counts live submissions per form). - +44 `website` i18n keys (en). No DB schema change (tables from the Phase 4 foundation migration `20260620005000`). tsc + lint green.
  8. Website CMS: Phase 4 form builder2026-06-20

    slice 2 (public render + submit)

    Added
    - **`form` section type** — a curated, auto-populate section that references a `website_forms` row by id and resolves its fields/settings live at render (edit the form in the Forms tab and the page updates instantly). Wired through the schema, `SiteDataByType.form` (`FormRenderData`), `assembleSiteDataByType` / `assembleSectionData` / the builder preview pool, `sectionDefaults`, the section library + thumbnail, and a builder editor case with a form picker (`listWebsiteFormsAction`). - **Public `FormSection`** — renders the curated fields dynamically (text / paragraph / email / phone / dropdown / checkbox / date), themed via `--site-*`, with a honeypot and the form's success message. Inert in the builder preview. - **Submit pipeline** — `lib/website/submitWebsiteForm.ts` + service-role route `app/api/website-form-submit`: validates values against the form definition server-side, persists every submission to `website_form_submissions`, and for email-bearing non-newsletter forms reuses `createWebsiteEnquiry` to open a "Website Enquiry" in the inbox (storing `conversation_id`). Honeypot-only spam, per the locked decisions; newsletter→CRM routing is slice 3. - +9 `website` i18n keys (en). No DB schema change. tsc + lint green.
  9. Website CMS: Phase 4 form builder2026-06-20

    slice 3 (newsletter → CRM)

    Added
    - **Newsletter routing** — a `newsletter`-type form submission now upserts the email into the host's CRM (`host_contacts`) with a `newsletter` tag and marketing consent (`email_consent`), and opens NO inbox conversation. A blocked contact is respected (no re-engagement). The submission is still persisted to `website_form_submissions` for the responses view. - **`upsertHostContact` gains `addTags`** — a merge-only (never-removes, deduped) tag option, so the canonical contact writer stays the single path. No DB schema change. tsc + lint green.
  10. Website CMS: Phase 4 form builder2026-06-20

    slice 4 (responses view)

    Added
    - **Responses view** `[websiteId]/forms/responses` — a host-facing list of every form submission. Filter by form + by status (Active / Archived / All), expand a row to read the full field→value detail, and manage status (mark read, archive, restore). New submissions are bold with a dot and auto-marked read on open; email-bearing submissions link straight to their inbox conversation. - **CSV export** — per-form, client-side: columns are the form's field labels + submitted-at + status (proper quoting). Disabled until a single form is chosen. - **`setSubmissionStatusAction`** (owner-scoped) + `loadFormResponses` loader; a "Responses" link on the Forms tab header and a per-form "View N responses" link in the builder footer. +24 `website` i18n keys (en). No DB schema change.
  11. Website CMS: Phase 4 form builder2026-06-20

    slice 5 (polish) + phase complete

    Fixed
    - **`FormSection` guards a zero-field form** — a `form` section pointing at a form with no fields now renders nothing on the public site (a hint in the builder) instead of an unsubmittable form. - **Checkbox values are readable** — stored as `Yes` (was `true`), so inbox threads, the responses view and CSV exports all read cleanly.
    Phase 4 status
    Form builder is feature-complete for the locked scope: builder + curated fields, public render + service-role submit, inbox routing, newsletter→CRM, responses view + CSV. **Deferred (noted for the founder):** Cloudflare Turnstile (no env keys yet — honeypot-only for now), newsletter double-opt-in, convert-to-booking deep-link, POPIA erase tooling, a default "quick contact" form per site. No DB schema change this slice. tsc + lint green.
  12. Website CMS enterprise build-out · Phase 8: Blog (Commit A2026-06-18

    dashboard, WIP)

    Added (dashboard half)
    - **Featured posts** — `website_blog_posts.featured`; pin/unpin a hero post from the list (star toggle, `setBlogFeaturedAction`), featured-first ordering in the list. - **Post search + status filter** (All / Live / Scheduled / Drafts) and a **"No SEO"** warning badge on posts missing a meta title/description. - **Category counts + slugs** shown inline in the categories editor. - **Scheduled publishing (editor side)** — a Scheduled status + datetime picker; the status CHECK already allowed `scheduled` (the cron worker that flips it is Commit B). - **Author profile fields** — `author_bio` + `author_avatar_path` (avatar upload + bio). - **Reading time** estimate + **auto-excerpt** (derived from the body when left blank). - Migration `…001700` (featured + author_bio + author_avatar_path + scheduled index).
    Remaining (Commit B — public + cron, next session)
    - Public blog index page (`/site/blog`) + RSS `feed.xml`, related posts + author card on the post page, featured-first in `blog_preview`, and the scheduled-publish cron worker. See `CURRENT_TASK.md` for the full checklist. NOT yet on `main` (phase incomplete).
    Note
    - `pnpm build` needs a bumped heap here (`NODE_OPTIONS=--max-old-space-size=6144`) — the default OOM-crashes the build worker (env memory, not a code issue). ---
  13. Affiliate programme (Phases 1–8)2026-06-16

    branch `feat/affiliate-program`

    Built
    - **Enterprise affiliate programme for Wielo's own products**, open to ANY user (a guest account is the only prerequisite — identity is `user_profiles.id`, not the host). Mounted at `/portal/affiliates` (universal authenticated area) with a cross-workspace discovery link from the host dashboard sidebar. - **Tracking**: `/r/<slug>` route drops a 30-day first-party cookie + logs the click; the referred user is bound to the affiliate permanently at signup (`UNIQUE(referred_user_id)`), surviving the guest→host transition. - **Commission engine** (the finance core): `accrue_affiliate_commission` RPC derives commission from each completed `platform_ledger` charge — NET base (amount − VAT), per-product rate + duration (once/months/forever), idempotent. Hourly clearing cron (pending→cleared after the refund hold); clawback RPC + trigger (void pending/cleared, negative offset for already-paid) wired to a new `platform_ledger.reverses_ledger_id` link + daily backstop. - **Affiliate UI**: Overview (hero, referral link, stat cards, referred-users breakdown), Products (per-product links + commission), Marketing (download + copy-embed with the link baked in), Payouts (balance, request modal with gross/fee/net, methods, threshold, history). - **Payouts**: `create_affiliate_payout` RPC atomically claims cleared commission (FOR UPDATE SKIP LOCKED), enforces threshold, deducts the per-method processor fee (affiliate earns gross, receives net). Manual-first settlement. - **Admin**: `/admin/affiliates` (payout queue approve/paid/reject + affiliate list with suspend/reactivate) and `/admin/affiliates/settings` (cookie/hold/ threshold/terms/attribution, per-method fees, marketing-asset upload). Wired the user-record **Referrals tab** to real data.
    Changed
    - `products.affiliate_*` (already present) is the commission source of truth. - Accrual hooked into the Paystack webhook (product + subscription paths) and the admin manual-charge action. `withAdminAudit` target union extended.
    Migrations
    - `20260616000010_affiliate_core.sql`, `…011_affiliate_payouts.sql`, `…012_affiliate_settings.sql`, `…013_affiliate_rpcs.sql`, `…014_affiliate_cron.sql`, `…015_affiliate_marketing.sql`, `…016_affiliate_payout_rpc.sql`, `…017_affiliate_admin_rpcs.sql`, `…018_help_affiliate_program.sql`.
    Notes
    - **Action required:** redeploy the `paystack-webhook` Edge Function so Paystack-triggered accrual goes live (`supabase functions deploy paystack-webhook --no-verify-jwt`) — the safety classifier blocked the agent from deploying the live payment function. - Setup-fee commission deferred (billing doesn't charge it as a separable ledger amount); the `kind='setup_fee'` path is reserved. - i18n: affiliate surfaces render English directly, matching the current portal/admin convention (those subtrees aren't yet wired to next-intl); a platform-wide i18n pass should cover them together. - `scripts/verify-affiliate-ledger.mjs` — 16/16 invariants pass (schema, no double-accrual/orphans, recompute parity, refund coverage, help article). - Preview-deploy errors on Vercel are environmental: the **Preview** scope is missing Supabase env vars, so prerendering pre-existing static pages (e.g. `/login`) fails. Not affiliate code. Fix: add the env vars to Vercel's Preview scope.
    Commit
    - `feat/affiliate-program` branch — Phases 1–8 (see git log). ---
  14. Super-Admin2026-06-15

    Paid platform services (P1.2) — branch `main`

    Built
    - New `platform_services` table — Wielo's own paid add-ons sold to hosts (one-time or recurring). Admin **Services** tab + CRUD (`/admin/subscriptions/services` + `[id]` editor): name, description, billing type, price, cycle, active, sort. Audited; purchases land in `platform_ledger` (service_id already wired) once the purchase flow ships.
    Migrations
    - `20260615000001_platform_services.sql`
    Commit
    - `feat(admin): paid platform services (P1.2)`
  15. Super-Admin2026-06-15

    Host-consent support access (edit gate) — branch `main`

    Built
    - `admin_support_grants` table — host-approved, time-boxed (72h) permission for Wielo support to edit a host's records. Financial tabs on the user record stay **read-only** until a grant is active. - Admin: `requestSupportAccessAction` (in-app request + host notification) + a "Request edit access" dialog on the user record's Ledger/Bookings tabs, with a banner showing read-only / pending / active-until state. - Host: `/dashboard/support-access` — approve / decline / revoke requests; an approval opens the 72h window. RLS scopes grants to the host owner.
    Migrations
    - `20260615000002_admin_support_grants.sql`
    Notes
    - The grant is general ("make changes to your account") so it also gates the upcoming business/listing support-edits. `next build` clean.
    Commit
    - `feat(admin): host-consent support access for edits`
  16. Super-Admin2026-06-15

    24h support window + transparent activity log — branch `main`

    Changed
    - Support-access window is now **24 hours** (was 72h) — auto-expires; host must re-grant. - Merged the user-record **Audit tab into Activity** — one transparent timeline. It now records the support-permission lifecycle (who requested + when, the host's decision + when, validity) alongside every admin action (with actor + acting-as flag) and the user's own activity. Governance rule recorded: staff need in-app consent before editing sensitive data.
    Migrations
    - `20260615000003_subscription_paused_status.sql` (adds `paused`/on-hold status, used by upcoming manual subscription management).
    Commit
    - `feat(admin): 24h support window + unified activity audit log`
  17. Super-Admin2026-06-15

    Manual subscription management — branch `main`

    Built
    - Subscription tab on the user record now has a **Manage subscription** action (top of the tab) → set plan, billing cycle and **status** including **paused (on hold)**, past_due, restricted, cancelled, expired — `adminUpdateSubscriptionAction` (audited; upserts the host's subscription).
    Commit
    - `feat(admin): manual subscription management on the user record`
  18. Super-Admin2026-06-15

    Table redesign sweep (host Guests style) — branch `main`

    Changed
    - Converted remaining admin data lists to the shared `AdminTable` (host Guests list style): **Hosts** and the **Subscriptions → Hosts** list (were `<ul>`). - Restyled the remaining admin table headers to match (white, uppercase muted `#8AA89C`, tracking): Audit log, Broadcasts, Notifications-sent, Platform staff, Help categories. Every admin table now shares one design. - (Ledger feeds keep the host LedgerList design intentionally.)
    Commit
    - `style(admin): unify all admin data tables to the host Guests design`
  19. Super-Admin2026-06-15

    Product Manager (Products + Wielo ledger reframe, Phase A) — branch `main`

    Built
    - `products` + `product_features` tables (and `platform_ledger.product_id`). **Product Manager** at `/admin/products` (+ Finance nav, `[id]` editor): the full workflow — name → details → price → type (subscription/once-off) → duration (weekly…annual) → **feature permissions** (per-product matrix) → **affiliate payout** (fixed/percent). Unifies the earlier Plans + Services. - Actions: upsert/toggle/delete product + per-product feature upsert (audited).
    Migrations
    - `20260615000004_products.sql`
    Notes
    - Phase A (catalog). Phase B = the Wielo quotes/invoices/credit-notes/refunds/ payments ledger keyed to products + product checkout/pay-links.
    Commit
    - `feat(admin): product manager — products + permissions + affiliate (Phase A)`
  20. Super-Admin2026-06-15

    Wielo payment settings (Paystack + EFT) + per-product methods — branch `main`

    Built
    - `platform_payment_settings` (singleton, service-role only): admin-managed platform **Paystack** keys + **EFT** bank details, at `/admin/products/payments`. Secret is write-only (never echoed back). - `products.payment_methods` — each product chooses Paystack and/or EFT; editor has the toggles. - Live billing now reads the **admin-configured Paystack secret** (DB) with env fallback (`getPlatformPaystackSecret`); the return page verifies with it; the webhook accepts either the env key (bookings) or the DB platform key (subscriptions/products).
    Migrations
    - `20260615000005_platform_payment_settings.sql`
    Commit
    - `feat(admin): wielo payment settings (paystack + eft) + per-product methods`
  21. Super-Admin2026-06-15

    Product pay-links (Paystack + EFT checkout) — branch `main`

    Built
    - `product_orders` + tokenised **pay-links** — mirrors the host booking pay-link for Wielo products. From a product, "Generate a pay-link" for a user's email → copy/send. Public `/pay/product/[token]` page: pay by **Paystack** (platform key) or see **EFT** bank details (per the product's accepted methods + platform payment settings). - Webhook gains a `product` branch — on `charge.success` marks the order paid and posts a `platform_ledger` row (idempotent on reference).
    Migrations
    - `20260615000006_product_orders.sql`
    Commit
    - `feat(admin): product pay-links + public checkout (paystack/eft)`
  22. Super-Admin2026-06-15

    Products hub consolidation — branch `main`

    Built / changed
    - Products is now the single hub (subscriptions + once-off + services). Added per product: **trial period**, **display controls** — `is_visible` (show on pricing/ signup) independent of `is_active` (purchasable): visible+active=live, visible+inactive=shown-but-disabled, hidden+active=link-only, hidden+inactive= draft — and a **slug**. - Each product gets a **standalone page** `/p/[slug]` (shareable to prospects): shows price/trial/bullets and lets them buy → checkout. Link shown in the editor. - Sidebar: removed **Subscriptions** (consolidated into Products) and **Bookings** (not needed); added a direct **Revenue** entry (Wielo ledger).
    Migrations
    - `20260615000007_products_hub.sql`
    Commit
    - `feat(admin): products hub — trial, display controls, standalone page; trim nav`
  23. Super-Admin2026-06-15

    Users/Listings parity with host Guests + nav cleanup — branch `main`

    Changed
    - Admin **Users** and **Listings** now match the host Guests/Listings design: a **KPI strip** + **segment pill-tabs** inside the table card (shared `AdminKpiCard` + `AdminSegments`; `AdminTable` gained a `topBar` slot). Users segments: All/Guests/Hosts/Staff/Suspended; Listings: All/Published/Draft/ Featured. - Nav cleanup: removed **App-wide ledger** (renamed/repointed **Ledger** → the Wielo user↔Wielo transactions ledger).
    Commit
    - `style(admin): users/listings KPI strip + segment tabs; ledger nav`
  24. Marketing2026-06-15

    In-app host pitch deck (`/pitch`) — branch `main`

    Built
    - Full-screen, slide-style **host pitch deck** at `/pitch` for stage/webinar presentations. 9 slides: hook → the marketplace tax → solution → your money → your guests → features-as-solutions → live pricing → why now → CTA. - Keyboard (→ ← space PgUp/PgDn Home/End), click-zone and chevron navigation, slide counter + top progress bar (`PitchDeck` client component). - **Features-as-benefits** slide is pain-point-driven: each card pairs a host headache (commission, losing the guest, tool sprawl, double-bookings…) with the {brand} solution. Pricing slide reads **live** from the `products` table via `getSubscriptionProducts()`; brand name from `platform_settings`.
    Changed
    - `apps/web/messages/en.json`: added `pitch` namespace (67 keys). Page marked `robots: noindex` (internal/marketing surface).
    Notes
    - No Help Centre article (internal pitch surface, not a host feature). - New locales fall back to EN per-key automatically; no translation needed now.
    Commit
    - _uncommitted — pending review_
  25. Refunds2026-06-14

    Remove refund escalation (direct-payment model) — branch `main`

    Removed
    - **The entire refund "escalation / platform adjudication" concept.** Wielo never holds or routes funds — bookings and refunds settle directly between host and guest — so a platform escalation step is meaningless. - DB (`20260614000001_remove_refund_escalation.sql`): unscheduled the `auto-escalate-refunds` cron, dropped the `escalated` partial index, removed `escalated` from the `refund_requests.status` constraint. (Left `escalated_at`/ `escalation_note`/`admin_decision`/`admin_*` columns inert to avoid cascading into the stats fn + status-history trigger — a later schema-tidy can drop them.) - Code: deleted the `RefundEscalatedAdmin` email template + its registry/resolver/ notification entries + admin email-preview fixtures; removed the "Escalated" tab/label/style + actionable check from the refunds page; dropped `escalated` from the active-refund status filters (portal trip refund, booking cancel, POPIA data export) and the guest-record finance status colour.
    Changed
    - Guest refund copy ("escalate to support afterwards") and the host refund-request email ("respond within 72h or it's escalated to Wielo") now say refunds are arranged directly between host and guest — no platform middleman.
    Notes
    - Dormant remnant left for a follow-up: the `refund_admin_override_host` email/notification + the `admin_decision` columns (admin-override path; no admin dispute UI drives it). `disputed` status value retained (distinct, unused). - `pnpm build` + lint + tsc green.
    Commit
    - `refactor(refunds): remove refund escalation (Wielo holds no funds)` ---
  26. Payments2026-06-14

    Per-business payment gateways (Phases 4–5) — branch `main`

    Built
    - **Payment gateways are now connected per business**, not per host account — mirroring how EFT banking already works. A booking charges the Paystack/PayPal of the business that owns its listing, so funds land in the right account. - Banking settings → Payment gateways: a **Business selector** (shown only when the host has >1 business) scopes the connect/test/disable/remove actions and the "Request payment" link to the chosen business.
    Changed
    - `savePaymentGatewayAction` validates the target `business_id` is owned and scopes the existing-row lookup by business; `toggle`/`test`/`delete` actions now take `(businessId, gateway, …)` and filter on `business_id`. - `createPaymentLinkAction(input, businessId?)` charges the selected business's Paystack (`getHostPaystackForBusiness`), falling back to the default business. - `paymentGatewaySchema` gains a required `business_id`; `GatewayView` carries it; `PaymentGatewayDialog` / `PaymentLinkDialog` thread the selected business. - Earlier phases (already shipped): schema `business_id` + per-business unique index (`20260614000010`), `getHostPaystackForBusiness`, business-aware lookups in `pay-booking.ts`, `/pay/[token]`, and the listing book page.
    Migrations
    - `20260614000011_help_gateways_per_business.sql` — Help Centre article updated with the "one set of gateways per business" section (idempotent re-publish).
    Notes
    - 0 gateway rows existed pre-change, so the backfill in `20260614000010` was a no-op. PayPal gets the same per-business treatment via the shared lookup. - `pnpm build` not run locally (sandbox font TLS); `tsc --noEmit` + `eslint` clean.
    Commit
    - `feat(payments): per-business payment gateways — UI + actions (Phase 4–5)` ---
  27. Super-Admin2026-06-14

    DB-driven custom plans + pricing (Pillar 1 / P1.1) — branch `main`

    Built
    - New `plans` + `plan_prices` tables — the plan catalog (name, tagline, trial days, free/paid, active, recommended, bullets, sort order) with one price row per plan × billing cycle. Public read RLS for active plans; admin writes via service role. Seeded the four current plans. - `apps/web/lib/plans/getPlans.ts` — single source of truth for the catalog (`getPlans` cached + tagged `"plans"`, `getAllPlans`, `getPlan`). Pricing is no longer hardcoded; the admin will edit it with no redeploy (P1.7).
    Changed
    - `subscription/plans.ts` reduced to shared types + `formatZar` (data removed). `PlanKey` relaxed to `string` so custom plan keys are allowed. - Subscription page + `PlanPicker` now read plans from the DB (PlanPicker takes a `plans` prop); `SettingsProfileHeader` resolves the plan name via `getPlan`. - `switchPlanAction` validates the plan against the live catalog and reads trial length per-plan (no more hardcoded 14-day / 4-tier enum) — forward-compatible with custom plans. - Replaced the hardcoded `plan IN (...)` CHECKs on `subscriptions`/`plan_features` with FKs to `plans(key)` (ON UPDATE CASCADE). Fixed the signup Business price divergence (999 → 1199) to match the canonical seed.
    Migrations
    - `20260614000020_plans_and_pricing.sql`
    Notes
    - Part of the deep Super-Admin portal build (plan: rustling-doodling-rainbow). Next: P1.3 feature-permission matrix, then P1.7 admin plan/console UI. - `tsc --noEmit` + eslint green on changed files. Migration applied to remote; types regenerated.
    Commit
    - `feat(admin): DB-driven custom plans + pricing (P1.1)`
  28. Super-Admin2026-06-14

    Feature-permission matrix + per-host overrides (P1.3) — branch `main`

    Built
    - Replaced the `/admin/platform/features` placeholder with a full plans × features **permission matrix** — toggle any feature per plan, set numeric caps on `_limit`/`_seats` features (blank = unlimited). Saves a cell at a time (optimistic, audited) via `upsertPlanFeatureAction`. - **Per-host override** creator — grant/revoke one feature for a single host (resolved by email), with optional cap + expiry and a required reason; writes `host_feature_overrides` (checked first by `check_feature_permission`). - Pre-MVP open-on-free warning banner so the founder knows toggles are stored but not yet enforced (AGENT_RULES §3.4).
    Changed
    - `withAdminAudit` target types extended with `plan` + `plan_feature`.
    Notes
    - Admin-internal surface — English-only + no Help Centre article (consistent with the rest of /admin). `tsc` + eslint green.
    Commit
    - `feat(admin): feature-permission matrix + host overrides (P1.3)`
  29. Super-Admin2026-06-14

    Admin plan editor + console tabs (P1.7, part 1) — branch `main`

    Built
    - Tabbed admin subscription console (`Hosts | Plans`) via `_SubsTabs`. - **Plans editor** — `/admin/subscriptions/plans` (cards) + `…/plans/[key]` (`new` to create). Name a plan, set tagline/description, monthly+annual price, currency, trial days, free/paid, recommended, active, selling-point bullets and sort order — applied live (busts the plans cache), no redeploy. - Actions: `upsertPlanAction` (writes `plans` + `plan_prices`), `togglePlanActiveAction`, `deletePlanAction` (blocked while hosts are on the plan) — all audited.
    Changed
    - The Hosts subscriptions list now derives its plan distribution + filter options from the live plan catalog (custom plans included), not a hardcoded 4-tier list.
    Notes
    - Remaining for P1.7: paid platform Services (P1.2), subscription Coupons (P1.4), per-host subscription management actions. Admin-internal (English-only). `tsc` + eslint green.
    Commit
    - `feat(admin): plan editor + subscription console tabs (P1.7)`
  30. Super-Admin2026-06-14

    Wielo revenue ledger (Pillar 2 / P2.1–P2.3) — branch `main`

    Built
    - New `platform_ledger` table — every **user→Wielo** transaction (subscription charges, services, refunds, manual adjustments). Signed amounts; idempotent on `provider_reference`; own-row RLS read for hosts; admin/webhook write via service role. **Not** the booking ledger (host↔guest stays untouched). - `lib/billing/vilo-ledger.ts` read model — `fetchWieloLedger` + `wieloLedgerStats` (collected/refunded/credits/net/pending), mirroring the host ledger engine. - Admin **Revenue** tab + page (`/admin/subscriptions/revenue`): KPI band (MRR, ARR, Collected, Refunded, Net, paying hosts) + transaction list + a **manual entry** form (goodwill credit / write-off / off-platform charge / correction), audited. MRR derived from active paying subs × live plan prices.
    Changed
    - `withAdminAudit` target types extended with `platform_ledger`.
    Migrations
    - `20260614000030_platform_ledger.sql`
    Notes
    - Auto-population lands with live billing (P1.5/P1.6); manual entries work now. Embed + tables verified against the live DB. `tsc` + eslint green.
    Commit
    - `feat(admin): Wielo revenue ledger (P2.1-P2.3)`
  31. Super-Admin2026-06-14

    Live host billing on Wielo's platform Paystack (P1.5) — branch `main`

    Built
    - `lib/billing/platform-billing.ts` — `startSubscriptionCheckout` charges a host for a paid plan on **Wielo's platform Paystack key** (never the host's own key; booking rails untouched). Inserts a pending `platform_ledger` row keyed by the reference (idempotency) and returns the Paystack URL. - `startPlanCheckoutAction` — decides server-side: first trial → start trial (no charge); charge due → Paystack checkout; **billing not configured → state-only** (pre-MVP smoke-testing preserved). PlanPicker redirects paid switches to Paystack. - Post-checkout **return page** (`…/subscription/billing/return`) verifies the transaction (defence-in-depth) and shows success/pending/failed. - **paystack-webhook subscription branch** — discriminates on `metadata.purpose` (booking path byte-identical). On `charge.success`: completes the ledger row (or inserts one for renewals), activates the subscription for the period, writes `subscription_history` with the amount. On `charge.failed`: marks the row failed, sets `past_due` + 5-day grace. - Host **Billing history** section on the subscription page (own-row RLS).
    Notes
    - **Capability-gated on the platform `PAYSTACK_SECRET_KEY`** — everything is built and inert until the founder adds the key (then it goes live with no code change). - Deferred: Wielo→host VAT invoice generation (P1.6) + native Paystack recurring Subscriptions/dunning cron (next increment). `tsc` + eslint green.
    Commit
    - `feat(admin): live host subscription billing (P1.5)`
  32. Super-Admin2026-06-14

    Wielo User Record + direct-edit (P3.1–P3.3) — branch `main`

    Built
    - Redesigned `/admin/users/[id]` into a role-adaptive **Wielo User Record** (mirrors the host Guest Record): identity header + badges (role, suspended, deleted, passwordless), stat band, and tabs — **Overview, Activity, Finances, Subscription** (hosts), **Notes, Audit**. - **Finances tab**: a host's booking ledger KPIs (reusing `fetchHostTransactions` + `txnStats`, not forked) **and** their Wielo account (what they've paid Wielo, from `platform_ledger`). - **Direct-edit powers** (all audited): edit profile, change role, suspend/ reinstate, **soft-delete** (never hard-delete), and internal admin notes — via canonical `FormModal` dialogs. - New `admin_user_notes` table (admin-only) + new permission keys `users.role` / `users.delete` seeded + granted to super_admin.
    Changed
    - Removed the standalone `SuspendDialog` (folded into the record toolbar).
    Migrations
    - `20260614000040_admin_user_notes.sql`
    Notes
    - Guest cross-host ledger + write "act as" are the next P3 increments. Embeds + permission seed verified against the live DB. `tsc` + eslint green.
    Commit
    - `feat(admin): wielo user record + direct-edit (P3.1-P3.3)`
  33. Super-Admin2026-06-14

    Business reporting dashboard (P4.2) — branch `main`

    Built
    - New `/admin/reporting` (+ Finance sidebar entry): Wielo-as-a-business overview — **Revenue** (MRR, ARR, all-time collected, paying hosts, trials, churned), **Growth** (total users, hosts, guests, new in 30 days), **Platform volume** (GMV processed + revenue-booking count, clearly flagged as host↔guest money Wielo never holds), and live **plan distribution**.
    Notes
    - Computed inline from existing tables + the revenue read model (no new schema). Full platform-analytics RPC suite + CSV/scheduled-report exports (P4.1/P4.3) are the next increment. `tsc` + eslint green.
    Commit
    - `feat(admin): business reporting dashboard (P4.2)`
  34. Super-Admin2026-06-14

    Unified Users hub + Guest-Record-style user record — branch `main`

    Built
    - **One Users hub** — removed the separate "Hosts" sidebar item; `/admin/users` is now the single home for every Wielo user (hosts + guests + staff), with role pills + filter by name/type. Host pages still exist at their routes. - Rebuilt `/admin/users/[id]` to mirror the **host Guest Record** design (sticky dossier + working column + RecordTabs), customised for super-admin management. Tabs: Overview, Subscription, Bookings (as guest + as host), **Ledger** (their booking ledger via the shared `LedgerList` + their Wielo account), Listings, Business, Reviews (written + received), Relationships (travelled-with), **Referrals** (placeholder for the coming referral graph), Support (data/privacy requests), Activity log, Notes, Audit. - Dossier quick-actions: edit profile, change role, suspend/reinstate, soft-delete, and **View as host** — all audited.
    Changed
    - Collapsible sidebar groups (shipped earlier) + Hosts merged into Users.
    Notes
    - **Design rule:** super-admin surfaces reuse the host dashboard component designs (LedgerList, RecordTabs, dossier layout) — only data + permissions differ. - Full `next build` compiles clean (the async-server-action build break is fixed).
    Commit
    - `feat(admin): unified users hub + guest-record-style user record`
  35. Super-Admin2026-06-14

    Two ledgers: app-wide + Wielo revenue (filters) — branch `main`

    Built
    - **App-wide ledger** (`/admin/ledger`, new Finance nav item) — a view-only, continuously-running ledger of every booking transaction across the platform (host ↔ guest money) for owner oversight. KPI band (collected / refunded / net processed) + filters by user email, status, and (deep-linkable) listing. Host table style (AdminTable). - **Wielo revenue ledger** now has matching **filters** — by user email, plan, type (charge/refund/credit/adjustment) and status.
    Notes
    - Two distinct ledgers, as intended: **Wielo ledger** = Wielo's own revenue (subscriptions/services); **App-wide ledger** = all user/booking money Wielo never holds. Embeds verified live; full `next build` compiles clean.
    Commit
    - `feat(admin): app-wide ledger + wielo ledger filters`
  36. Super-Admin2026-06-14

    Control-centre overview + host-style tables — branch `main`

    Built
    - Refined `/admin` into a **Control Centre**: headline KPIs (MRR, paying hosts, total users, platform collected, listings, bookings), a **Needs attention** row (past-due subs, pending refunds, open data requests — only shows non-zero), a users-at-a-glance row, and recent admin activity in the host table style. - Converted the remaining admin list tables — **payments, bookings, listings** — to the shared `AdminTable` (host table style), matching users + the app-wide ledger. Every admin list now looks consistent.
    Notes
    - Per the design rule, admin reuses host designs (AdminTable). `next build` clean.
    Commit
    - `feat(admin): control-centre overview + host-style tables`
  37. Reviews2026-06-13

    Guest Reputation: hosts rate guests (cross-host) — branch `main`

    Built
    - **Host → guest ratings**, the mirror of guest→listing reviews. A new **Reputation** tab on the Guest Record shows a cross-host aggregate (overall + 5 dimensions: Payments, Communication, Cleanliness, House rules & respect, Integrity), the host's own editable review, and other hosts' reviews (anonymised "A verified host"). - **`guest_ratings`** table — one living review per host per guest (`UNIQUE(host_id, guest_id)`), keyed on the guest's Wielo account id. **Cross-host RLS:** any active host may READ every host's rating of a guest (shared reputation network); each host may only INSERT/UPDATE/DELETE its own row. **No guest policy** → guests never see it; no notifications. - **Rate-a-guest modal** (`FormModal` + `CategoryStars`) — overall star (required) + summary + optional per-dimension scores with short notes. Eligibility gated to a **completed** or **no-show** stay, enforced in `hostCanRateGuest` (shared by page + action) AND in RLS. - Help article `how-guest-ratings-work` (audience `host`).
    Changed
    - Extracted the interactive `CategoryStars` star input from `ReviewSubmissionForm` to a shared `components/reviews/CategoryStars.tsx` (single source of truth); the guest review form now imports it.
    Migrations
    - `20260613000020_create_guest_ratings.sql` - `20260613000021_help_guest_ratings.sql`
    Notes
    - Email-only / OTA guests (no `u_` account) aren't rateable — the tab shows a "no Wielo account yet" state. - **Verify (founder, needs 2 host accounts + 1 shared guest with a completed stay):** host A rates → host B sees it under "Other hosts" and the aggregate reflects both; B can add but not edit A's; a guest token reads **zero** `guest_ratings` rows (RLS). - Followed the existing Guest Record convention of inline English copy (the whole record is not yet i18n-wired); a guests-dashboard i18n sweep is a separate task. No feature gate added — it's open to all hosts (pre-MVP "open on free").
    Commit
    - `feat(reviews): host → guest cross-host reputation (guest_ratings + Reputation tab)` ---
  38. Finance2026-06-13

    Ledger ↔ multi-business: per-business filter (Phase 1 + Ledger) — branch `main`

    Built
    - **Ledger is now business-aware.** Each `Txn` carries a derived `businessId` (from `booking → listing → business_id`, one batched lookup — business is never stored on transaction rows; the listing stays the single source of truth). `fetchHostTransactions` gains a `businessId` filter; running balances are computed within the filtered scope. - **"All businesses / Business…" selector on the Ledger** (`/dashboard/ledger`), shown only when the host has more than one business. It's a server-side scope (drives `?business=`, re-fetches) so per-business KPIs and running balances are correct, not just row-hiding. Header subtitle reflects the active business.
    Notes
    - Plan saved at `LEDGER_MULTIBUSINESS_PLAN.md`. Confirmed all finance *documents* already render the listing's business (no work needed there). Decisions locked: derive business via listing (no new columns); store credit will be per-business.
    Also built (Phase 2b — Guest Record filter)
    - The same **business filter on the Guest Record Finances tab** — a selector (shown only when this guest engaged >1 business) scopes the transaction rows + their running balance via `?business=`. The guest's **headline net balance stays all-businesses** by design (with an on-screen note when a filter is active). Business options derive from the guest's bookings' listings.
    Also built (Phase 3 — per-business store credit)
    - `business_id` on `guest_credit_ledger` (`20260613000022`). A BEFORE INSERT trigger auto-attributes each credit row to its booking's business (`booking_business_id()` — listing = SSOT), so the five credit write-paths (overpayment auto-post, apply-credit, manual credit note, credit-note void) are untouched; existing rows backfilled. Store credit is now attributable to a business; the guest's headline balance still sums all businesses.
    Migrations (this strand)
    - `20260613000022_guest_credit_business.sql`
    Commit
    - `feat(finance): per-business ledger filter (Txn.businessId + Ledger selector)` - `feat(finance): per-business filter on the Guest Record Finances tab` ---
  39. Phase 1 (Multi-business)2026-06-12

    Data foundation — branch `main`

    Built
    - **`businesses` table** — promotes "business" from the 1:1 `host_business_details` extension to a first-class entity (1 host → many businesses). Holds legal/trading name, VAT, company reg, a listing-style address (incl. lat/lng for the LocationPicker), logo, `default_currency`, `default_language`, plus `is_default`/`is_archived`. Partial unique index = one default per host; owner-only RLS. - **`host_personal_details`** — private 1:1 table for the account holder's physical address. Internal use only; owner-only RLS, never selected by guest/public paths. - **`guest_business_links`** — M:N join (host_contact ↔ business). Keeps one canonical guest record per host while tagging which businesses each guest has engaged.
    Changed
    - `listings.business_id` and `eft_banking_details.business_id` added, NOT NULL, backfilled to each host's default business. Banking's default index moved from per-host to **per-business**. - Two invariant triggers: `on_host_created_default_business` (AFTER INSERT ON hosts) and `set_listing_default_business` (BEFORE INSERT ON listings) guarantee every host has a default business and every listing has a `business_id` on all code paths. - Backfilled one default business per existing host (mapping `billing_*` → `address_*`), assigned all listings/banking to it, and linked guests to businesses from existing bookings.
    Migrations
    - `20260612000001_multi_business_foundation.sql`
    Notes
    - `host_business_details` is intentionally **kept** as the live read/write source until Phase 3 (documents switch to resolve from the listing's business, then it's dropped). - `businesses.default_currency` is the **settlement/listing** default (inherited into `listings.currency`); it does **not** touch the viewer display layer (`vilo_display_ccy` / `displayAmount()`). `default_language` is the per-business locale (next-intl `en/af/fr/de/pt`) for future guest-facing doc/email localization — stored now, wiring owned by the currency/i18n effort. - Types regenerated; `pnpm lint` + `pnpm build` green.
    Commit
    - `feat(business): phase 1 — businesses table + listing/banking/guest links + invariant triggers` ---
  40. Phase 2 (Multi-business)2026-06-12

    Businesses management centre — branch `main`

    Built
    - **New "Businesses" settings tab** (`/dashboard/settings/businesses`) — lists each business as a saved-data card (default badge, address, currency · language, listing count) with Edit, Set-as-default, and Archive (confirm via the canonical Modal). "Add business" → full-page form. - **Business add/edit form** — identity (name, legal name, VAT, company reg), the **LocationPicker** address UX (keyless OSM/Photon map + search, reused from the listing editor), default currency (from `DISPLAY_CURRENCIES`) and default language (next-intl `en/af/fr/de/pt`) selects, and a per-business logo uploader. - **Per-business banking** — each business's `eft_banking_details` are managed on its detail page; the default account prints on that business's documents. `BankAccountList`/`BankAccountDialog` now take an optional `businessId`. - **Private personal-address card** — writes `host_personal_details` via the LocationPicker; clearly labelled "never shown to guests". - New `businesses` settings server layer (create/update/archive/set-default, per-business logo, personal address) + `lib/business/resolveBusiness.ts`.
    Changed
    - EFT banking actions are now **business-scoped**: new accounts attach to a chosen business (or the host's default); the per-business default index is honoured. - The old "Banking & business" tab is now **"Card payments"** — pared down to the account-wide card gateways (Paystack/PayPal). Business identity + EFT banking moved to the Businesses tab (single source of truth per business). - New `settings` + `businesses` i18n namespaces (en + af); SettingsTabs labels wired through next-intl.
    Migrations
    - `20260612000002_help_businesses.sql` (Help Centre article "Managing multiple businesses")
    Notes
    - **Interim window:** financial documents still read the frozen `host_business_details` snapshot until Phase 3 switches them to resolve from the listing's business. Editing a business here updates `businesses`; documents catch up in Phase 3 (which also drops `host_business_details`). No real users, so this window is safe. - `BusinessDetailsForm`/`LogoUploader` and the host-level `saveBusinessDetailsAction`/`uploadHostLogoAction` are now unused — removed in Phase 3. - `pnpm lint` + `pnpm build` green; the three new routes compile.
    Commit
    - `feat(business): phase 2 — businesses management centre (settings)` ---
  41. Fix2026-06-12

    LocationPicker town vs. municipality + English default + dropdown z-index — branch `main`

    Fixed
    - 🔴 **Address picker filled the town with the local municipality** (and pushed the town name into the street line). In SA, OSM/Photon returns the municipality in the `city` slot (e.g. "Sabie" → city="Thaba Chweu Local Municipality", name="Sabie"). `LocationPicker` now extracts the real settlement (the `name` for a place node, else a non-municipality `city`/`locality`/`county`), keeps the municipality out of the town field, and no longer dumps a town/suburb name into the street address. - **Autocomplete suggestions hidden behind the map** — the Photon dropdown was at `z-10`, below Leaflet's panes/controls; raised to `z-[1100]`. - **App defaulted to Afrikaans** — `localeDetection: false` in the next-intl routing so an unprefixed URL stays English (no Accept-Language / stale-cookie auto-redirect); manual switching still works.
    Added
    - Optional **Local municipality** field on the business + personal address forms (the picker now captures it separately). New `businesses.municipality` / `host_personal_details.municipality` columns.
    Migrations
    - `20260612000003_business_municipality.sql`
    Commit
    - `fix(location): extract real town, capture municipality separately` ---
  42. Fix/polish2026-06-12

    Businesses card: set-default refresh, banking pill, logo — branch `main`

    Fixed
    - **"Set as default" didn't update the UI** — `BusinessesList` is a client component holding the list as props; the action updated the DB but the card never re-fetched, so the Default badge didn't move. Added `router.refresh()` after set-default and archive.
    Added
    - **Banking indicator pill** on each business card — green "Bank account" when the business has a non-archived EFT account, amber "No bank account" otherwise. - **Business logo on the card** — the card avatar shows the business logo when one is uploaded, falling back to the building icon.
    Commit
    - `fix(business): refresh card on set-default; show banking pill + logo` ---
  43. Phase 3a + Phase 4 (Multi-business)2026-06-12

    docs resolve from business; listing→business assignment — branch `main`

    Built (Phase 4)
    - **Business selector in the listing editor** (Basic info tab) — assign a listing to any of the host's businesses. New `assignListingBusinessAction` validates the chosen business belongs to the listing's host. The owning business's identity/banking/currency then drive that listing's quotes + invoices.
    Built (Phase 3a — documents resolve from the listing's business)
    - `ensure_booking_invoice` (`20260612000004`) snapshots the booking's listing → business identity + that business's default banking (same `host_snapshot` keys → PDF templates untouched). - `getHostParty` + `hostLogoDataUri` now read the `businesses` table (+ business banking/logo) with a default-business fallback and accept a `businessId`. - Invoice, credit-note, quote, pay and receipt pages — plus both quote PDF routes — pin the document's listing business, so a guest sees the right company, banking and logo.
    Migrations
    - `20260612000004_invoice_business_source.sql`
    Notes
    - Deferred to 3b (cosmetic/cleanup): per-business document numbering, addon-invoice business snapshot, dropping the now-dead `host_business_details`.
    Commit
    - `feat(business): phase 4 — assign listings to a business` ---
  44. Fix2026-06-12

    Generate-quote: explicit "pull in an existing guest" + search the Guests directory — branch `main`

    Fixed
    - The quote form's returning-guest search only looked at **past bookings**, so guests added to the Guests directory (host_contacts) with no booking yet never appeared. `searchGuestsAction` now searches **both** bookings and `host_contacts`, merged by email.
    Added
    - An explicit **"Pull in an existing guest"** search field at the top of the Guest section on the quote form — picking a result fills name/email/phone. (The name-field autocomplete still works too.)
    Commit
    - `fix(quotes): explicit existing-guest picker + search Guests directory` ---
  45. Phase 5 (Multi-business)2026-06-12

    Signup step 3: LocationPicker + seed the first business — branch `main`

    Built
    - Signup **step 3** now uses the **LocationPicker** (map + search, same UX as the listing editor) for the property address, capturing latitude/longitude and auto-filling city/province/postal on pick. - New **Business name** field on step 3. `finalizeOnboardingAction` enriches the auto-created default business (from the host-insert trigger) with that name + the listing's address + lat/lng, and the first listing now stores lat/lng. Blank business name falls back to the host's display name.
    Notes
    - The default business is still created by the `on_host_created_default_business` trigger; finalize just enriches it. The first listing's `business_id` is set by the `set_listing_default_business` trigger.
    Commit
    - `feat(business): phase 5 — signup step 3 LocationPicker + seed default business` ---
  46. UX2026-06-12

    App-wide required-field validation highlight — branch `main`

    Built
    - **Red border on invalid fields, app-wide.** A global `[aria-invalid="true"]` CSS rule in `globals.css` styles any invalid form control. shadcn's `FormControl` already sets `aria-invalid` on RHF errors, so all dialog/RHF forms get it automatically; the signup `FormField` and dashboard `Field` wrappers now inject `aria-invalid` on the child control when a submit fails. - **Required-field star.** `FormField`/`Field` show a red `*` when `required`. Marked the required signup step-3 fields (listing name, property type, street, city, postal).
    Notes
    - Mechanism is now app-wide; individual forms should pass `required` to mark their required fields (the star) — the red-border-on-submit works wherever a field error is surfaced.
    Commit
    - `feat(ux): app-wide required-field star + red invalid border` ---
  47. Fix2026-06-12

    Finish-setup seeds business details from the default business — branch `main`

    Fixed
    - The finish-setup flow read/wrote business details on the deprecated `host_business_details`, so the name + address captured at signup (now seeded onto the `businesses` default) showed blank, and edits there didn't reach documents. The setup page now **reads** the default business (aliased to the form's `billing_*` shape) and `saveBusinessDetailsAction` **writes** the default business — read + write consistent with the rest of the app.
    Commit
    - `fix(setup): seed + save business details from the default business` ---
  48. UX2026-06-12

    Finish-setup: single nav + instant "Saving…" feedback — branch `main`

    Fixed
    - **Duplicate Continue/Back buttons** in the finish-setup flow — the wizard footer now shows a single global **Back** (+ Publish on the review step); each step keeps its own contextual Continue/Save. - **Stuck-feeling saves.** New reusable `BusyOverlay`; step saves route their refresh through a transition so a "Saving your details/room/policy…" overlay stays up until the refreshed UI commits. - **Bank accounts now refresh immediately** on the per-business detail page (previously needed a reload) — `BankAccountList` self-refreshes when no parent `onChanged` is supplied, and shows the overlay. Businesses list set-default/archive show it too.
    Commit
    - `feat(ux): single setup nav + BusyOverlay for instant save feedback` ---
  49. Currency (Phase 1b)2026-06-11

    `<Money>` + activate the display switcher — branch `main`

    Built
    - **`<Money amountZar={…}>`** (`components/currency/Money.tsx`) — the missing render piece from phase 1a. Wraps `useCurrency()`, converts a base-ZAR amount into the viewer's selected display currency, and prefixes non-ZAR with "≈" to signal it's a browsing estimate. Reuses the context's `convert`/`format` — no forked money maths.
    Changed
    - Committed the dangling phase-1a wiring: `CurrencyProvider`/`CurrencySwitcher` (was untracked), root `layout.tsx` (injects `getDisplayRates()` + reads the `vilo_display_ccy` cookie), and `SiteHeader` (the canonical `CurrencySwitcher`). - Removed the redundant dead "ZAR · R" placeholder button from `UtilityBar` — `SiteHeader`'s switcher is the single canonical currency control (it has site-wide reach; `UtilityBar` only renders on home + listing). The language placeholder stays until L-B. - `lib/fx.ts`: FX cache refresh cadence daily → **hourly** (`STALE_MS`), still cached (never per-view), source unchanged (`open.er-api.com`), admin override intact.
    Notes
    - Display conversion is browsing-only; transactional/host amounts stay in settlement currency via `formatMoney`. Next: C2 — wire `<Money>` into listing/room/`[handle]` browsing prices. - `tsc --noEmit` clean, `next lint` clean. `pnpm build` not run here — sandbox blocks Google Fonts fetch (TLS); unaffected by these changes (builds on Vercel/normal network). - Full roadmap: see the multi-currency + multi-language plan.
    Commit
    - `feat(currency): phase 1b — <Money> + activate display switcher, hourly FX`
    Fixed
    - 🔴 **Migration `20260610180008` (guest directory email-merge) couldn't apply and blocked the whole queue** (and any `supabase db push` / deploy), including the trailing policy + help migrations. Root cause: it changed the `RETURNS TABLE` shape of `_host_guest_rows` with `CREATE OR REPLACE` (Postgres `42P13: cannot change return type` — needs `DROP FUNCTION` first), and in doing so dropped the `is_added_guest` column + its `addedrel` CTE that `20260610150003` added (which the reader RPCs `fetch_host_guests*` depend on), and the `REVOKE … FROM PUBLIC` on a SECURITY DEFINER function. - Fixed forward (kept the dedup feature): added `DROP FUNCTION IF EXISTS _host_guest_rows(uuid)`, restored the `is_added_guest` column / `addedrel` CTE / `hc_id` join and the `REVOKE`, with the email-merge logic layered on top. An added guest whose email resolves to an account that has bookings simply stops counting as "added" — the two compose correctly.
    Notes
    - All pending migrations now apply: `180008`, `180009`, `180010` are live on the remote; `migration list` is fully in sync. Types regenerated (no diff — `_host_guest_rows` is internal/REVOKEd). - Verified: `tsc` clean, policy resolver verifier green (4/4), and `fetch_host_guests` + `fetch_host_guests_summary` (which read `is_added_guest`) both return without error. ---
  50. Currency (C2)2026-06-11

    convert listing/room browsing prices — branch `main`

    Built
    - **Source-aware conversion core.** `displayAmount(amount, sourceCurrency, display, rates)` in `lib/currency.ts` is the single rule: only **ZAR** amounts convert (we hold ZAR-base rates only); any non-ZAR settlement amount renders **natively** via `formatMoney` — never a false cross-conversion. `<Money>` API changed `amountZar` → `amount` + `currency` (no external callers yet). Added `formatFrom(amount, sourceCurrency?)` to the currency context for labels/template literals where a `<Money>` JSX node can't go (used by client widgets).
    Changed
    - Wired browsing-price conversion into the listing area: `RatesSection` (rate card, cleaning fee, weekend, extras), `ReservePanel` (From … /night), `SimilarListings` cards, `RoomsInfoGrid` cards, `RoomBookingWidget` (headline + live breakdown + Reserve label), and `[handle]` host-profile listing cards. Converted values carry an "≈" estimate marker.
    Notes
    - **`BookingForm` (the `/book` flow) deliberately left in settlement currency** — it's transactional (what's charged). Conversion is browsing-only. - Known follow-up: a few server-rendered **prose** prices stay ZAR for now (`SuitabilityChips` chip text, `rooms/[roomId]/page.tsx` `pricingLine`, the unused `roomPriceLabel`) — converting them needs a server→client refactor; the prominent interactive/card/rate prices all convert. - `tsc --noEmit` clean, `next lint` clean. Next: C3 — explore/browse/featured/category cards.
    Commit
    - `feat(currency): C2 — convert listing/room browsing prices via <Money>`
  51. Currency (C3)2026-06-11

    convert discovery cards + estimate note — branch `main`

    Built
    - **`FxEstimateNote`** (`components/currency/FxEstimateNote.tsx`) — shows "Prices shown in X are estimates — you'll be charged in ZAR" **only** when the displayed price actually differs from the charge (settlement is ZAR **and** a non-ZAR display is selected). Non-ZAR-settled listings render natively, so no note. Placed at the booking entry point (`ReservePanel`).
    Changed
    - Converted discovery-card prices: `BrowseResults` (explore + category results), `c/[slug]` category page, and homepage `FeaturedListings`. For the home cards, `home-data.ts` now carries raw `priceAmount`/`priceCurrency`/`fromLabel` on `HomeListingCard` (instead of a prebuilt string) so the card can convert via `<Money>`.
    Notes
    - Region/destination teaser "from R X" stats (`home-data.ts` line ~334) intentionally left in ZAR for now (aggregate teaser, not a per-listing price). - `tsc --noEmit` clean, `next lint` clean. Currency display layer (C1–C3) complete for guest browsing. Next: Part 2 — language (next-intl), starting L-A infra.
    Commit
    - `feat(currency): convert discovery cards + add fx estimate note (C3)`
  52. Language (L-A)2026-06-11

    next-intl infra + [locale] restructure — branch `main`

    Built
    - **next-intl 3.26 wired** (URL-based routing, `localePrefix: "as-needed"` → English keeps its current URLs, others get `/af /fr /de /pt`). Config in `i18n/{routing,request,navigation}.ts`; `next.config.mjs` wrapped with the plugin. Message catalogs in `messages/{en,af,fr,de,pt}.json` (en is the source; non-en fall back to en). - **App tree moved under `app/[locale]/`** (529 files) — every UI route. Route handlers stay flat: `api/`, `auth/confirm`, `ical/`, `unsubscribe/`, `quote/*/pdf`. Root layout → `app/[locale]/layout.tsx`: dynamic `<html lang>`, `setRequestLocale`, `generateStaticParams`, `NextIntlClientProvider` wrapping the existing Brand/Currency providers. Added `app/[locale]/not-found.tsx`. - **Middleware composed**: next-intl runs first for UI routes (honouring its locale redirects), then Supabase `updateSession` attaches refreshed auth cookies to the same response; functional routes get Supabase only (no regression). `updateSession` strips the locale prefix before auth-gate matching so `/af/dashboard` gates like `/dashboard`. - Reserved the locale codes in `[handle]` so a host vanity handle can't be shadowed by the `[locale]` segment.
    Changed
    - Mechanical import rewrites for the move: `@/app/{dashboard,(auth),signup,help,explore}` → `@/app/[locale]/…`; relative top-level `_components/{home,legal,browse}` imports → absolute `@/app/_components/…`.
    Notes
    - **No visible change intended** (en-only passthrough; no strings translated yet). `tsc --noEmit` + `next lint` clean. **`pnpm build` NOT verifiable in this environment** (Avast HTTPS-scanning blocks the Google-Fonts fetch the build needs) — must be built + smoke-tested locally. Watch for next-intl static-rendering opt-outs and the `x-pathname` header propagating through the i18n rewrite (dashboard inbox full-bleed). Next: L-B (hreflang + sitemap + language switcher), then translate surface-by-surface.
    Commit
    - `feat(i18n): next-intl infra + [locale] restructure (L-A)`
  53. Language (L-B)2026-06-11

    language switcher — branch `main`

    Built
    - **`LanguageSwitcher`** (`components/i18n/LanguageSwitcher.tsx`) — compact picker (English/Afrikaans/Français/Deutsch/Português) that navigates to the same page in the chosen locale via next-intl's locale-aware router and persists the `NEXT_LOCALE` cookie. Added to `SiteHeader` beside the currency switcher (the single canonical controls, site-wide).
    Changed
    - Removed the dead "English (SA)" placeholder button from `UtilityBar` — language + currency now live only in `SiteHeader`, consistent with the currency decision.
    Notes
    - **Deferred: sitemap `hreflang` alternates.** Advertising `/af`, `/fr`, … while they still serve English fallback content would create duplicate-content signals. hreflang lands per-locale as real translations ship. - `tsc` + `lint` clean. Next: translate surface-by-surface (L-C onward), starting with the marketing shell.
    Commit
    - `feat(i18n): language switcher in the header (L-B)`
  54. Language (L-B fix)2026-06-11

    locale-aware links + switchers in top bar — branch `main`

    Fixed
    - **Internal links dropped the locale** (clicking the logo or nav links on `/af/…` went back to the unprefixed English URL). Swept all 172 `import Link from "next/link"` → `import { Link } from "@/i18n/navigation"` across `app/` + `components/`, so every internal link preserves the active locale (en stays unprefixed under `as-needed`).
    Changed
    - **Switchers moved to the top utility bar** (above the main header), per design. `UtilityBar` now hosts the language + currency switchers (new `variant="dark"` on both, styled for the dark strip) and is rendered by `SiteHeader`, so it appears on every public/guest page. Removed the switchers from the main nav row and the now-duplicate standalone `<UtilityBar/>` from the home + listing pages.
    Notes
    - Verified by founder: the [locale] restructure **builds and runs on Vercel**; `/af/dashboard` auth-gate redirect works (`/af/login?next=/af/dashboard`). - Not yet migrated: programmatic navigation (`useRouter().push`, server-action `redirect()`, `next/navigation` `usePathname`) still uses the non-locale APIs — fine for now; migrate per-flow if a specific redirect is seen dropping the locale. `booking-management`/`change-log` use a separate header and don't show the top bar yet. - `tsc` + `lint` clean.
    Commit
    - `fix(i18n): locale-aware internal links + switchers in top utility bar`
  55. Language (L-C·1)2026-06-11

    translate global chrome (top bar + header) — branch `main`

    Built
    - **First translated surface.** Added a `nav` namespace to all five catalogs (`messages/{en,af,fr,de,pt}.json`) and wired `useTranslations("nav")` into `UtilityBar` (tagline, "List your property", "Help") and `SiteHeader` (nav links, search pills, "Sign in", "Join {brand}", tagline). Switching the language now visibly translates the chrome on every public page. `"Join {brand}"` uses ICU interpolation off the dynamic brand name.
    Notes
    - Non-English copy is a **solid draft — flag for native-speaker review before launch** (Afrikaans/French/German/Portuguese). I'm not passing these off as final professional translations. - Catalogs use complete namespaces per locale (request.ts shallow-merges over English); switch to deep-merge before shipping partial namespaces. - Logged-in `UserMenu` strings + footer/hero/home sections come in the next L-C slices. `tsc` + `lint` clean.
    Commit
    - `feat(i18n): translate global chrome — nav namespace (L-C·1)`
  56. Language (L-C·2)2026-06-11

    translate site footer — branch `main`

    Built
    - Added a `footer` namespace to `en.json` and wired `getTranslations("footer")` into `SiteFooter` (server component): tagline, all four column titles + links (Explore/Guests/Hosts/Company), and the bottom legal row. "How {brand} works" uses ICU interpolation off the dynamic brand name.
    Notes
    - **English-only extraction going forward** (decided): non-English values fall back to English until the planned admin Translations portal + bulk JSON import fills them (export keys → AI-translate → upload → native-speaker review). The `nav` namespace remains the 5-language sample. - `tsc` + `lint` clean.
    Commit
    - `feat(i18n): translate site footer — footer namespace (L-C·2)`
  57. Fix2026-06-11

    per-person room prices missing on listing page — branch `main`

    Fixed
    - **Room prices didn't show on the listing page for `per_person` rooms** (and showed "R 0" in the rate card). Root cause: `RoomsInfoGrid` + `RatesSection` read `room.base_price` only, but a per-person room keeps its rate in `price_per_person` (base_price is 0). Confirmed via live DB (room "Kanarie Main": per_person, base_price 0, price_per_person 699). Both now use the canonical `roomFromNightly(room)` helper (per_person → price_per_person, else base_price), so the real "from" price shows. Room cards label it "/ person · night" and the rate card tags the row "priced per person". Pre-existing gap — not the C2 currency swap.
    Notes
    - Same underlying gap likely affects the **discovery "from" prices** (explore/similar/featured/`[handle]` cards) for per-person `rooms_only` listings, which still min() over `room.base_price`. Can fix next (use `listing.base_price`, which the room-recompute already sets correctly, or extend those selects). - `tsc` + `lint` clean.
    Commit
    - `fix(listing): show per-person room prices via roomFromNightly`
  58. Fix2026-06-11

    per-person "from" prices on discovery cards — branch `main`

    Fixed
    - Follow-up to the listing-page fix: explore (`BrowseResults`), category (`c/[slug]`), homepage featured (`home-data`), similar-stays (`SimilarListings`), and host-profile (`[handle]`) cards derived the "from" price as `min(room.base_price)`, which is 0 for per-person rooms → no price on the card. All now read `listing.base_price`, which `recomputeListingFromRooms` already maintains as the effective cheapest rate (per-person aware). `fromLabel` still keys off `rooms_only`.
    Notes
    - `tsc` + `lint` clean.
    Commit
    - `fix(cards): use listing.base_price for per-person "from" prices`
  59. Language (L-C·3+4)2026-06-11

    full homepage — branch `main`

    Built
    - Translated the entire public homepage into the `home` namespace via `getTranslations("home")`: **Hero** (badge, headline, search bar labels/placeholder/guest options/aria, popular cities, trust row — subtitle is an ICU plural on the verified-property count) and **all sections** — `CategoryChips`, `TrendingDestinations`, `RecentReviews`, `BrowseByType`, `DealsBanner`, `TrustPillars`, `HostCTA`, `AppNewsletter`, `FeaturedListings`. Brand-dependent copy uses `{brand}` ICU values.
    Notes
    - Several home sections became `async` server components to call `getTranslations`. English source only; other locales fall back until the admin portal bulk-import. Per `RULES.md §10`. - `tsc` + `lint` clean. Next batch: listing + booking flow.
    Commit
    - `feat(i18n): translate rest of homepage sections (L-C·4)`
  60. Language (L-C)2026-06-11

    Afrikaans copy for the public landing + per-key fallback — branch `main`

    Built
    - **Afrikaans translations** for the `footer` + `home` namespaces (draft — native review pending), so switching to Afrikaans now visibly translates the entire public landing (chrome + footer + full homepage), not just the header. French/German/Portuguese still fall back to English until filled. - **request.ts now deep-merges** the locale catalog over English (was a shallow spread). This makes per-key English fallback actually work — a locale can translate *some* keys in a namespace and the rest render in English. Essential for partial translations / the portal bulk-import and for new English keys added later (Rule §10).
    Notes
    - `tsc` + `lint` clean; `af.json` validated.
    Commit
    - `feat(i18n): Afrikaans copy for public landing + per-key deep-merge fallback`
  61. Language (L-C·5)2026-06-11

    listing page (slice 1: reserve panel) — branch `main`

    Built
    - Started the `listing` namespace (en + **Afrikaans**, per "fill Afrikaans as we go"): `ReservePanel` (From / per-night / price-on-request / instant book / reserve / not-charged-yet / held-securely) made `async` with `getTranslations`, plus the listing page's inline strings (Availability heading + body, the quote-button trigger labels, the cancellation-note fallback).
    Notes
    - Listing page is large — remaining slices: `ListingBody` section headings, `RatesSection`, `SuitabilityChips`, `HostCard`/`TrustCard`, `ListingHero`, `RoomsInfoGrid` labels, `RequestQuoteButton` modal; then the booking flow. - `tsc` + `lint` clean.
    Commit
    - `feat(i18n): listing page reserve panel + page strings, en+af (L-C·5)`
  62. Listing2026-06-11

    redesign "Things to know" + i18n — branch `main`

    Changed
    - **Reworked the "Things to know" block** (`components/policy/ThingsToKnow.tsx`) per founder feedback — it was too compact/confusing (three bare columns of tiny text). Now three clean, on-brand **cards** (House rules / Safety & property / Cancellation) with an icon-badge header and roomier rows; the platform legal line moved to a full-width footer. Same data + policy dialogs, clearer hierarchy. - Wired all its strings through i18n (en + Afrikaans) into a new `thingsToKnow` namespace, including ICU plurals (guests/nights/refund-rule days) and a `t.rich` legal line with terms/privacy links — per `RULES.md §10`.
    Notes
    - `tsc` + `lint` clean. `PolicyDialog`'s own "Read full policy" trigger is shared and still English — translate in a later policy slice.
    Commit
    - `refactor(listing): cleaner Things-to-know cards + i18n (en+af)`
  63. Language (L-C·6)2026-06-11

    listing room cards — branch `main`

    Built
    - Wired `RoomsInfoGrid` (made `async`, `getTranslations`) into the `listing` namespace (en + af): "from", per-night / per-person-night, "Sleeps {n}", baths (ICU plural), "{view} view". `bedSummary`/`roomFlagPills` (shared roomDisplay helpers) still English — a later slice.
    Notes
    - `tsc` + `lint` clean. Listing page still to do: `ListingBody` section headings, `RatesSection`, `SuitabilityChips`, `HostCard`/`TrustCard`, `ListingHero`, then the booking flow.
    Commit
    - `feat(i18n): listing room cards, en+af (L-C·6)`
  64. Language (L-C·7)2026-06-11

    listing body (headings, host strip, highlights) — branch `main`

    Built
    - Wired `ListingBody` into the `listing` namespace (en + af): section sub-nav labels, host strip ("{type} hosted by {host}", bedrooms/bathrooms ICU plurals, sleeps-up-to, identity verified), the four highlights (instant book / smooth check-in / cancellation / verified host), section headings (About this place, What this place offers, The rooms + subtitle + `t.rich` "Tap Reserve…"), Meet your host, and the safety note. Brand/time/count via ICU values.
    Notes
    - Listing page now largely translated. Remaining (later slices): `RatesSection` copy, `HostCard`/`TrustCard`, `ListingHero`, `PolicyDialog` modal content, `roomDisplay` bed/flag helpers (shared). Then booking flow → dashboard → admin → emails. - `tsc` + `lint` clean.
    Commit
    - `feat(i18n): listing body headings/host strip/highlights, en+af (L-C·7)`
  65. Language (L-C·8)2026-06-11

    listing rates section — branch `main`

    Built
    - Wired `RatesSection` (made `async`, `getTranslations`) into the `listing` namespace (en + af): eyebrow/title, intro, current-season callout (`t.rich` highlighted label), season legend (Standard/Baseline/Current), rate-card header + cleaning note (`t.rich` with inline `<Money>`), table headers, sleeps + per-person, whole-place/weekends, extras line, and the weekly-discount note.
    Notes
    - **Scope update:** super-admin (`app/[locale]/admin`) stays **English-only** — not translating it. Remaining to translate: rest of listing (`HostCard`/`TrustCard`/`ListingHero`, `PolicyDialog` modal, `roomDisplay` helpers), booking flow, host dashboard, guest portal, emails. - `tsc` + `lint` clean.
    Commit
    - `feat(i18n): listing rates section, en+af (L-C·8)`
  66. Language (L-C·9)2026-06-11

    listing hero, trust card, host card — branch `main`

    Built
    - Wired `ListingHero`, `TrustCard` (made async), and `HostCard` into the `listing` namespace (en + af): hero pills (superhost/guest-favourite/instant-book), rating/reviews (ICU plural), rooms + sleeps, verified host, breadcrumb aria, share/save, country label; trust-card verified badge + replies-in/years-hosting (ICU) + see-reviews aria; host-card stats, response rate, replies window, languages, identity-verified, rating-from-stays, view-profile. Inlined the reply/years helpers using `t` (removed the English-only helper fns).
    Notes
    - **Listing page now fully translated** except `PolicyDialog` modal *content* and the shared `roomDisplay` `bedSummary`/`roomFlagPills` (used across 6 files — a coordinated change). Next: booking flow, then host dashboard + guest portal. Admin stays English-only. - `tsc` + `lint` clean.
    Commit
    - `feat(i18n): listing hero/trust card/host card, en+af (L-C·9)`
  67. Language (L-D·1)2026-06-11

    booking failed page — branch `main`

    Built
    - New `booking` namespace (en + af); wired the booking **failed** page (`booking/[id]/failed`) — title/body, reference + listing labels, try-again / back-home, and a `generateMetadata` title.
    Notes
    - Booking flow's big piece, `BookingForm.tsx` (~2,600-line checkout) + the success/confirmation page, are large multi-slice jobs — best tackled with fresh context. `tsc` + `lint` clean.
    Commit
    - `feat(i18n): booking failed page, en+af (L-D·1)`
  68. Language (L-D·2)2026-06-11

    booking-form scaffolding + translation PAUSE — branch `main`

    Built
    - New `book` namespace (en + af); wired `BookingForm`'s navigational backbone: the 3 step labels (Rooms/Details/Payment), "Step n of 3", the three step titles + subtitles, progress nav, secure-checkout badge, Back, summary footer hints, and the Continue-to-details/payment CTAs. (`STEPS` → `STEP_KEYS` so the nav renders translated labels.)
    Notes
    - **⏸ Translation work paused here** (founder shifting focus to MVP hardening). Clean save point — everything builds (`tsc` + `lint` green). **Resume points:** the rest of `BookingForm` body (room/date/guest pickers, add-ons, coupons, payment options, summary line items — money stays settlement-currency), booking **success** page, then host **dashboard** + guest **portal**, then **emails**. Admin stays English-only. New work must still wire i18n per `RULES.md §10`.
    Commit
    - `feat(i18n): booking-form step scaffolding, en+af (L-D·2) — pause translation`
  69. Fix2026-06-11

    pets/children SSOT on listing + finish suitability i18n — branch `main`

    Fixed
    - 🔴 **"Who it suits" showed "Pets welcome" while the host had disabled pets.** Root cause: a dual source of truth — the chip read the legacy `listings.allow_pets`/`allow_children` columns, while the House-rules card reads the resolved **house-rules policy** (`pets_allowed`/`children_welcome`). They disagreed (verified live: `allow_pets=true` but policy `pets_allowed=false`). The listing page now derives the pets/children booleans for `SuitabilityChips` from the **policy** (the canonical, more complete source — it also has smoking/parties/quiet-hours), falling back to the listing columns only when the policy is silent. The listing columns still supply pricing + age bands + infants (no policy equivalent). "Who it suits" can no longer disagree with the House rules card.
    Changed
    - Finished the leftover translations in those areas (en + af): `SuitabilityChips` (made async), the "Things to know"/"Who it suits" headings, the reserve-panel `FxEstimateNote` estimate line, and the `PolicyDialog` "Read full policy" trigger. New `currency` + `policy` namespaces.
    Notes
    - **Editor-side SSOT still open:** the listing editor's `allow_pets`/`allow_children` toggles no longer drive display (policy does) — to fully consolidate, those toggles should be removed/redirected so a host sets pets/children only in the house-rules policy. Flagged for confirmation. - Still English (later slices): `PolicyDialog` modal content, `roomDisplay` bed/flag helpers (shared across 6 files), `ListingBody` section headings. - `tsc` + `lint` clean.
    Commit
    - `fix(listing): pets/children single source of truth (policy) + suitability i18n`
  70. Reviews2026-06-10

    MVP hardening, delayed request, photos — branch `main`

    Built
    - **Photos on reviews** — guests add up to 6 photos (JPEG/PNG/WebP, ≤8 MB) on the submit form via a token-gated signed upload straight to a new public `review-photos` bucket. New `review_photos` table. One reusable `ReviewPhotoGrid` (thumbnails + lightbox) renders them on the listing page, host dashboard, admin moderation, guest portal and the submit confirmation. - **Delayed review request** — checkout enqueues `review_request_queue` (`send_at = +5 min`); `/api/review-request-worker` drains due rows via one SSOT `sendReviewRequest()` that fires email + in-app + a tokenised thread card. `drain-review-requests` pg_cron pings it each minute; the old daily queuer is now a paid-aware 24h backstop. - **Host "Send review link" card** on completed bookings (Copy / WhatsApp / Email / Send-in-chat), mirroring the pay-link card; shown until a review exists.
    Changed
    - **Reviews publish immediately** (was a 48h hold) — admins can still hide. `on_review_published` now recalcs aggregates on INSERT + un-publish too. - **Fixed the long-broken review email link** — the resolver now signs `bookingId`+`reviewToken` (the template builds the link from those); added the missing in-app builder. Fixed the tokenless portal "Write review" CTA. - **Reviews are immutable** — `protect_review_content()` trigger blocks anyone but a super admin from changing rating/body/etc.; hosts may only respond. - Extracted `postGuestSystemCard`/`resolveGuestConversation` (pay-booking now reuses it); `buildReviewPath`/`buildReviewUrl` = SSOT for the tokenised link.
    Migrations
    - `20260610000001_reviews_mvp_hardening.sql` — `review_photos`, `send_at`, content-lock trigger, publish-on-insert recalc. - `20260610000002_review_request_cron.sql` — worker cron + paid-aware backstop. - `20260610000003_review_photos_bucket.sql` — public `review-photos` bucket. - `20260610000004_help_reviews.sql` — host + guest Help Centre articles.
    Notes
    - **Ops (one-time per env):** add Vault secret `review_request_worker_url` = `https://<app>/api/review-request-worker` (reuses `email_worker_secret` as the bearer; `EMAIL_WORKER_SECRET` already set in Vercel). Set `NEXT_PUBLIC_SITE_URL` (or `NEXT_PUBLIC_APP_URL`) so absolute review links resolve in prod. - Probe: `node --env-file=.env.local scripts/verify-reviews.mjs` (all green). - "Paid" = `payment_status IN (completed, partially_refunded, refunded)` — a guest who stayed can review even if later refunded (founder decision).
    Commit
    - `feat(reviews): …` — see `git log` 42ebed0…adacffc ---
  71. Reviews2026-06-10

    request flow + record tabs — branch `main`

    Built
    - **"Request reviews" modal** (`RequestReviewButton`) — lists only qualifying stays (completed + paid + **no review yet**, so already-reviewed guests can never be nagged), with bulk select + send (email + in-app + thread via the SSOT `sendReviewRequest`), per-row Copy / WhatsApp link, and a "requested Xd ago" status. On the Reviews manager header + the guest record Reviews tab (filtered to that guest). - **Booking Review tab** — a primary "Send review request" button on the `ReviewLinkCard` (same flow), above the share options. - `requestReviewsAction(bookingIds[])` — host-scoped; reuses `sendReviewRequest` and stamps `review_request_queue.sent_at` so the 5-min auto-send can't double-fire and "last requested" stays accurate. - `lib/reviews/eligible.ts → fetchRequestableReviews()` — SSOT for "who can be asked": completed + paid + registered guest + no review yet.
    Decisions
    - **Verified stays only** — no ungated/anonymous review path. A genuine off-platform guest is added as a manual booking (which qualifies them); the per-stay token link is what gets WhatsApp'd, so every review still maps to a real stay.
    Commit
    - `feat(reviews): …` — see `git log` ---
  72. Reviews2026-06-10

    account-less (manual) guests can review — branch `main`

    Changed
    - **A guest no longer needs an account to review.** Dropped `NOT NULL` on `reviews.guest_id` + `review_request_queue.guest_id`; the review still maps 1:1 to a real booking (`booking_id` UNIQUE), so it stays a *verified stay*. Name falls back to `bookings.guest_name` on the listing, dashboard, admin and host email when there's no account. - `sendReviewRequest` now branches: account guest → email + in-app + thread card; account-less guest → a direct transactional email to `guest_email` with the tokenised link. Checkout enqueues the request when there's an account **or** an email; the manager/guest-record modal and booking button include manual guests. - Guest record reviews are matched by the guest's bookings (not `guest_id`), and requestable stays match by account id **and/or** email, so manual guests appear.
    Migrations
    - `20260610000005_reviews_account_optional.sql` — nullable review/queue guest_id.
    Commit
    - `feat(reviews): …` — see `git log` ---
  73. Reviews2026-06-10

    activity log, featured review, star fix — branch `main`

    Built
    - **Activity tab** on the Reviews manager (record-style tabs: Reviews | Activity): per completed stay — request sent/scheduled, the review + stars, and who needs a public response (attention-first, count on the tab). `fetchReviewActivity`. - **Featured review** — host pins a review per listing (`listings.featured_review_id`); "Feature on listing"/"Unfeature" on every ReviewCard. The listing page uses the pin, else falls back to the **latest highest-rated** published review. - **Shared `RecordTabs`** — one underline tab bar; booking + guest records reuse it so tabs are identical across the app.
    Fixed
    - Listing "What guests are saying" hero showed a hardcoded 5 stars — now renders the real average (filled + empty), so the stars match the numeric rating.
    Migrations
    - `20260610000006_listing_featured_review.sql` — `listings.featured_review_id`.
    Commit
    - `feat(reviews): …` — see `git log` ---
  74. Marketing2026-06-10

    live platform commission-saved hero stat — branch `main`

    Built
    - `fetch_platform_commission_saved()` RPC (SECURITY DEFINER, scalar) — sums `total_amount * 0.15` over all direct confirmed/checked_in/completed bookings across every host, all-time. Granted to `anon, authenticated`. Mirrors the per-host calc in `fetch_secondary_metrics`; only the host filter is dropped. - `CommissionSavedStat` client component — count-up animation (easeOutExpo, IntersectionObserver, fires once on scroll-in) + soft card pulse + shimmering bar to draw attention. Honours `prefers-reduced-motion`.
    Changed
    - `/booking-management` Hero "Commission saved" card was a hardcoded `R 11 240 · vs. Airbnb 18% · this month`. Now shows the real platform-wide total. Copy updated to `vs. OTA 15% · across every host` to match the actual rate used. Decorative progress bar (fake 78%) replaced with the shimmer. - `booking-management/page.tsx` now `export const dynamic = "force-dynamic"` so the live Supabase read isn't frozen into the Data Cache at build time.
    Notes
    - Pre-launch the DB has no real bookings, so the value reads ~R0 until direct bookings flow in (chosen behaviour: show the true number always).
  75. Reports2026-06-10

    host savings vs OTAs (header "$" badge + Savings page) — branch `main`

    Built
    - `fetch_host_savings(p_host_id)` RPC (SECURITY DEFINER, JSONB) — per-host sibling of `fetch_platform_commission_saved`. Returns the raw direct-booking revenue **base** (plus booking count, first-booking date, currency and a monthly trend) so the web app can apply each OTA's rate. Same revenue set: confirmed/checked_in/completed direct bookings, not soft-deleted. Authorises by host ownership; granted to `authenticated` only. - `lib/savings/ota-competitors.ts` — single source of truth for the feature: `HEADLINE_OTA_RATE` (15%, matches the platform stat), the SA-focused `OTA_COMPETITORS` table (Booking.com 15, Airbnb 14, Expedia 16, LekkeSlaap 12, SafariNow 12, Vrbo 8) and a pure `computeSavings()` that turns revenue into the headline + per-OTA "what you'd have paid" rows. - `lib/savings/getHostSavings.ts` (server) + `dashboard/_actions/savings.ts` (`fetchMySavingsSummary` server action). - Header **"$" badge** (`SavingsBadge`) left of the booking-link icon → canonical `Modal` reading "Wielo has saved you R X so far" (lazy-fetched on click). - **Reports → Your savings vs OTAs** sub-page (`/dashboard/reports/savings`): dark hero with total saved + a comparison table (Wielo at 0% vs each OTA). - Help Centre article `savings-vs-otas` (category `payments`).
    Changed
    - `dashboard/layout.tsx` — added `<SavingsBadge />` to the header actions slot. - `dashboard/reports/page.tsx` — header now carries a "Your savings vs OTAs" link.
    Migrations
    - `20260610130000_host_savings_rpc.sql` - `20260610130001_help_savings_vs_otas.sql`
    Notes
    - OTA rates are reference figures (typical SA host-side commission) and live in one constants file — adjust there, not in SQL. The host's revenue base is always pulled live from the DB. - Pre-existing `<img>` lint warnings remain in two untouched reports components (`PerformanceTableClient`, `PopularRooms`) — out of scope here.
    Commit
    - `feat(reports): host savings vs OTAs — header $ badge + savings comparison page` ---
  76. Reviews/Dashboard2026-06-10

    fix invisible reviews + harden query error handling — branch `main`

    Fixed
    - Host reviews weren't showing on `/dashboard/reviews` despite the tab/stats counting them. Root cause: `listings.featured_review_id` (added earlier today) created a second FK between `reviews` and `listings`, making the un-pinned `listing:listings` embed ambiguous → PostgREST `PGRST201` (HTTP 300). The error was swallowed, so the feed rendered empty. Pinned the FK (`listings!reviews_listing_id_fkey`) in the three affected reviews→listings embeds: dashboard reviews feed, admin reviews list, global search. Verified live (old embed → 300, new → 200).
    Built
    - `lib/supabase/query.ts` → `throwOnError(query, context)`: awaits a Supabase query, logs server-side + throws on error instead of the silent `const { data } = await` pattern (68/78 dashboard pages used it). An empty list now only ever means "no rows", not "query failed". - `app/dashboard/error.tsx`: catch-all route boundary (renders inside the dashboard layout — sidebar survives). Shows a loud failure instead of a misleading empty/zero view. Does not swallow `redirect()`/`notFound()`. - Reviews feed got a dedicated inline error card (keeps partial-page failure + shows how many reviews aren't displaying).
    Changed
    - Wrapped the primary list/figure query in `throwOnError` on the high-value money/list pages: bookings, payments, invoices, credit-notes, guests, quotes, refunds. Added `throwOnErrorWithCount` (preserves pagination count) and rolled it across all admin list pages: audit, bookings, broadcasts, data-requests, hosts, listings, payments, reviews, subscriptions, users (caught by the existing `admin/error.tsx`). Verified all wrapped queries return 200 live, so no currently-working page changes behaviour. - Left reports as-is — it already logs each analytics RPC error and degrades gracefully (intentional; throwing would blank the whole dashboard).
  77. Fixes2026-06-10

    booking party manifest + guest record hero — branch `main`

    Fixed
    - **Additional guests invisible on a booking.** Checkout captures an optional party manifest (`bookings.additional_guests` jsonb `[{name, email?, phone?}]`), but the booking detail page never selected the column and the Guest tab only rendered the lead booker. Now selected, parsed and rendered in `GuestPanel` with a count and tap-to-contact email/phone per guest. - **Guest record hero looked misaligned.** The marketing-consent control was a heavy bordered box on its own line with the verification chips on another row. Restructured to match the booking record hero: verification, "All direct" and marketing consent share one aligned chip row, and the consent control is now a lightweight pill. No elements removed.
    Commits
    - `fix(bookings): show the party manifest (additional guests) in the Guest tab` — `3b90c98` - `fix(guests): tidy the guest record hero — one aligned status row` — `bcd062b` ---
  78. Listing extras2026-06-10

    auto-suggest nearby places (OpenStreetMap) — branch `main`

    Built
    - **"Suggest nearby places" on Listing extras.** Hosts no longer have to type every "Where you'll be" spot by hand. A new button uses the listing's saved coordinates to query the free, keyless **OpenStreetMap Overpass API** for real places around it, buckets them into Eat / Do / Travel, and shows them in a picker (canonical `FormModal`) with checkboxes and editable travel times. The host ticks what to show and they're batch-inserted into `listing_points_of_interest`. The manual "Add" form stays untouched. - Travel time is an estimate from straight-line (haversine) distance (~40 km/h), always editable. Suggestions skip places already added and are capped per category and sorted by distance.
    Changed
    - `listing-extras` Help Centre article updated to document the new button.
    Migrations
    - `20260610140001_help_listing_extras_suggest_nearby.sql` (help article only — no schema change)
    Notes
    - Overpass is called server-side in `suggestNearbyPlacesAction` (8s timeout, graceful failure → toast, manual flow remains). OSM coverage is thinner in small/rural towns; that's expected and the manual add covers the gap. - New files: `apps/web/app/dashboard/listing-extras/overpass.ts` (pure helpers). Batch insert via `createPoisBatchAction` reuses the same insert path/RLS as `createPoiAction`.
    Commit
    - pending ---
  79. Reviews/Guests2026-06-10

    party guests become guest records + relationships — branch `main`

    Built
    - **Party guests are now first-class guest records.** Every person named on a booking's party manifest (`bookings.additional_guests`) becomes a real, deduped `host_contacts` row the host can open, message, tag and note — not just a line on the booking. They appear automatically in the Guests directory + have a working guest record (the directory already UNIONs `host_contacts`). - **Guest relationships.** New `guest_relationships` table links each party member ↔ the lead booker (one row per direction, tagged with the source booking). Surfaced on a new **Relationships** tab on the guest record — "travelled with X · Booking …", linking both ways. - **Merged Guests tab on the booking record** (replaces the singular "Guest" tab): lead booker + every party member in one place, each linking to their own record, plus an **Add guest** action (name + email) that appends to the party and mints the record + relationship. - **Thank-you page** now lists the rest of the party under *Your details*.
    Changed
    - Checkout party manifest now requires **name AND email** per added guest (so each becomes contactable); the form blocks partial rows and the Zod schema enforces it.
    Migrations
    - `20260610150000_guest_relationships.sql` — table + RLS + `_materialize_booking_party()` + ownership-checked `materialize_booking_party()` RPC + `AFTER UPDATE OF status` confirm trigger. - `20260610150001_help_party_guests.sql` — Help Centre article.
    Notes
    - Materialisation is single-source: the trigger and the app (lazy fallback on the booking record + Add-guest) both call the same SQL function. (Gating later widened from confirmed-only to on-create — see the follow-up entry above.) - Relationships are fetched with two plain queries (the relation has two FKs to `host_contacts`, which would make a PostgREST embed ambiguous).
    Commit
    - `migration: guest_relationships + party materialiser` — `e0f2db6` - `feat(bookings): merged Guests tab + add-guest-to-booking` — `3e5bc05` - `feat(guests): relationships tab on the guest record` — `673ac13` - `feat(checkout): require email per party guest + list party on thank-you` — `9ee9a00` ---
  80. Guests2026-06-10

    party follow-ups: materialise on create, Bookings tab, start-thread — branch `main`

    Built
    - **Bookings tab on the guest record** — the guest's full reservation history (current + historical, newest first) with listing, dates, totals, balance-due, status, each linking to the booking. (Data was already loaded; gave it a home.) - **Start a message thread from the guest record** — the hero *Message* button now opens the Messages tab, where the host can compose the first message to a registered guest (`conversations.listing_id` is nullable, so no listing context is needed). Email-only contacts still have no in-app thread by design.
    Changed
    - **Party guests now materialise on booking *creation*, not only on confirmation** (and on any party/status change). The lead booker already shows in Guests the moment a booking exists, so party members do too now — including on pending/EFT bookings. Earlier "on confirmation" choice reversed after real usage showed a pending guest's party member stayed invisible.
    Migrations
    - `20260610150002_party_materialise_on_create.sql` — broaden the trigger to `AFTER INSERT OR UPDATE OF status, additional_guests` + one-time backfill of existing non-cancelled bookings (idempotent via the same SQL function).
    Notes
    - Verified against the live DB with `scripts/verify-party-guests.mjs`: the one existing party booking (pending EFT) now has its member as a contact + a two-row relationship.
    Commit
    - `fix(guests): materialise party on booking creation, not just confirmation` — `00ee780` - `feat(guests): bookings tab on the guest record + start thread from record` — `7501650` ---
  81. Inbox2026-06-10

    one chat design across host + guest (single source of truth) — branch `main`

    Built
    - **Shared inbox components** (`components/inbox/`) consumed by BOTH the host inbox and the guest portal, so the message-centre design lives in one place: `ConversationList`/`ConversationRow` (the left list + rows), `ChatMessageWall` (WhatsApp-style green/white bubbles, day pills, system + access-detail cards, inline quote cards), `ChatComposer` (rounded send box + optional quick-reply chips), `ChatThreadHeader`, and `InboxAvatar`.
    Changed
    - **Host inbox redesigned to match the guest inbox** — the Gmail-style folder rail, deal **pipeline** section, tabs strip, server-side pagination, assignee picker, follow-up/snooze and internal notes are **gone**. It's now the same two-pane chat: a conversation list (search + **All / Unread / Enquiries / Archived** filters + a per-listing menu) on the left, the thread on the right. - **Kept for hosts:** quick-reply templates, a slim **Booking/Details** slide-out (listing, stay details, totals, open-booking link, guest contact + WhatsApp), **archive/un-archive**, and **pin to top**. Quotes still render as cards in the thread. Deep links (`?c=`, `?f=enquiries`) and the full-bleed shell are intact. - **Guest inbox** refactored onto the shared components (visually unchanged — it was the canonical design). - **WhatsApp-exact read receipts (full 3-state).** Outgoing messages now show **sent** (single grey ✓) → **delivered** (double grey ✓✓) → **read** (double blue ✓✓ `#53BDEB`), with a **pending** clock for optimistic sends. Delivered is driven by new per-side `conversations.host_last_seen_at` / `guest_last_seen_at` stamps (recipient's inbox loaded at/after the message); read uses `read_by_host`/`read_by_guest`. Receipts render on the outgoing **thread cards** too — the guest's **quote-request** card, the host's **issued-quote** card, the **accepted** card and **access details** — not just plain bubbles. Both apps stamp "last seen" on inbox open + live message arrival; the guest thread also subscribes to `conversations` so ticks flip live.
    Migrations
    - `20260610160000_help_inbox_redesign.sql` — new `using-your-inbox` host article; re-seeds the stale `enquiry-pipeline-inbox` article (no more pipeline rail). - `20260610170000_inbox_last_seen.sql` — `host_last_seen_at` / `guest_last_seen_at` on `conversations` (delivered-receipt timestamps).
    Notes
    - Dropped `PipelineControl.tsx` + `ConversationNotes.tsx` and the now-unused actions (`setPipelineStageAction`, `assignConversationAction`, `setFollowUpAction`, `addConversationNoteAction`). The `pipeline_stage` column + the guest-reply auto-advance are left in the DB (harmless; quote flow + any analytics keep working) — only the host-facing pipeline UI was removed. - `tsc --noEmit` + `pnpm lint` are green. Full `pnpm build` not re-run to completion here: clearing `.next` dropped the cached Google Fonts and the local TLS proxy blocks refetching them — environmental, unrelated to these changes.
    Commit
    - `refactor(inbox): one shared chat design across host + guest` ---
  82. Perf2026-06-10

    instant navigation via loading.tsx skeletons — branch `main`

    Built
    - `ContentSkeleton` (`app/_components/`) — shared loading skeleton for the padded content shell (title row, KPI cards, list rows). - `InboxSkeleton` (`components/inbox/`) — full-bleed two-pane chat skeleton for the inbox. - `loading.tsx` Suspense boundaries at each shell root: `/dashboard`, `/portal`, `/admin`, plus inbox-specific `/dashboard/inbox` and `/portal/inbox`. - `OnboardingFreshness` (`app/dashboard/_components/`) — refetches `/dashboard` on mount/focus while in the onboarding branch, so the getting-started checklist can never be served stale from the Router Cache.
    Changed
    - Sidebar navigation now feels instant. Every logged-in page is `force-dynamic`, so each click was a blocking server roundtrip with no visual feedback until the server responded. The new `loading.tsx` boundaries render a skeleton the moment a link is clicked (and let Next prefetch the boundary on hover/viewport), so the app no longer feels delayed. - `next.config.mjs` `staleTimes.dynamic` raised `0 → 30`. The client Router Cache now reuses a just-visited dynamic page for 30s, so bouncing back and forth between sidebar items is instant instead of refetching every time. Mutation flows already call `router.refresh()` (clears the cache) so edits stay fresh; the onboarding checklist is guarded by `OnboardingFreshness`.
    Notes
    - No functionality changed — purely perceived/real performance. The shell (header + sidebar) stays mounted across navigation; only the content column swaps to a skeleton while data streams in.
  83. Perf2026-06-10

    parallelize sequential page queries — branch `main`

    Changed
    - `dashboard/page.tsx` (Overview): `getBrandName` now runs alongside `auth.getUser` (it doesn't need the user), and the `hosts` row + `fetchGettingStartedState` (both depend only on `user.id`) run in one wave instead of two sequential awaits. - `dashboard/bookings/page.tsx`: the listing count and the bookings list (both depend only on `myHostId`) now run in a single `Promise.all` instead of two sequential roundtrips. - `dashboard/guests/page.tsx`: the accepted-quotes query (previously awaited after the main `Promise.all`) is folded into that wave — all four reads now fire together.
    Notes
    - Pure latency reduction on the three highest-traffic host pages; no behaviour or query results changed. The data now arrives faster behind the loading skeletons added in the previous entry.
  84. Strategy2026-06-10

    foundational business principles + guest-identity ownership — branch `main`

    Built
    - `BUSINESS_PRINCIPLES.md` (repo root) — new canonical home for Wielo's foundational *business/strategy* principles, distinct from technical ADRs. Wired into `CLAUDE.md` → "Read These First" (as #4, after `AGENT_RULES.md`) so it loads every session. - **Principle #1 — Wielo owns all guest identity**: every guest entry point (direct signup, booking, added/party guest, quote request) mints a free, global, passwordless Wielo guest account keyed on email; email is mandatory (no name-only guests); returning guests claim by setting a password; history follows them across all hosts in one portal; shared-not-gatekept with hosts; minting ≠ marketing consent. - `DECISIONS.md` → **ADR-021** — the technical counterpart: one canonical `ensureWieloGuestIdentity` helper, mint passwordless at every entry, email required server-side, signup/login claim-detection. Cross-linked to Principle #1.
    Changed
    - `CLAUDE.md` read-order list renumbered to insert `BUSINESS_PRINCIPLES.md`.
    Notes
    - This session is **doc + roadmap only**. The identity-spine implementation (extract `ensureWieloGuestIdentity`, wire into manual booking / added-guest / checkout, signup claim-detection, make email required everywhere) is captured as a phased roadmap in plan `ok-here-now-lies-greedy-sunbeam.md` and ADR-021 — to be built in later chunked commits. - ~70% of the machinery already exists (the passwordless-lead pattern in `create-enquiry.ts:184-217`, `is_lead`, `gkey`, `/claim`). The work is making it universal, not net-new. - Founder-directed architecture; not a violation of the pre-MVP feature freeze.
  85. UI fix2026-06-10

    Policies cards: stop title/subtitle overflowing the card — branch `main`

    Changed
    - `apps/web/app/dashboard/policies/PolicyLibrary.tsx` — the policy card header's flex wrapper was missing `min-w-0`, so the `truncate` on the title (`h3`) and subtitle never engaged and long names/summaries spilled past the card edge. Added `min-w-0` to the header row and `shrink-0` to the status pill so the text side truncates cleanly while the pill keeps its size.
    Fixed
    - Removed a duplicate **Delete** button that rendered twice on every non-locked policy card (one inside the edit/duplicate/delete group plus a second standalone one).
    Notes
    - CSS/markup only — no schema, no behaviour change. `pnpm lint` clean for the file. `pnpm build` not run to completion locally: it fails before compilation on a Google Fonts fetch (`UNABLE_TO_VERIFY_LEAF_SIGNATURE`, network/TLS in this environment), unrelated to this change. ---
  86. Calendar2026-06-10

    manage availability + book from the calendar — branch `main`

    Built
    - **Selected-day Availability panel** in the calendar right rail. Tap any date; per listing it shows **Open / Booked / Blocked** with one-tap actions: **Block** a night, **Open up** (unblock) a manual block, and **Book** — which deep-links the New booking wizard with that listing + check-in already filled in. Booked nights and pending-quote holds are read-only here. - **Block dates** button in the calendar top bar opens a canonical `FormModal` to block (or re-open) a whole date range listing-wide. Booked + quote-held nights inside the range are left untouched. - **New-booking deep-link prefill** — `/dashboard/bookings/new` now honours `?listing=`, `?checkIn=`, `?checkOut=` (validated server-side: listing must be the host's, dates ISO + checkOut > checkIn). The wizard seeds the listing, dates and date-picker month from them.
    Changed
    - The calendar is no longer display-only — it now drives the existing `toggleBlockedDateAction` / `setManualBlocksAction` server actions (previously built but unwired). No change to the block/availability data model or RPCs.
    Migrations
    - `20260610170000_help_calendar_manage.sql` — new host help article `managing-your-calendar` (category `bookings`), idempotent upsert.
    Notes
    - `npx tsc --noEmit` clean; `pnpm lint` clean for the changed files (only pre-existing `<img>` warnings remain in unrelated reports components). `pnpm build` still fails before compilation on a Google Fonts fetch (`UNABLE_TO_VERIFY_LEAF_SIGNATURE`, network/TLS in this environment) — unrelated to these changes.
    Commit
    - `feat(bookings): new-booking wizard accepts listing + date prefill` — `73ae1f9` - `feat(calendar): block/unblock days + quick-book from the rail, range-block modal` — `f95a48f` ---
  87. Policy system refinement (Phase 1/6)2026-06-10

    resolver + snapshot SSOT — branch `main`

    Fixed
    - 🔴 **Refunds could silently pay 0% when a host relied on a default policy.** `get_listing_policy_summary` resolved a listing's policy as *listing-wide assignment → host default*, but `snapshot_booking_policies` only ever snapshotted an explicit listing-wide assignment (no default fallback, no room scope). A listing covered solely by the host's default showed a real cancellation policy to the guest, but the booking snapshot was empty → `calculate_policy_refund_amount` returned `no_policy_snapshot` → 0% refund. The displayed policy did not match the enforced policy.
    Built
    - Migration `20260610180000_policy_resolver_snapshot_ssot.sql`: - `resolve_listing_policy_id(listing, room, type)` — the single canonical resolver. Precedence: room-level → listing-wide → host active default → NULL. - `snapshot_booking_policies` rewritten to resolve via the canonical resolver (incl. default fallback) and derive the room from `booking_rooms` (single-room → that room; whole-listing/multi-room → listing-wide/default). No call-site change. - `get_listing_policy_summary` now delegates to the resolver and accepts an optional `p_room_id` (1-arg RPC still works). - Idempotent backfill: any booking missing a cancellation snapshot is re-snapshotted. - `apps/web/scripts/verify-policy-resolver.mjs` — live-DB QA gate: resolver callable, every published listing resolves a cancellation policy, every booking has a cancellation snapshot, refund calc returns a real rule.
    Notes
    - Verified against the linked remote: all bookings now carry a cancellation snapshot and refund calc returns a real rule (e.g. `Full refund 100%`) instead of `no_policy_snapshot`. - Known gap surfaced by the verifier: a published listing whose host set **no default** still resolves nothing — closed in Phase 2 (auto-default per type). - Types regenerated from linked remote. ---
  88. Policy system refinement (Phase 2/6)2026-06-10

    guaranteed coverage + summary fix — branch `main`

    Fixed
    - 🔴 **`get_listing_policy_summary` threw on any policy with `body_html`** (latent since `20260531000020`): `v_cont` was declared `jsonb` but `body_html` is raw HTML TEXT, so the assignment cast tried to parse HTML as JSON → `invalid input syntax for type json`. The whole RPC failed, so the public `ListingPolicyBlock` silently rendered nothing whenever a resolved house-rules/check-in/legal policy had prose. Fixed by typing `v_cont` as `text` (`20260610180003`).
    Built
    - `ensure_host_default_policies(host)` (`20260610180001`) — guarantees an active default per type (cancellation prefers the Moderate preset; check-in/house-rules take the oldest active). Idempotent; only fills types with no current default. Backfilled all hosts. - AFTER INSERT trigger on `hosts` (`20260610180002`) seeds the locked refund presets + a default at host creation, so every host (and every listing) resolves a cancellation policy from day one — presets are no longer only materialised lazily on the Policies page. Backfilled existing hosts + re-snapshotted bookings still missing a cancellation snapshot. - `createPolicyAction` / `togglePolicyStatusAction` now call `ensure_host_default_policies` after create/activate, so a host's first active policy of a type automatically becomes the default (immediately valid on unassigned listings). Policies page also ensures defaults on load.
    Notes
    - `verify-policy-resolver.mjs` now passes fully: 1/1 published listings resolve a cancellation policy, all bookings carry a snapshot, refund calc returns a real rule. - Types regenerated. ---
  89. Policy system refinement (Phase 3/6)2026-06-10

    terms & privacy go platform-wide — branch `main`

    Fixed
    - 🔴 **Every new booking's policy snapshot was failing** (`min(uuid) does not exist`). The Phase-1 snapshot rewrite derived a single-room booking's room via `min(room_id)`, but room_id is uuid and Postgres has no `min(uuid)` — the function threw at plan time, and since the booking-create call is best-effort the booking got NO cancellation snapshot → 0% refund. Pre-existing bookings were unaffected. Fixed by counting then selecting the lone room id (`20260610180006`), plus a heal-backfill. Caught by `verify-policy-resolver.mjs`.
    Changed
    - **Booking terms + privacy (POPIA) are now platform-wide, Wielo-authored** — not per-host policies (founder decision): - Removed both types from the host Policies UI (`POLICY_TYPES`, the "Terms & privacy" filter bucket, the create menu, the library query). Existing per-host legal policies retired (soft-deleted) and their listing assignments removed (`20260610180004`). - Resolver + snapshot + public summary scoped to the three host-controlled types (cancellation, check-in/out, house rules). `ensure_host_legal_presets` is now a no-op. - New `platform_settings` keys `legal_booking_terms` / `legal_privacy` hold a versioned `{html, version}` blob; `bookings.accepted_terms_version` / `accepted_privacy_version` record what each guest accepted. - **Admin → Platform settings → Legal**: super-admin editor (`LegalDocsForm` + `saveLegalDocAction`, audited) — publishing bumps the version. Public `/terms` and `/privacy` render the published HTML when set, else fall back to the built-in static copy. - `lib/legal.ts` read helper; `LegalPage` shell gained a `bodyHtml` mode.
    Built
    - Help article `booking-terms-and-privacy` (RULES §9) explaining hosts control refunds/check-in/house-rules while terms & privacy are platform-managed.
    Notes
    - `tsc --noEmit` clean app-wide (0 errors). Resolver verifier fully green (4/4 bookings snapshotted). - Types regenerated. ---
  90. Calendar2026-06-10

    select a range on the grid + inline quick-book — branch `main`

    Built
    - **Date-range selection on the month grid** (industry-standard host-calendar UX). Tap a check-in day, then a later check-out day; the nights highlight and a **Selected range** card appears in the rail with a listing picker, estimated total and a live booked/blocked conflict check. Tapping on/before the anchor restarts; an ✕ clears. - **Inline quick-book modal** — *Create booking* on the range card opens a compact `FormModal` over the calendar (dates locked; guest name/email/phone, party size, nightly rate + cleaning pre-filled, payment state) that posts straight to the existing `createManualBookingAction`. The host never leaves the calendar; on success the grid refreshes. **Open the full editor** deep-links the full wizard (carrying listing + both dates) for rooms/add-ons/discounts. - **Block from the range card** — one tap blocks every night in the selection listing-wide (`setManualBlocksAction`).
    Changed
    - Reused the booking SSOT — the quick-book modal calls the same server action as the full wizard, so pricing/availability/calendar-block writes are **not forked**. `cleaning_fee` is now carried onto the calendar's `CalListing` for rate prefill. - Earlier same-day review fixes to the single-day Availability panel: booked rows open the booking; real status label shown (not a flat "booked"); past dates read-only.
    Migrations
    - `20260610180007_help_calendar_inline_booking.sql` — re-upserts `managing-your-calendar` with the range-select + inline-book flow (applied to remote).
    Notes
    - `npx tsc --noEmit` and `eslint` clean for the three changed calendar files. (Repo-wide tsc shows one unrelated error — `ThingsToKnow` in `app/listing/[slug]/page.tsx` — from a concurrent agent's uncommitted WIP, not touched here.) Full `pnpm build` still blocked pre-compile on the Google-Fonts TLS fetch in this environment.
    Commit
    - `fix(calendar): availability panel polish from UI re-review` — `d22f8eb` - `feat(calendar): select a date range on the grid + inline quick-book` — `5673295` ---
  91. Policy system refinement (Phase 4/6)2026-06-10

    public listing page SSOT cutover — branch `main`

    Changed
    - The listing page's **Things to know** section is now driven entirely by the listing's effective policies (resolve: room → listing-wide → host default), not legacy columns: - **Cancellation** shows the real refund schedule inline (e.g. `5+ days → 100%`, `<24h → 0%`) with a non-refundable badge, plus "Read full policy". - **Check-in / out** times come from the `check_in_out` policy (falling back to the listing's own times only when no policy resolves). - **House rules** render as chips from the `house_rules` policy flags (pets/smoking/children/parties + quiet hours), with the host's prose and a "Read full" popup. - Booking terms + privacy are linked at the foot (platform-wide docs). - Removed the hardcoded `CANCELLATION_BLURB` and the `listing.cancellation_policy` enum display path. The reserve panel's refund note and the cancellation highlight now derive from the resolved policy via `cancellationNote()`.
    Built
    - `components/policy/ThingsToKnow.tsx` — the single inline renderer for the section. - `lib/policy/listing-summary.ts` — shared `getListingPolicySummary()` + `cancellationNote()` so the page fetches once and feeds both the refund note and `ThingsToKnow` (no double RPC).
    Notes
    - `tsc --noEmit` clean (0 errors), lint clean. Verified the summary RPC returns all three host types with real data for the demo listing. - `ListingPolicyBlock` is now used only by the checkout page — handled in Phase 5. ---
  92. Guests2026-06-10

    one record per email (dedup fix) — branch `main`

    Built
    - `apps/web/lib/guests/contacts.ts` — `upsertHostContact()`, the ONE canonical find-or-update-by-email writer for `host_contacts`. Finds by `lower(email)`, updates in place (keeps the email, back-fills `guest_id`), or inserts. `mode: "fill"` (default, never clobbers host-curated fields — used by lazy-mint & enquiry leads) vs `"overwrite"` (explicit Add/Edit guest form).
    Changed
    - **Guests directory no longer shows the same person twice.** `_host_guest_rows` now resolves any booking/contact whose email matches a registered account into that account's `u_` identity (the same email-merge the guest *record* RPC already used), so an email-only/OTA/manual row folds into the signed-in guest instead of forming a second card. Heals existing duplicates on read — no data backfill. - Routed the three contact-creation paths through the canonical writer: `addGuestContactAction` (manual add — now returns the canonical `u_`/`e_` gkey and back-fills `guest_id`), `ensureContact` (lazy mint — now back-fills `guest_id`), and `createEnquiry` (lead capture).
    Migrations
    - `20260610180008_guest_directory_email_merge.sql` — `CREATE OR REPLACE _host_guest_rows` with email→account canonicalization (read-only; reversible). - `20260610180009_help_guests_email_merge.sql` — refresh the Guests help article with the one-record-per-email guarantee.
    Notes
    - Root cause was a split-identity gkey: bookings/contacts keyed `u_<id>` when they had a `guest_id` else `e_<email>`, so the same email could occupy two keys. The fix makes email the canonical identity at both read (directory) and write (helper) layers, consistent with BUSINESS_PRINCIPLES #1. - Pre-existing, unrelated: `app/listing/[slug]/book/page.tsx` has a `cancellation` prop type error from the concurrent policies WIP — not touched here. - Migrations still need `supabase db push --linked` against the cloud project. ---
  93. Policy system refinement (Phase 5/6)2026-06-10

    checkout shows + records acceptance — branch `main`

    Fixed
    - **The guest's policy acknowledgement was never persisted.** `policy_acknowledged` was required by the schema but the booking insert never wrote it (and the form hardcoded `true`). Now the guest checkout writes `policy_acknowledged`, `policy_acknowledged_at`, and the accepted platform legal versions (`accepted_terms_version` / `accepted_privacy_version`) onto the booking.
    Changed
    - The checkout's **Cancellation policy** section now shows the listing's **real effective policy** (resolver: room → listing-wide → host default) — the actual refund schedule / non-refundable state and policy name — replacing the generic flexible/moderate/strict bullet copy. So what the guest accepts matches what's snapshotted and used for refunds. - Added an **explicit acceptance checkbox** ("I understand the cancellation policy and refund schedule, and accept the booking terms + privacy") that gates the confirm/pay button, with links to `/terms` and `/privacy`. The legal disclaimer + refund strip also use the real policy name/note.
    Notes
    - `book/page.tsx` resolves the cancellation via `getListingPolicySummary` + `cancellationNote` and passes it to `BookingForm`. Manual/quote bookings are host-made (no guest checkbox), so their acknowledgement stays unset — they still snapshot policies for refunds. - `tsc --noEmit` clean, lint clean. ---
  94. Policy system refinement (Phase 6/6)2026-06-10

    graceful retirement — branch `main`

    Built
    - **Impact-aware "Remove policy" flow.** The card's delete action now opens a modal (`RetirePolicyModal`) that first shows where the policy is used (listings + room overrides) and how many live bookings rely on it, then lets the host **reassign those listings to a replacement** (or fall back to their default) before the policy is **archived**. - `getPolicyRetirementInfoAction` (impact summary) + `retirePolicyAction` (reassign → archive → keep a default covered). Existing bookings are never touched — they keep their immutable snapshot, so refunds are unaffected; the modal states this explicitly. Locked presets remain non-removable. - After archiving, `ensure_host_default_policies` runs so a default always remains for the type (and a replacement is promoted if the retired one was the default). - Help article `removing-a-policy` (RULES §9).
    Notes
    - Archived policies were already excluded from the library query, the resolver (`status = 'active'`), and pickers — so no extra filtering was needed. - `tsc --noEmit` clean, lint clean. - The help-article migration `20260610180010` is committed but its `supabase db push` is currently **blocked by a concurrent session's pending migration (`20260610180008`/`009`, guest dedup) which errors** — so the article was applied directly to the linked DB (idempotent upsert, identical to the migration). The migration will apply once the concurrent one is fixed. ---
  95. Quotes & Inbox2026-06-09

    Per-room overrides, no-flash claim, event-sourced thread cards — branch `main`

    Built / Changed
    - **Per-room price override** — in rooms scope, the pulled-in per-room amounts are now editable line items; "Re-price from calendar" resets them. - **No-flash quote request + two-column claim** — the public request modal no longer flashes the form before navigating; it shows an in-place two-column thank-you (confirmation + create-account prompt left, request recap right). `/claim` redesigned to a two-column page mirroring signup (fields left, request preview as a dark hero right). - **Inbox badges = unread only** — every tab, folder-rail and pipeline-stage badge now counts unread threads only and hides at zero; opening a thread `router.refresh()`es so the read drops out of every badge (and the sidebar) at once. Quote requests still land under Enquiries. - **Event-sourced quote thread cards** — the conversation thread renders one immutable card per lifecycle event (request → sent/revised → accepted / declined → converted) instead of a single mutating card. Older sent/revised cards grey out as "Superseded" (the message body is the frozen snapshot); the request card greys to "Answered" once a quote is sent. - **Quote revisions with a reason** — editing an already-sent quote prompts for a reason, keeps the prior version (`quote_versions`), and posts a "revised" card showing the reason. Quotes stay non-posting — the ledger only engages on accept → booking → invoice → payments.
    Migrations
    - `20260608000011_quote_thread_events.sql` — `messages.quote_version_no` + `quote_versions.reason` (additive; pushed to remote, types regenerated).
    Notes
    - Superseded cards show the snapshot from the event message body (accurate at send time) rather than a re-rendered priced breakdown — a future enhancement. - `pnpm build` + `pnpm lint` pass clean across all commits.
    Commits
    - `4b6e0f4` per-room override · `e8212bf` no-flash claim · `2a49cf8` inbox badges · `a36d9b0` lifecycle events (phase 1) · `de942ad` thread cards + revision reason (phase 2/3) ---
  96. Help Centre2026-06-08

    Ledger article + rich help-content design system — branch `main`

    Built
    - **Help article `ledger-account-finance-view`** ("The Ledger: every transaction in one place") covering the account-wide Ledger (`/dashboard/ledger`): the five KPI totals (Outstanding, Collected, Refunded, Credits, Net), how to read a row (Type / For / Amount parentheses convention / running Balance / Document), filter pills + guest dropdown + search + date sort, the per-row `…` actions (record payment, mark received, refund, credit note, add charge, document share), voiding as a non-destructive audit correction, and closing/reopening accounting periods. Published, `host` audience, `payments` category. - **Rich help-content design system** (`apps/web/app/help/help-article.css`, scoped under `.help-article`) — reusable `hc-*` components any article can opt into: check-mark lists (no black dots), brand-coloured Type/For pills mirroring the live Ledger, KPI cards, a faithful mini-ledger table, an action grid, audit/periods callouts, and tasteful **CSS motion** (a staggered sheen wave across the KPI cards, a pulsing "Pending" dot, a shimmering progress-bar fill). All animation is wrapped in `prefers-reduced-motion: reduce`. - The Ledger article now uses these components end-to-end so it shows real, on-brand elements of exactly what the text describes.
    Changed
    - **`lib/help/sanitize.ts`** — allow `div`, `span`, and the `class` attribute (via `'*': ['class']`) so articles can carry layout + design-system classes. `style`/`script`/event handlers stay banned; verified the sanitiser still strips `onclick`/`style`/`<script>` while keeping `div`/`span`/`class`. - **Both help renderers** (`app/help/[slug]/page.tsx`, `app/dashboard/help/[slug]/page.tsx`) import the new stylesheet and add `help-article` to the body wrapper. Backward-compatible: existing articles (plain semantic HTML) render unchanged.
    Notes
    - Complements the existing `booking-payments-deposits-credit` article (per-booking Payments tab); this documents the whole-account finance view. - No animated GIFs — used CSS-animated real elements instead (cleaner, lighter, themable, reduced-motion-safe). - Timestamp collisions with parallel-agent migrations forced two renames (`…000001`→`…000004`, `…000005`→`…000007`).
    Migrations
    - `supabase/migrations/20260608000004_help_ledger.sql` — initial article (idempotent upsert; applied to linked remote) - `supabase/migrations/20260608000007_help_ledger_rich.sql` — rich-layout body for the same slug (idempotent upsert; applied to linked remote)
  97. Payments2026-06-08

    Host-Paystack spine + shareable pay-now link — branch `main`

    Built
    - **Pay-now link.** Every unpaid booking now has a secure, unguessable `pay_token` backing a public **`/pay/[token]`** page (no login). The guest sees the stay + amount due and pays by **card on the host's own Paystack** or by **EFT** (banking + reference) when the host hasn't connected card. On return from Paystack the page verifies with the host key and confirms via the ledger. - **Host share UI.** A **Payment link** panel on the booking's Payments tab (`PaymentLinkCard`) with Copy, **Send on WhatsApp** (pre-filled, uses the guest's number), and **Email the link** (pre-filled) — shown only while a balance is outstanding. - **Shared payment core** `lib/payments/pay-booking.ts` — `startBookingPayment` (host-Paystack init + EFT fallback + ledger-aware amounts) and `confirmHostCardPaymentByReference` (verify with host key → flip pending row → `recomputeBookingPaymentState` → confirm booking). Both the signed-in pay flow and the public pay link funnel through it. - **`lib/payments/host-paystack.ts`** — `getHostPaystack(hostId)`, the single source of truth for a host's connected, enabled Paystack secret.
    Changed
    - **Guest card payments now charge the HOST's own Paystack account** (not the platform key). `createBookingAction`, `initializePaymentForBookingAction` and the `/booking/[id]/success` verify were all using no key → platform account + stuck-pending host-account transactions. Fixed; checkout only offers Card when the host has Paystack connected. - `initializePaymentForBookingAction` slimmed to call the shared core.
    Migrations
    - `20260608000005_booking_pay_token.sql` — `bookings.pay_token` (`gen_url_token()`, unique). **Applied to the linked remote**; types regenerated. - `20260608000006_help_payment_links.sql` — host help article `send-a-payment-link` (payments category). Applied to the linked remote.
    Notes
    - New guardrails: **AGENT_RULES §4.7** (wire into the ledger — never fork the balance maths) and **§4.8** (booking card payments use the host's gateway via `getHostPaystack`; success-page verify is the authoritative confirmation). - Renamed my migration from `…0001` to `…0005` after discovering a concurrent finance agent had already applied `20260608000001-000004`. - Deferred (fast follow): "send payment link in the guest message thread" (needs conversation lookup/creation; Copy/WhatsApp/Email cover resend today).
    Commit
    - `fix(payments): route guest card payments to the host's own Paystack account` — `8a83d31` - `migration: add bookings.pay_token for the public pay-now link` — `d6cffe3` - `feat(payments): shareable pay-now link (/pay/[token]) + host share UI` — `3cd1134` ---
  98. Refactor2026-06-08

    Single-source-of-truth consolidation (payments/finance) — branch `main`

    Changed (no behaviour change unless noted)
    - **One `round2`** in `lib/format.ts`; `ledger`, `pay-booking`, `pricing/engine` and `finance/void` import it. **Bug fix:** `void.ts` previously rounded without the `Number.EPSILON` guard. - **One `INBOUND_KINDS` + `sumPaidFromRows`** exported from `ledger.ts`; the booking detail page dropped its hardcoded copy + inline reduce. - **Booking success page** now confirms via `confirmHostCardPaymentByReference` (verify-with-host-key → flip row → recompute ledger → confirm) instead of an inline copy that set `payment_status` by hand — closes the §4.7 gap. - **One `requireHost()`** in `lib/host/current.ts`; the ~14 per-file `getHost`/`getHostId`/`resolveHost`/`currentHost`/`getMyHostId` copies now import it (aliased to their old names, so call sites are unchanged). Files: ledger, refunds, quotes, banking, payments, payment-actions, addons, coupons, guests, policies, seasonal-pricing, staff, subscription, inbox. - **Banking `createPaymentLinkAction`** loads its secret via `getHostPaystack` instead of re-selecting + decrypting inline. - **One `nightsBetween`** (from the pricing engine) in the booking action and `pricing/quote.ts` — dropped two local copies.
    Notes
    - Per the new RULES §3 single-source-of-truth principle. Net code reduction. - **Deliberately NOT consolidated:** the per-page `fmtDate`/`fmtLong`/`fmtStamp` date formatters — they're intentionally different per surface (weekday vs not, etc.), so forcing them into one risked changing displayed formats (guardrail: don't merge divergent code). Left as justified-local. - Minor: a few unified error strings (e.g. banking/subscription host-lookup messages) are now the canonical "Not signed in." / "No host profile.".
    Commit
    - `refactor(payments): one round2, one INBOUND_KINDS, success page via the ledger` — `723adfd` - `refactor(payments): one requireHost + getHostPaystack + nightsBetween (finance)` — `0ec85a1` - `refactor(host): route remaining actions through the canonical requireHost` — pending ---
  99. Fix2026-06-08

    Paid bookings stuck `pending` (invoice trigger 42703) — branch `main`

    Built / Fixed
    - **Root cause:** `on_booking_confirmed_create_invoice()` (regressed by `20260606000012` + `20260607000006`) read host contact via `SELECT * INTO v_host FROM hosts` → `v_host.contact_email`/`contact_phone`, columns that don't exist on `hosts`. Every `pending → confirmed` flip raised `42703 record "v_host" has no field "contact_email"`, rolling back **only** the status UPDATE — so a Paystack-paid booking kept its `completed` payment + settled ledger but stayed `pending` with no invoice, no calendar block, no counter bump. (A guest test booking on Paystack test keys hit this.) - **Fix:** new migration restores the canonical snapshot — host email/phone from `user_profiles` (via `hosts.user_id`), banking from `eft_banking_details`, business from `host_business_details`, plus `booking_ref` — keeping the post-regression VAT split, `kind = 'booking'`, and `source = 'quote'` add-on filter. Snapshot shape now matches `invoice/[token]/pdf/route.ts`. - **App hardening:** `confirmHostCardPaymentByReference` now THROWS if the final `→ confirmed` UPDATE errors instead of swallowing it — a paid-but-unconfirmed booking must never masquerade as benign `pending` again. - Reconciled the stuck test booking `BK-LONECREEK-6EDD7-0007` → confirmed/paid, invoice `INV-MANA-10355-00007` minted, dates 9–11 Jun blocked.
    Migrations
    - `20260608000008_fix_invoice_host_snapshot_source.sql`
    Notes
    - No `database.types.ts` regen — function-only change, no table/column reshape. - Activity-timeline "Manual booking" vs "Wielo direct" is driven correctly by `bookings.origin` (`guest_request` → "Wielo direct"); 0007 is `guest_request`, so it now reads "Wielo direct". No code change needed there. ---
  100. Feature2026-06-08

    Shared Messages tab on the booking record — branch `main`

    Built
    - Extracted the guest record's message panel into one shared component `components/messages/GuestMessagesPanel.tsx` (carries `MessageItem` / `TemplateItem`). The Guest CRM record now imports it instead of its own copy. - Added a **Messages** tab to the booking detail page (`BookingDetail.tsx`) rendering that same component, bound to the SAME host↔guest conversation the guest record resolves (match by `guest_id` OR same-email profile → most recent `conversations` row). Messaging a guest from a booking and from their CRM record is now literally one thread — no per-booking fork.
    Changed
    - `GuestRecord.tsx` re-exports `MessageItem` / `TemplateItem` from the shared component so its `page.tsx` import is unchanged; dropped the local `MessagesPanel` + `applyTemplate` + the `Sparkles` / `sendMessageAction` imports they owned.
    Notes
    - Start-a-thread affordance added in the follow-up entry above. ---
  101. Refactor2026-06-08

    One payment path (guest checkout → startBookingPayment) — branch `main`

    Changed
    - Guest checkout (`createBookingAction` in `listing/[slug]/book/actions.ts`) no longer reimplements the Paystack init + EFT fallback + pending-payment-row creation inline. After it builds the booking it now calls the canonical **`startBookingPayment`** — the same path the guest pay page and the host pay-link use. ~95 lines of duplicated payment logic deleted; dropped the now-unused `initializeTransaction` / `getHostPaystack` / `hostHasValidEft` imports. - Net effect: ONE creation path + ONE payment path + `origin` as a data column (`guest_request` / `host_manual` / `quote_converted`). Origin is data, not a forked code path — the model we're standardising on.
    Notes
    - Behaviour parity verified by review: card → host Paystack checkout; no card rail or gateway down → host EFT fallback; no EFT either → booking unwound. - `startBookingPayment` sets `balance_due` to the post-payment balance (0 for a full charge) at init, same as the existing pay-page flow — the ledger recomputes it on confirm/cancel. Consistent now across every entry point. - Worth a live test-checkout to confirm the redirect chain end-to-end. ---
  102. Feature2026-06-08

    Start a conversation from the Messages tab — branch `main`

    Built
    - New `startGuestConversationAction` (inbox actions): find-or-creates the host↔guest conversation (reuses the most recent non-archived one — never forks a duplicate) and posts the host's first message. Guarded so a host can only open a thread with a guest they have a booking or CRM contact with. - `GuestMessagesPanel` now shows the composer when there's no thread yet but the guest has an account (`guestId`), so the host can open the conversation right from the booking record or the guest record. Email-only contacts still show the "no account" empty state (a conversation needs a `guest_id`). - Wired `guestId` (+ `bookingId` / `listingId` context) through both call sites.
    Notes
    - The first message sets `last_message_at` via the message AFTER INSERT trigger, so the new thread immediately resolves on BOTH the booking and guest-record Messages tabs — still one shared thread. ---
  103. Feature2026-06-08

    "Respond to quote request" framing + rich request card — branch `main`

    Built
    - The quote editor stays the single source of truth (`QuoteForm`). When a quote came from a guest's public request (`conversation_id` set), the edit page now reframes as **"Respond to {guest}'s request"** (eyebrow + design subtitle, back-to-inbox) and shows a redesigned **`QuoteRequestCard`** above the one form — the only thing that differs between "new quote" and "respond". - `QuoteRequestCard` matches the supplied design: dark "Their request" header with received-relative-time · via {listing}; guest avatar + stays/returning chips; contact + last-stayed line; their message bubble; a 4-up grid (wants to stay · dates · party · asked-about add-on); footer with calendar-open status and an "Open full chat" deep link (`/dashboard/inbox?c=…`). - Edit page loads the real context: prior-stays count + last checkout, guest avatar, requested room names + draft add-on labels, and a dates-open check against `blocked_dates` (excluding this quote's own soft-hold).
    Notes
    - No fork: `QuoteForm` (shared with New Quote) is untouched. The request card is the sole respond-mode addition, per the single-source-of-truth rule. ---
  104. Fix2026-06-08

    Guest checkout step 3 (payment) bugs — branch `main`

    Fixed
    - **Auto-redirect without clicking Pay.** The whole 3-step wizard was one `<form onSubmit={pay}>` with `type="submit"` Pay buttons, so any implicit submit (Enter in the coupon field, etc.) charged the guest and created a booking before they chose a method. Payment now fires ONLY from the Pay button's `onClick={pay}`; the form's `onSubmit` just advances the step (`if (step !== 2) goNext()`) and never charges. - **R0 / "weird stuff" on the payment step.** Added a `canPay` guard: when the total is R0 (e.g. the picked room just became unavailable for the dates and was auto-deselected) or the host has no payment rail, the Pay button is disabled and a clear amber notice explains it + offers "Back to dates & rooms". No more silently sitting on a R0 checkout that can't complete. - **"Booking made before payment" copy.** Reworded the EFT panel from "Your dates are held while you pay" to "Nothing is booked yet. When you tap reserve, we'll hold your dates…" so the pre-payment state is unambiguous.
    Notes
    - A provisional `pending` booking is still created at the Pay click (Paystack needs it) and auto-expires after 30 min via `expire-pending-bookings`. ---
  105. Fix/UX2026-06-08

    Checkout availability messaging + host attribution — branch `main`

    Verified (no code bug)
    - Per-room availability already works correctly: a confirmed booking blocks ONLY the booked room (`blocked_dates.room_id` set), so two rooms can be booked on the same dates by different guests. Only confirmed (paid) bookings + quote soft-holds write blocks — pending/unpaid bookings never close a room. (`BK-0007` proved it: only "rrom 1" is blocked; `room_is_available(Room 2)` = true.) The "Lone Creek" confusion was a `whole_listing`-mode listing where one occupied room correctly makes the *whole place* unavailable.
    Fixed (UX)
    - The whole-place card no longer says "Not available — try different ones" when rooms are still free; it now says "The whole place is taken — but you can still book an available room below" (amber, not red) when `anyRoomAvailable`. - Rooms step now shows a plain-language reason under the disabled Continue button (pick dates / select an available room / no rooms for these dates / whole place booked), on desktop + mobile — so the guest always knows why they can't proceed.
    Changed
    - Removed the redundant top "You're booking at …" listing hero card. The summary card now leads with **"You're booking with {host}"** + host avatar (fetched in page.tsx and threaded as `hostName`/`hostAvatarUrl`). ---
  106. Reports2026-06-08

    Ledger-backed Cash position on Analytics — branch `main`

    Built
    - **Cash position panel** on Analytics & Reports — a ledger-sourced money section sitting under the booked-value KPIs: Collected (period + lifetime), Outstanding (live, all-account), Refunded, Net cash, and a lifetime collection-rate bar. Reads from the SAME `fetchHostTransactions` ledger as the Ledger/Finances/Payments views, so the numbers reconcile to the cent. (`apps/web/app/dashboard/reports/_components/CashPosition.tsx`) - An inline explainer that reconciles **booked value (accrual)** vs **collected (cash)** so the headline "Total revenue" and the bank balance no longer look contradictory, plus an "Open ledger" jump to chase what's owed.
    Changed
    - **Reporting now wired to the ledger.** Previously Analytics computed revenue only from `bookings.total_amount` (accrual) with no cash view anywhere; it now surfaces collected/outstanding/refunded straight from the canonical ledger. - New canonical `txnFlows(entries)` in `lib/finance/transactions.ts` (the one definition of collected/refunded/credits/charged); `txnStats` refactored to build on it. Period totals = date-filtered slice; lifetime/outstanding = full. - `RefundsCancellations` cards now label their rates "of bookings" (frequency), distinct from the value-based "Refund rate" KPI — no more two unexplained refund-rate %s on one page.
    Migrations
    - `20260608000009_help_reports_cash_position.sql` — Help article `reports-cash-position` (idempotent; verified live).
    Notes
    - All 12 analytics RPCs were probed live against the real schema — none stale; `fetch_primary_kpis` revenue reconciles exactly with `SUM(total_amount)` for confirmed/checked_in/completed. ADR/RevPAR/occupancy/channel/regional left on accrual (correct for those). Refund flow auto-completes to `completed`, so the ledger's wider refund-status set already matches analytics in practice.
    Commit
    - `feat(reports): ledger-backed Cash position panel` — [hash below] ---
  107. Bookings2026-06-08

    Host-scope leak fix on bookings list + dashboard home — branch `main`

    Fixed
    - **Cross-host booking leak.** The bookings list (`/dashboard/bookings`) and the dashboard home KPIs/upcoming-arrivals queried `bookings` with no `host_id` filter, trusting RLS to scope them. But a host who is also a *guest* on another host's booking gets that row back via the guest-read RLS policy — so another host's booking surfaced on their board and linked to a detail page that (correctly) 404s. Now every host-dashboard booking read filters `.eq("host_id", myHostId)` explicitly, matching the booking detail page's guard. - `apps/web/app/dashboard/bookings/page.tsx` — list query now scoped via `getMyHostId`; empty board when the user has no host. - `apps/web/app/dashboard/page.tsx` — all five booking reads (month, prev month, last-90, upcoming, pending-count) now filter `host_id`.
    Changed
    - The bookings list no longer relies on RLS alone for scoping (it never was sufficient for users who are both host and guest).
    Notes
    - No DB/data change — the booking and guest records were consistent; this was purely a query-scoping bug. Calendar, Guests CRM, inbox, new-booking and quote-edit reads were already correctly `host_id`-scoped. - **Full audit follow-up.** Swept every `/dashboard` subtree (bookings, quotes, invoices, credit-notes, payments, refunds, ledger, inbox, reviews, guests, calendar, listings + sub-resources, reports, settings, staff, setup, help) for the same guest-read/public-read RLS leak class. Every other surface was already correctly scoped — explicit `host_id`, `bookings!inner` host join, or a transitively host-scoped parent id (conversation/booking/quote/invoice). Only extra change: `dashboard/notifications/page.tsx` now filters `.eq("user_id", user.id)` explicitly (was RLS-only; not a leak — the table has only a `user_id = auth.uid()` policy — but hardened to kill the pattern).
    Commit
    - `fix(bookings): scope host dashboard booking reads to own host_id` - `fix(notifications): explicit user_id filter on notifications list (audit hardening)` ---
  108. Quotes2026-06-08

    Redesigned quote-response builder (3-step layout) — branch `main`

    Changed
    - **Rebuilt `QuoteForm` to match the "Respond to Quote Request" design.** The builder is now a single-column, three-step flow — **Confirm the stay** (guest + matched listing/room + dates/party behind Change/Adjust), **Your price**, and **Terms & your reply** — with a sticky **Review & send** bar at the bottom. Replaces the old 7-section form + right-hand summary sidebar. One form still powers new quotes, edits, and request responses (the "Their request" card only shows for an actual request). - **New: Itemised vs Single-total price mode.** Single total stores the whole stay as one accommodation amount (no breakdown shown to the guest) — no schema change; maps onto existing `base_amount`. - **New: preview-before-send.** "Review & send" opens a guest-facing preview of the exact branded quote (hero, message, breakdown, accept-and-pay button); nothing is sent until confirmed. - Added a **Your payout** readout (0% fee) and an "Until check-in" hold option. - Narrowed the New/Edit quote pages to a single 880px column.
    Omitted (feature freeze)
    - The mockup's "Let the guest propose changes" (counter-offers) and "Suggest with AI" controls were intentionally left out — neither is wired in the backend.
    Migrations
    - `20260608000010_help_quote_response_redesign.sql` — refresh the `sending-quotes` Help article for the new flow (not yet pushed to remote).
    Notes
    - Soft-hold is shown as an informational note (sending soft-holds the dates via the existing quote-status trigger) rather than a fake toggle. - `pnpm build` + `pnpm lint` pass clean. ---
  109. Listings2026-06-07

    redesigned host listings index (KPI strip, tabs, grid/list, listing health) — branch `main`

    Changed
    - **Listings page redesign** (`app/dashboard/listings/page.tsx`). Replaced the dark portfolio hero with a lighter page header (live `places · published · draft · paused` counts) plus a 4-card KPI strip, a filter card (status tabs + search + sort + grid/list toggle), redesigned listing cards, a list view, and a **Listing health** recommendations panel. - All figures are **real stored values** — never placeholders. KPIs: total bookings (Σ `listings.total_bookings`), avg nightly rate (mean `base_price`), live/total, host avg rating + review count. Per-card stats: `total_bookings`, `total_reviews`, `avg_rating`. Status derives from `is_published` / `is_suspended` (suspended → "Paused · hidden from search"); spotlight/"Top performer" from `is_featured`. - The mock's occupancy %, booked-nights and next-booking (data we don't track) were swapped for these real metrics rather than fabricated. Draft cards show a real "finish to publish" checklist computed from stored columns (photos, pricing, description, location, rooms); Listing health is generated from the same real conditions (e.g. published with <8 photos, live without a price). - Tabs/sort/view are server-rendered via search params (`status`, `q`, `sort`, `view`) — no client island; `force-dynamic` retained for fresh DB reads.
    Notes
    - No schema change. Built alongside a parallel finance agent; touched only the listings page (the concurrent guests/finance edits are that agent's and were left untouched).
    Commit
    - `feat(listings): redesign host listings index` — see git log ---
  110. New booking2026-06-07

    v3 design refresh of the 5-step wizard — branch `main`

    Changed
    - **New Booking wizard restyle** (`bookings/new/ManualBookingForm.tsx`) to the "New Booking v3" design. Shared `.pick` selectable-card style (lighter green wash + thin ring) across listing / room / add-on / payment cards; sentence-case field labels (#3A5A4E) instead of all-caps; comfier inputs (11px radius); larger toggle (38×22) and stepper (34×36) to match the spec; progress step's active dot now uses brand ink. Added "Manage rooms" (`?tab=rooms`) and "New add-on" (`?tab=addons`) deep-links to the relevant section headers, and renamed step 1 to "Which property?".
    Notes
    - Surface-only: all booking logic, validation, real-data wiring and the payment step's behaviour are unchanged. Deliberately did **not** adopt the mock's non-functional/finance-coupled extras (pet/infant counters, Country field, deposit & damage-hold toggles) — those would be placeholder UI or collide with the in-flight payments/ledger work. Built in parallel with a finance agent; touched only the booking form.
    Commit
    - `feat(bookings): refresh New Booking wizard to v3 design` — see git log ---
  111. Rooms2026-06-07

    redesigned rooms manager with real 14-day occupancy — branch `main`

    Changed
    - **Rooms page redesign** (`dashboard/rooms/page.tsx`) to the Rooms design: breadcrumb header (Listings › Rooms), a 4-up stat band, listing filter chips + status + search, listing-grouped room tables, and a right rail (Needs attention · Top performers · Calendar legend). - **Real 14-day occupancy heatmap per room** computed from `blocked_dates` — each day is classified booked (`booking_id`/`source` booking|ical), held (`quote_id`/hold) or blocked (manual), else open; listing-wide blocks (`room_id null`) apply to every room. Drives the per-room strip, per-listing occupancy %, "booked nights", portfolio **Avg occupancy**, **Open tonight**, and **Top performers** ranking. - Stat band + rail are 100% real: live/total rooms, avg rate + min–max range, unpriced count, rooms-missing-photos and no-rate items in Needs attention. Replaced the dark portfolio hero/photo montage.
    Notes
    - No schema change. The mock's per-listing **revenue** and **"channels synced"** were intentionally omitted (not tracked) rather than faked — booked-nights/occupancy stand in. Reused the existing room data mapping + `roomRate`/`effectiveNightly` helpers. Rooms page is outside the parallel finance agent's files.
    Commit
    - `feat(rooms): redesign rooms manager with real 14-day occupancy` — see git log ---
  112. Finance control center2026-06-07

    receipts, refund/credit controls, guest balance, shareable docs — branch `main`

    Built
    - **Receipts.** Every completed payment is auto-numbered (`{HANDLE}-RCT2026-NNNN`), tokenised, and downloadable (PDF + tokenised record page). Booking Payments tab shows a Receipt link per paid entry. (migration `20260607000001`; `lib/pdf/ReceiptDocument`, `lib/payments/receipt-data`, `/receipt/[token]` + `/pdf`.) - **Shared `FinancialDocument` template** (`components/finance/FinancialDocument.tsx`) — the canonical brand "paper" for every finance doc (currently backing receipts; ready for invoice/quote/CN). - **Pastel format / auto-pull host details** (`lib/finance/doc-party.ts`): documents pull the host's full business into *From*, full guest into *To*, and the default EFT account into a footer *Payment details* block — live from settings (business/banking had been dropped from the invoice snapshot in migration 000601). - **Payment control center** — per-payment ⋯ menu (Refund this / Credit this) and whole-booking Issue credit note alongside refund. Manual credit notes now post to `guest_credit_ledger` (feed the guest's balance); refund-origin ones don't (no double-count). - **Refund documents** — `refund_requests` gets a per-host `REF` number (migration `20260607000002`), so invoice/quote/credit-note/receipt/refund are all numbered + booking-associated. - **Guest record** — net balance banner (green = you owe credit, red = guest owes, with breakdown) + expandable bookings showing a per-booking finance mini-table (payments/receipts/credit-notes/refunds) and a View booking button. - **Send to guest** — `SendDocumentButton` + `sendDocumentLinkAction` post a doc's public link into the guest inbox thread; wired on receipt, invoice (Share), credit-note. Quotes already had share-to-inbox.
    Migrations
    - `20260607000001_payment_receipts.sql`, `20260607000002_refund_numbers.sql`
    Notes
    - Remaining polish: migrate the public invoice / quote / credit-note record pages onto `FinancialDocument` for full visual unification (functional Send + download + numbering already done on all).
  113. One ledger everywhere2026-06-07

    guest Finances & booking Payments read the single transaction source — branch `main`

    Built
    - **Shared `LedgerList`** (`components/finance/LedgerList.tsx`) — the canonical transaction table (Transaction · Date · Guest · Type · Amount · running Balance · Document · actions) extracted from the account-wide Ledger so the *exact* same component renders everywhere.
    Changed
    - **Account Ledger** (`/dashboard/ledger`) now renders via `LedgerList` (no behaviour change). - **Guest record → Finances tab** now reads a `gkey`-filtered slice of `fetchHostTransactions` through `LedgerList`, dropping its own invoices/payments/refunds/credit-notes queries. Quotes (pre-booking) stay as a section below. Rows, money signs and running balances now match the Ledger exactly. - **Booking → Payments tab** now reads a `bookingId`-filtered slice of `fetchHostTransactions` (with `includePending`) through `LedgerList`, dropping its bespoke charge/payment table. Per-row settle / refund / credit and the record-payment / apply-credit / issue-credit-note action bar are preserved (injected via a `rowActions` slot).
    Database
    - None. `fetchHostTransactions` gained an `includePending` option (query-filter only) and the `Txn` type gained optional `pending`/`paymentId`/`kind`/`status` fields reading existing columns — no migration, no type regen.
    Notes
    - Pending payments carry zero balance/cash effect until they settle, so they never distort the running balance or collected total on any view. - The three money views are now genuinely filtered reads of one source — they can no longer drift.
    Commit
    - `refactor(finance): guest Finances tab reads the one ledger source` — `118848c` - `refactor(finance): booking Payments tab reads the one ledger source` — `51269c1`
  114. Booking redesign2026-06-07

    simplified guest journey (display-only listing + 3-step checkout) — branch `main`

    Built
    - **`ReservePanel`** (`app/listing/[slug]/ReservePanel.tsx`) — display-only sidebar (dark sticky card + mobile bottom bar) with two actions: **Reserve** (→ booking flow) and **Request a quote** (existing modal). No inline date/room/guest selection on the listing anymore. - **Self-contained 3-step checkout** — `BookingForm` now runs **Rooms → Details → Payment** in-page (guests pick dates, guests and rooms inside the flow), replacing the old 2-step Review → Payment that depended on a listing-page cart.
    Changed
    - **Listing page** collapsed to a single display-only body for every booking mode; rooms shown via `RoomsInfoGrid` (now with a from/night price). `RequestQuoteButton` gained `triggerClassName` / `triggerLabel` for panel + mobile styling. - **`book/page.tsx`** no longer gates on dates (Reserve arrives with no params) and loads add-ons unconditionally. - All existing server logic reused unchanged: `createBookingAction`, `priceStay()`, coupons, add-ons, Paystack + manual EFT, `/booking/[id]/success`.
    Removed
    - Now-unused interactive listing components: `BookingWidget`, `RoomsCartSidebar`, `MobileBookingBar`, `WholeListingToggle`, `RoomsGrid`, `RoomsCalendarSection`, `RoomsCartProvider` (its `BookingMode` type moved to `roomDisplay.ts`).
    Migrations
    - `20260607000003_help_guest_booking_flow.sql` — guest Help Centre article "How to book a stay" (Reserve vs Request a quote; the 3 steps). _Not yet pushed — apply with `supabase db push --linked`._
    Notes
    - Plan + progress tracked in `BOOKING_REDESIGN_PLAN.md`. - **Still open:** live per-room availability inside step 1 (server already enforces it at submit); finer visual alignment to `Booking Flow.html`.
    Commit
    - `feat(listing): display-only listing with Reserve + Request-a-quote CTAs` — 55b0ae2 - `feat(checkout): self-contained 3-step Rooms -> Details -> Payment flow` — 80a0d72 ---
  115. Guests (CRM)2026-06-06

    full feature, Phases 1–8 — branch `main`

    Built
    - **Guests directory** (`/dashboard/guests`) — KPI strip (incl. direct revenue / ~OTA fees saved), segment tabs (All/VIP/Returning/New/Via OTA/Lapsed) with counts, debounced search, listing/channel/rating filters, density toggle, sort, server-side pagination, row quick actions, selection + bulk Tag/Export, distinct empty states. Sidebar **Guests** entry with live count badge. - **Guest record** (`/dashboard/guests/[gkey]`) — sub-header with prev/next, identity header (segment + tags, contact row, Email-confirmed/All-direct chips, Message/Call + More menu), 5-tile lifetime stat band, tabs Overview / Bookings / Messages (bubbles + reply + template picker) / Payments / Notes (composer + pin/delete). - **Add guest** modal, CSV export (filtered or selected) + per-guest vCard, bulk tag, block/unblock (display-only), guest notes timeline. - **Message templates manager** at `/dashboard/inbox/templates` (replaced the "coming soon" stub) reusing the existing CRUD + `{{guest_name}}` tokens. - Two-way link: Booking Details ↔ guest record; "New booking for guest" prefills the wizard. - Help Centre article `guests-crm`.
    Changed
    - **Architecture:** built on the existing `host_contacts` (tags/notes/blocked) + `message_templates` instead of the plan's parallel tables — founder chose reuse over duplication. `gkey` is a URL/resolution scheme (`u_<id>` | `e_<base64url(email)>`), not a stored column; only `guest_notes` is new. - Removed the redundant **inbox Contacts tab + page** (Guests supersedes it); kept the `host_contacts` table as the CRM backing store. - `ManualBookingForm` seeds guest fields from `initialGuest` (prefill via query params).
    Migrations
    - `20260606000001_guest_crm_schema.sql` — extend host_contacts (+country, email_consent, blocked_reason/at), new `guest_notes`, user_profiles verify cols, seed starter templates. - `20260606000002_guest_crm_list_rpcs.sql` — `guest_gkey_for_email`, `_host_guest_rows`, `fetch_host_guests`(+`_summary`). - `20260606000003_guest_crm_record_rpc.sql` — `fetch_guest_record`. - `20260606000004_help_guests_crm.sql` — help article.
    Notes
    - Phase 9 (bulk mailer: guest_marketing + guest_broadcasts, send-guest-broadcast Edge Function, /unsubscribe, BroadcastModal) is the remaining phase. - Live-DB probes: `scripts/verify-guest-crm-p1.mjs`, `verify-guest-crm-p2.mjs` (run from apps/web).
    Commits
    - `feat(guests): phase 1…7` — 59856e8, 632aa71, e627e55, 06f0f76, d2d9092, 5a332e0, 6aebc9b, cc8c089 ---
  116. Guests (CRM)2026-06-06

    Phase 9 mailer + record Reviews/Finances/consent — branch `main`

    Built
    - **Record Reviews + Finances tabs.** Reviews the guest left; a consolidated Finances tab (invoices, quotes, refunds, credit notes) deep-linking to each. Payments stays its own tab. (Tabs: Overview · Bookings · Messages · Payments · Finances · Reviews · Notes.) - **POPIA marketing consent** on the record: locked status (Subscribed/Unsubscribed/No consent/No email), host can only ever **Record opt-out** — opt-in is the write-once Add-guest consent tick or the guest's own link. `email_consent` is write-once-to-true. - **Bulk mailer (Phase 9, build-only — not deployed/sent):** `guest_marketing` + `guest_broadcasts` tables; `broadcast_audience` / `count_broadcast_recipients` / `can_send_broadcast` RPCs; `lib/guests/broadcast.ts` (server-side Resend, recipients re-resolved + deduped, unsub tokens, branded template, reply-to = host, List-Unsubscribe header); `sendBroadcastAction` with server-side monthly cap; `BroadcastModal` ("Email guests") with live recipient preview + recent history; public `/unsubscribe/[token]` (GET page + RFC 8058 one-click POST).
    Notes
    - **Per-host isolation** is fully enforced (RLS keyed off `auth.uid()`, ownership-checked RPCs, `guest_marketing` per `(host_id, gkey)`) — one guest can sit in many hosts' lists with separate private data. - Mailer reuses existing env (`RESEND_API_KEY`, `EMAIL_FROM_ADDRESS`, `NEXT_PUBLIC_SITE_URL`) and sends from a Server Action (consistent with `lib/email`), so no edge-function deploy. **Before real use:** verified Resend sender domain + a live-send smoke test.
    Migrations
    - `20260606000005_guest_broadcast_schema.sql`, `20260606000006_guest_broadcast_rpcs.sql`, `20260606000007_help_guest_broadcasts.sql`.
    Commits
    - `feat(guests): reviews+finances / consent / phase 9` — e8e3282, 99c0f08, 8347639 (+ help) ---
  117. Payments2026-06-06

    single-booking ledger, manual EFT, store credit, add-on transactions + inbox fix — branch `main`

    Built
    - **Payment ledger.** One booking now carries many payment entries (deposit / balance / addon / payment / credit / refund). The booking's money state (`balance_due`, `payment_status` incl. new `partial`) is derived from completed inbound entries. New `Payments` tab UI shows Paid / Balance due / Store credit, a progress bar, the full ledger, and controls to **Record a payment**, **Mark received** (on seeded/pending entries) and **Apply store credit**. (`lib/payments/ledger.ts`, `bookings/[id]/payment-actions.ts`, `PaymentsManager.tsx`.) - **Deposit-first flow.** Quote accept/convert creates ONE full-amount booking and seeds a pending **Deposit** ledger entry (= quote deposit). Host records manual EFT to confirm + collect the balance. - **Per-host store credit.** Overpayment auto-posts to a new `guest_credit_ledger` keyed by the CRM gkey; host can apply it to an outstanding balance. - **Add-on transactions.** Host (Add-ons tab) and guest (trip page) can add extras to an existing booking. Each is its own transaction → joins the booking, raises the total, issues a **supplementary `addon` invoice**, and (host, if marked paid) records a linked `addon` payment; otherwise it lands on the balance. Guest add-on price is always resolved server-side from the host catalogue. (`lib/payments/invoicing.ts`, `AddonManager.tsx`, `portal/trips/[id]/addon-actions.ts` + `AddExtraCard.tsx`.) - **Help articles** for payments/deposits/credit (host) and adding extras (guest).
    Changed
    - **Double-booking fixed.** `convertQuoteAction` is now idempotent on `quote.converted_booking_id` — it adopts the existing booking (created by the guest-accept path) instead of minting a second one. - **Two-way inbox unread fixed.** `on_message_inserted` compared `messages.sender_id` (user id) to `conversations.host_id` (hosts row id) — never matched — so host replies never flagged the guest and inflated the host's own count. Now resolves the host user (staff + system cards count as host-side). - **Guest record Messages tab** resolves the thread by `guest_id` AND any lead profile sharing the guest's email, so enquiry/quote-request messages show in context. - `markBookingInvoicesPaidIfSettled` also flips a deposit-first booking's invoice to paid once fully settled.
    Migrations
    - `20260606000010_payments_ledger_and_credit.sql`, `20260606000011_fix_message_unread_trigger.sql`, `20260606000012_addon_transactions.sql`, `20260606000013_help_payments_and_addons.sql`
    Notes
    - Manual EFT path completed first (per request); Paystack/PayFast webhooks → ledger reuse `recordBookingPayment` with a `providerReference` (still to wire). - `invoices.booking_id` UNIQUE dropped (many invoices per booking) + `kind`/`payment_id` added; confirm trigger scopes the main invoice's add-ons to `source='quote'`.
    Commit
    - `feat(payments): single-booking ledger…` — 0f936e1 · `fix(inbox)…` — 062fea6 · `feat(bookings): host adds…` — 4c6ab29 · `feat(portal): guests add extras…` — 31c131b
  118. Inbox: quote→booking→payment deal card, read receipts, pipeline2026-06-05

    branch `main`

    Built
    - **Deal lifecycle card** in the thread (host + guest, same format): a quote card now carries the whole deal — New quote → (guest/host) **Accept** auto-creates a booking → **Pay to confirm** (guest CTA Pay now) → **Confirmed/Paid** (with balance-due) → Booking info; plus **Quote rejected**. - **Accept auto-converts**: accepting a quote (portal or token) creates a `pending` booking via `acceptAndConvertQuote`, keeping the quote soft-hold until payment; a trigger flips the quote to `converted` when the booking confirms. - **Pay an existing booking**: `/booking/[id]/pay` + `initializePaymentForBookingAction` — choose **deposit or full**, pay by **Card (Paystack)** or **EFT**, reusing the existing payment pipeline + confirmation. - **Read receipts** (whatsapp-style): grey double-check = delivered, blue = read, live via message UPDATE subscriptions. - **Pipeline**: stage auto-advances (guest reply → negotiating, accept → accepted, decline → declined) without affecting the deal; a **Projected value** total under the pipeline rail.
    Migrations
    - `20260605000004_quote_converted_on_payment.sql` — quote→converted trigger on booking confirm. - `20260605000005_help_accept_and_pay.sql` — guest help article.
    Notes
    - PayPal still out (lib only); real Paystack refunds still pending; unpaid quote-bookings keep the soft-hold (cleanup cron is a follow-up). `pnpm build` + `pnpm lint` green per chunk.
    Commit
    - `migration: flip quote to converted…` → `migration: help accept-and-pay` — `4bc69eb`…`(this)` ---
  119. Reviews: invite sending + host replies on display2026-06-05

    branch `main`

    Built
    - **Review invite now sends**: mapped the checkout transition to `review_request_guest`, so the existing per-transition dispatch fires the invite (email + push + in-app, deduped per booking) the moment a stay completes. Closes the loop — guests were never being nudged. Simplest path: no queue/cron/migration. - **Host replies now display**: `host_response` was captured (host dashboard) but never shown. Now rendered as a "Response from the host" block under each public listing review and on the guest's own reviews in `/portal/reviews`.
    Notes
    - Listing review display (`ReviewsSection`/`loadListingReviews`) and the guest portal reviews page already existed and are wired — they populate as reviews flow in. - **Deferred (migration jam):** a guest "how reviews work" help article needs a migration, and `supabase db push` is currently blocked by a parallel agent's migrations that live on the remote DB but aren't in git. My schema objects are all intact (verified); no new migration this pass. Will add once histories reconcile.
    Commit
    - `feat(reviews): send the review invite on checkout` → `feat(reviews): show host replies…` — `55605f0`…`78f9ceb` ---
  120. Inbox redesign: "Classic" Gmail-style layout2026-06-05

    branch `main`

    Built
    - **Gmail-style folder rail**: rounded-right active pills, count badges, a Pipeline section (with projected values), and dot-marked **Listings** filters — all wired to real folders/data (no invented Starred/Snoozed/Sent/Drafts). "Starred" maps to `pinned`. - **Tabs strip** (All · Enquiries · Booked · Action needed · Past), each mapped to a real query. Added `booked` (has a booking) and `past` (archived) folder filters. - **Single-line conversation rows**: star (pin) · importance marker · colour avatar · fixed-width sender · `listing · dates — snippet` · status chip · relative time, with hover actions (archive / mark-read / snooze-to-tomorrow). - **Real pager**: server-side `range()` pagination (25/page) with "from–to of total" derived in-memory from the counts query (no extra round-trip). Hidden while searching (search filters in-memory). - **Slide-over details drawer**: booking/guest details now open from a "Booking details" button over a dimmed scrim (was an always-docked pane). Leads with quote total + Confirm/Decline (wired to pipeline accepted/declined), then listing card, stay details, guest, and keeps the existing PipelineControl + assignee + private notes. - **Read view** restyled to match the mock: header bar, identity bar with status tag, centered thread on a tinted canvas with day dividers, guest/host bubbles, inline quote cards, and a rounded composer.
    Changed
    - The inbox is now a **view switch** (list ↔ thread) instead of a persistent list+thread split: a thread opens only when explicitly selected (`page.tsx` no longer auto-selects the first conversation). - `conversations` query now reads `pinned`/`booking_id`/`listing_id` for counts + filters; thread `listing` context now includes `city`/`province`/`max_guests`/`bedrooms` for the drawer.
    Migrations
    - None (queries only; no schema change → no type regen).
    Notes
    - Drawer is **real-data-only**: no "verified / N stays / rating" or cover image (no clean single source) — omitted rather than stubbed. - Dropped controls with no backend: Compose (no host-initiated threads) and bulk-select. "Past" = archived (completed/cancelled-not-archived not folded in to avoid a cross-table OR). - No new help article: visual-only restyle of already-documented inbox features (messaging, enquiry pipeline) — existing articles stay accurate. Will add one if the list↔thread switch needs explaining for real users.
    Commit
    - `feat(inbox): Classic Gmail-style redesign (rail, tabs, single-line rows, slide-over drawer)`
  121. Analytics: fix variable mismatches (dashboard was all zeros)2026-06-05

    branch `main`

    Changed
    - Rewrote all 12 analytics RPCs to reference columns/values that actually exist. Root cause: the functions filtered on `status 'checked_out'`/`'cancelled'`/`'refunded'`, `listings.status='published'`, `listings.title`, `listings.cover_image_url`, `payments.payment_type`, `reviews.deleted_at`, `conversations.deleted_at`, and `quotes.status in ('accepted','booked')` — none of which exist. Every metric returned 0/empty. - Revenue/active set now matches `dashboard/page.tsx`: `('confirmed','checked_in','completed')`. Cancellations use `cancelled_by_host`/`cancelled_by_guest`. Refunds sourced from `refund_requests` (status `completed`). Listing names from `listings.name`; status derived from `is_published`/`is_suspended`; cover image from `listing_photos`. Date window standardised to `check_in BETWEEN start AND end`. - Fixed latent bug: `EXTRACT(DAY FROM (date - date))` (date subtraction is already an integer) in property/popular-rooms night counts.
    Migrations
    - `20260605200526_analytics_fix_variable_mismatches.sql` — CREATE OR REPLACE all 12 analytics functions with correct variables + the JSON shapes the components consume. - `20260605201359_analytics_create_missing_tables.sql` — creates `listing_view_events` (+ corrected admin RLS on `scheduled_reports`/`report_runs`). These tables' original migrations (135911/135912) were stamped applied via `migration repair` but never ran, and used a non-existent `user_profiles.user_role` column.
    Notes
    - All 12 RPCs now return correct, real data and shapes (verified against the demo host: revenue R27,150, avg rating 4.8, named properties with cover images, etc.). - `views` / `listing_views` / `time_to_book` still read 0 because `listing_view_events` is empty — run `node --env-file=.env.local scripts/seed-analytics.mjs` (now that the table exists) to populate funnel/journey demo data. - Migration drift: the deployed functions had diverged from the on-disk migration files (parallel-reset wipes). This re-aligns both.
    Commit
    - `fix(analytics): align RPC variables with real schema; create missing tables`
  122. Brand: dynamic brand name in financial PDFs2026-06-04

    branch `main`

    Changed
    - **Quote / Invoice / Credit-note PDFs** now render the configurable platform brand name instead of a hardcoded "Wielo". `DocHeader` takes a `brandName` prop (used for the "Powered by …" tagline and the host-name fallback), and each document's footer reads "Generated by {brandName}". The three `*/pdf/route.ts` render routes plus `dashboard/invoices/actions.ts` resolve it via `getBrandName()` (from `lib/brand.ts`) and pass it into the props. - Dropped the hardcoded `wieloplatform.com` from the PDF footers — there's no configurable domain setting, so a placeholder domain beside a custom brand would have been wrong. Footer is now `Generated by {brandName} · Reference {number}`.
    Notes
    - Continues the dynamic-brand tail documented in the brand memory. **Still hardcoded:** push/in-app notification bodies (`lib/notifications/registry.ts` — next batch), some help/marketing strings, and metadata `description` strings. `pnpm build` + `pnpm lint` green.
    Commit
    - _pending_ ---
  123. Brand: dynamic brand name in push / in-app notifications2026-06-04

    branch `main`

    Changed
    - **Push + in-app notification copy** now uses the configurable platform brand name instead of a hardcoded "Wielo". `dispatchEvent` injects a `brand_name` (resolved via `getBrandName()`, caller value wins, safe fallback) into the refs passed to the `push`/`inApp` builders — the same payload-injection approach `drain.ts` uses for email subjects. Four builders updated: `refund_admin_override_host`, `subscription_expiring`, `subscription_failed`, `subscription_restricted`. - Raw refs are still what gets persisted to `notification_queue` / in-app payloads; the brand string is only baked into the rendered title/body at dispatch time (same as before).
    Notes
    - `brand_name?` added to `RefundRefs` + `SubscriptionRefs`. No schema change. **Remaining dynamic-brand tail:** some help/marketing strings ("How Wielo works", "Wielo Directory"), several metadata `description` strings, and `wieloplatform.com` references. `pnpm build` + `pnpm lint` green.
    Commit
    - _pending_ ---
  124. Fix: host inbox showed "Guest" instead of the visitor's name2026-06-04

    branch `main`

    Fixed
    - Quote-request (and all) inbox threads displayed **"Guest"** instead of the name the visitor entered, with email/phone showing as `—`. Root cause: the inbox embeds the guest via `user_profiles!conversations_guest_id_fkey`, but `user_profiles` had no host-read RLS policy (only `users_read_own` + `admin_read_all`), so the embedded guest resolved to `null` and the UI fell back to the literal `"Guest"`. The name was being captured correctly all along (`createEnquiry` stores `full_name`); the host simply couldn't read the row.
    Migrations
    - `20260604000003_host_read_guest_profiles.sql` — adds a `user_profiles` SELECT policy letting a host (or their staff) read the profile of any guest they share a **conversation or booking** with. Scoped to the host's own relationships via `get_my_host_id()` / `get_my_host_id_as_staff()` (both SECURITY DEFINER, return NULL for non-hosts → no broader directory exposure). Applied to remote.
    Notes
    - No app-code or type changes — the inbox query already selected `full_name/email/phone`. Fix is purely the missing RLS grant, so it corrects the name across every host thread, not just enquiries. - Migration was renamed from `…000001` to `…000003` to avoid a version collision with parallel-session migrations (`brand_name_setting`, `company_identity_settings`) already on remote.
    Commit
    - _pending_ ---
  125. Quotes: show the guest's original request as context on the quote form2026-06-04

    branch `main`

    Built
    - New `QuoteRequestCard` (read-only) rendered at the top of the **edit quote** form (`/dashboard/quotes/[id]/edit`) when a quote originated from a guest's public "Request a quote" enquiry. It snapshots what the visitor actually asked for so the host has that context while pricing: their **own message**, the requested **dates + nights**, the **party breakdown** (adults/children/infants/pets), and the **scope** (whole place / N rooms), plus when the request came in.
    Notes
    - A quote is treated as enquiry-originated when it carries a `conversation_id` (host-created quotes via `/new` never do), so the card only shows for real requests. The guest's message is the first non-system line in the linked conversation thread. No schema change. `pnpm build` + `pnpm lint` green.
    Commit
    - _pending_ ---
  126. Refactor: migrate payments + refunds money formatters to formatMoney2026-06-04

    branch `main`

    Changed
    - Replaced seven copy-pasted private money formatters (`fmtR` / `money`) with canonical `lib/format.ts#formatMoney`: `PaymentsBoard`, `payments/[id]/page`, `admin/payments/page`, `refunds/page`, `RefundActions`, `portal/trips/[id]/RequestRefundButton`, and `components/booking/CancelBookingDialog`.
    Notes
    - Behaviour-preserving for ZAR (the only live currency); non-ZAR now gains the ISO-code prefix (`USD 1 500`), the same tradeoff as the bookings batch. `pnpm build` + `pnpm lint` green. (CHANGELOG entry was deferred from commit `c9567c0` to avoid a concurrent-session collision on this file.)
    Commit
    - `refactor(format): migrate payments + refunds money formatters to formatMoney` — `c9567c0` ---
  127. Refactor: migrate invoice + credit-note money formatters to formatMoney2026-06-04

    branch `main`

    Changed
    - Replaced six more private money formatters (`fmt`) with canonical `lib/format.ts#formatMoney`: `credit-note/[token]/page`, `dashboard/credit-notes/page`, `dashboard/credit-notes/[id]/page`, `dashboard/invoices/[id]/CreateCreditNote`, `dashboard/invoices/[id]/page`, and `invoice/[token]/page`.
    Notes
    - Behaviour-preserving for ZAR (identical output, verified each copy by hand — they used differing `symbol`/spacing forms that all collapsed to `R 1 500`). Non-ZAR now renders `USD 1 500` (some copies previously emitted a double-spaced `USD 1 500`; `formatMoney` fixes that). `pnpm build` + `pnpm lint` green. See `SIMPLIFICATION_PLAN.md`.
    Commit
    - _pending_ ---
  128. Refactor: migrate quote money formatters to formatMoney2026-06-04

    branch `main`

    Changed
    - Replaced the private `fmt`/inline money formatters in the quotes area with canonical `lib/format.ts#formatMoney`: `QuoteForm`, `quotes/[id]/page`, the guest-facing public `q/[id]/[token]/page`, `QuoteShare` (WhatsApp/email share message), and one equivalent inline spot in `quotes/actions.ts` (the quote-sent inbox message body).
    Notes
    - Behaviour-preserving for ZAR (verified identical output). **Deliberately left one inline formatter in `quotes/actions.ts` untouched** — the `quote_sent` system-message body used bare `Math.round()` with no thousands grouping (`R 1500`), so it is *not* identical to `formatMoney` (`R 1 500`); migrating it would change a displayed amount, which the no-behaviour-change rule forbids. Logged in `SIMPLIFICATION_PLAN.md` as a latent inconsistency to fix on purpose later. `pnpm build` + `pnpm lint` green.
    Commit
    - _pending_ ---
  129. Refactor: migrate host/admin dashboard money formatters to formatMoney2026-06-04

    branch `main`

    Changed
    - Replaced five standard private `fmtR` copies with canonical `lib/format.ts#formatMoney`: `admin/bookings/page`, `dashboard/page` (home), `dashboard/listings/page`, `dashboard/coupons/CouponsManager`, `dashboard/addons/AddonsArchive`.
    Notes
    - All type-A copies — identical ZAR output. Remaining formatter work is the guest-facing listing/explore pages, which carry edge cases (null-guard, symbol-spacing, an extra inline formatter) documented in `SIMPLIFICATION_PLAN.md` for a careful follow-up pass. `pnpm build` + `pnpm lint` green.
    Commit
    - _pending_ ---
  130. Refactor: migrate guest-facing listing money formatters to formatMoney2026-06-04

    branch `main`

    Changed
    - Replaced 15 private money formatters with canonical `lib/format.ts#formatMoney` across the guest-facing surface: `c/[slug]`, `explore`, `[handle]`, `_components/home/home-data`, `booking/[id]/success/BookingConfirmation`, `RoomEditor`, the public `roomDisplay.ts` util, and the `listing/[slug]/*` components (`BookingWidget`, `MobileBookingBar`, `RatesSection`, `RoomsCartSidebar`, `SimilarListings`, `book/BookingForm`, `rooms/[roomId]/page`, `RoomBookingWidget`).
    Notes
    - Behaviour-preserving for ZAR (verified each — incl. `BookingConfirmation.fmtMoney`, whose `Number(n)||0` null-guard can't fire since all call sites pass typed numbers and `0` formats identically). This **completes the bulk formatter migration**. Three non-identical inline spots were deliberately left untouched and flagged in `SIMPLIFICATION_PLAN.md` (the `quote_sent` message body, `SuitabilityChips.money`, and a `BookingForm` add-on line — each renders a slightly different grouping/symbol and migrating would change a displayed amount). `pnpm build` + `pnpm lint` green.
    Commit
    - _pending_ ---
  131. Polish: normalise last 3 money formatters + de-brand public invoice/credit-note pages2026-06-04

    branch `main`

    Changed
    - **Money consistency:** normalised the three remaining non-canonical inline formatters to `formatMoney` (a deliberate display fix — they previously rendered `R 1500` / `R1,500` / `R 1,500` instead of the canonical `R 1 500`): the `quote_sent` inbox message body (`quotes/actions.ts`), `SuitabilityChips`, and a `book/BookingForm` add-on line. **The formatMoney migration is now fully complete** — every amount in the app renders through one helper. - **Brand:** the public hosted `/invoice/[token]` and `/credit-note/[token]` pages no longer hardcode the brand — the header initial + footer now render the configurable brand name via `getBrandName()`, and the placeholder `wieloplatform.com` domain was dropped (consistent with the PDF footers).
    Notes
    - No new features (feature freeze for MVP). `pnpm build` + `pnpm lint` green. Remaining brand tail: help/marketing copy strings + a few metadata `description` strings (catalogued in the brand memory note).
    Commit
    - _pending_ ---
  132. Brand: full dynamic brand-name sweep (marketing, app UI, metadata)2026-06-04

    branch `main`

    Changed
    - Completed the dynamic-brand tail: every remaining user-facing hardcoded "Wielo" across the app now renders the configurable brand name. Server components/route metadata use `getBrandName()` (static `metadata` → `generateMetadata`); client components use `useBrandName()`/`<BrandName />`. Covers the marketing site (`booking-management/*`, about/contact/cookies/terms/privacy, home `_components/*`), auth + signup flows, admin chrome, dashboard copy, listing/booking/quote/portal surfaces, and ~15 metadata descriptions. ~88 files.
    Notes
    - Purely additive — only brand strings swapped, no logic/layout changes. Deliberately left non-brand-display occurrences: code comments, the brand infra files, `samplePayloads.ts` (test data), the calendar-sync `User-Agent` header, the `EMAIL_FROM_ADDRESS` env fallback, `globals.css`, opaque order-reference prefixes, and example domains. **The dynamic-brand work is now complete** end-to-end (emails, PDFs, notifications, tab titles, app chrome, marketing, metadata). `pnpm build` (105 pages) + `pnpm lint` green.
    Commit
    - _pending_ ---
  133. Guest portal: complete & harden (quotes hub, in-portal browse, settings, book-again)2026-06-04

    branch `main`

    Built
    - **In-portal Quotes hub** (`/portal/quotes` list + `/portal/quotes/[id]` detail): guests now see every quote a host has sent them, with status pills, and accept/decline in-app instead of via emailed token links. Accept/decline run through session-gated, ownership-checked server actions (no `accept_token`). - **In-portal Browse** (`/portal/browse`): the `/explore` search/results rendered inside the portal shell so guests can find and book another stay without leaving. Extracted shared `searchListings` + `<BrowseResults>` and added a `basePath` prop to `SearchBar`/`TypeChips`. - **Book again**: a deduped "Book again" block on the portal overview plus rebook CTAs on trip cards and the trip-detail action bar, deep-linking to `/listing/[slug]/book?guests=N`. - **Consolidated tabbed Settings** (`/portal/settings`: Profile / Notifications / Data & privacy / Security), including a real **Security** tab to change sign-in email (with confirmation) and password via `auth.updateUser`.
    Changed
    - Request-a-quote now session-aware: a signed-in guest no longer re-enters name/email/phone and is routed straight to their portal inbox thread (anonymous lead + magic-link path unchanged). - Relocated the orphaned `/account/*` routes into the portal (notification preferences, data/privacy, and the notifications inbox → `/portal/notifications`); deleted the `/account` tree. Portal sidebar gained a Quotes link and "Browse stays" now points at `/portal/browse`. - Public token quote page `/q/[id]/[token]` renders the dynamic brand name (no hardcoded "WIELO").
    Migrations
    - `20260604000004_quotes_guest_read.sql` — guest SELECT RLS on quotes/quote_rooms/quote_addons (`guest_id = auth.uid()`). - `20260604000005_help_guest_quotes.sql`, `20260604000006_help_message_host.sql`, `20260604000007_help_account_security.sql` — guest Help Centre articles.
    Notes
    - Reuse-heavy: forked the public quote accept/decline + detail markup, the explore search, and existing notification/data page bodies rather than rebuilding. Verification tracked in `GUEST_PORTAL_QA.md`. `pnpm build` + `pnpm lint` green at each step; committed/pushed per chunk. - Follow-up: no notification bell in the portal sidebar yet (inbox reachable via Settings → Notifications); add when the notification system work resumes.
    Commit
    - `migration: quotes guest read` … `docs(qa): guest-portal verification pass` — `2213816`…`5d54e2d` ---
  134. Notifications: quote-requests tab + grouped bell tabs2026-06-04

    branch `main`

    Built
    - New **Quote requests** notification category + `quote_request_host` event; guest enquiries now notify the host through it, so quote requests get their own bell tab instead of being lumped into Messages.
    Changed
    - Dashboard/portal notifications list now groups categories under display-label tabs: `account_security` + `subscription` + `calendar_sync` → **System**; `admin_broadcasts` + `marketing_tips` → **Announcements**. Tabs render in a stable order: Bookings / Quote requests / Messages / Payments / Reviews / System / Announcements.
    Migrations
    - `20260604000008_notification_quote_requests.sql` — quote_requests category + quote_request_host event.
    Notes
    - Notification tabs still surface only for categories that have notifications (existing behaviour). Regular inbox messages keep using `new_message` (Messages tab). `pnpm build` + `pnpm lint` green.
    Commit
    - `feat(notifications): quote-requests category + grouped bell tabs` — `92046df` ---
  135. Guest access: per-room + gate code, 1h unlock2026-06-04

    branch `main`

    Built
    - Per-room guest access: new `listing_room_access` table + a **gate code** field on both listing and room access. Host edits room access in the room editor (new Guest access section) and listing access in the Guest access tab (gate code added). - Guest trip page resolves access by **booking scope**: whole-listing → listing access; room booking(s) → each booked room's access (two rooms = two blocks), each merging room values over listing values **per field** (fallback).
    Changed
    - Sensitive codes (gate/door) + Wi-Fi password now unlock **1 hour before check-in** (was 24h), using the listing check-in time of day.
    Migrations
    - `20260604000009_room_access_and_gate_code.sql` — gate_code + listing_room_access. - `20260604000010_help_room_access.sql` — refreshed host help article.
    Notes
    - Follow-up (not yet built): auto-post a designed access card into the guest's inbox 1h before check-in (needs a 15-min cron + conversation find-or-create + a card renderer). Real Paystack refunds also still pending (see prior note). `pnpm build` + `pnpm lint` green.
    Commit
    - `migration: per-room guest access...` → `migration: update guest-access help...` — `6b7e631`…`ad3249c` ---
  136. Consolidation → main: room/quote pricing + host payment gateways2026-06-03

    branch `feat/host-payment-gateways`

    Built (pricing workstream)
    - **Per-room & per-listing allow toggles** for children / infants / pets — OFF removes the category from checkout/quotes entirely; ON exposes its flat per-night rate (`listing_rooms` / `listings.allow_children|infants|pets`). - **Quote-level discount** — percentage or flat Rand off a quote (with reason), shown as its own line on the quote/PDF; carries onto the booking on convert. - **Quote deposit terms** — deposit (%) / full / reserve, with computed deposit + balance and a balance-due date tracked onto the booking (`bookings.deposit_amount`, `balance_due`, `balance_due_date`). Invoice/payment triggers untouched. - **Capacity guard** — adults + children must fit the room/listing capacity at booking time. - **Listing suitability** — children/infants/pets suitability chips + extras surfaced on the public listing (`SuitabilityChips`, `RatesSection`). - **Payment record page redesign** (`/dashboard/payments/[id]`) to the new layout.
    Migrations (pricing workstream)
    - `20260602000018_quote_discount.sql` - `20260602000019_allow_age_categories.sql` - `20260602000020_help_age_toggles.sql` (Help article update) - `20260602000021_quote_deposit.sql`
    Notes
    - `database.types.ts` is hand-edited for BOTH workstreams (Docker unavailable) and build-verified. **Combined deploy TODO:** `supabase db push --linked` applies migrations `…000016`→`…000021` in order, then `supabase gen types typescript --linked > packages/types/database.types.ts` (output should match the hand-edits). - Still required before storing real keys: set `PAYMENT_CIPHER_KEY` (see below).
  137. Rule: EFT is the payment backbone (publish gate + gateway fallback)2026-06-03

    branch `feat/host-payment-gateways`

    Built
    - **No listing goes live without a valid bank account.** "Valid" = a default, non-archived `eft_banking_details` row. New single source of truth `apps/web/lib/payments/eft.ts › hostHasValidEft(hostId)`. Enforced at two layers: the app gate in `togglePublishAction` (tightened from "any non-archived account" → "default account") and a new DB trigger `trg_listing_requires_bank` on `listings` (fires only on the `is_published` false→true transition, so seeds/tests that INSERT published rows are unaffected). - **Payments always fall back to EFT.** When Paystack/PayPal init fails during checkout, the booking no longer dies — it keeps the booking + reserved inventory, switches to `payment_method = 'eft'` / status `pending_eft`, and sends the guest to the awaiting-transfer view. (`book/actions.ts` catch.) - Codified both as **AGENT_RULES.md §4.5 / §4.6**; Help article updated.
    Migrations
    - `20260602000022_listing_requires_bank.sql` — publish-requires-bank trigger. - `20260602000023_help_payment_fallback.sql` — Help article update.
    Notes
    - Logic + trigger only — no new columns, so `database.types.ts` is unchanged. - `hostHasValidEft` matches the predicate the checkout already used in `book/page.tsx`; that inline check was left as-is (already correct).
  138. Trip Details (guest) + Quote Detail (host) redesign to match reference HTML2026-06-03

    branch `feat/trip-quote-detail-design`

    Built
    - **Guest Trip Details page** rebuilt to the founder's reference design, now living inside the guest-portal shell at `/portal/trips/[id]` (was a bare `SiteHeader` page at `/my-trips/[id]`). Real-data sections: status + days-to-go, bento photo gallery, host welcome note, getting-there/access (with gated door code + Wi-Fi), amenities, host local picks, house rules, receipt, refund history, a dark countdown rail, host card (real `avg_rating`/`response_rate`/superhost/languages + review count) and a manage-booking rail (reuses Cancel + Request-refund). - **Host Quote Detail page** rebuilt to the reference: big value header + key-facts strip, live **status stepper** (Created→Sent→Viewed→Accepted→Booked), open-tracking nudge, the stay card, price breakdown with payout, guest message, an **activity timeline** from real timestamps + view events, dark conversion card, guest card, and a host-only **internal notes** thread. Reuses existing `QuoteActions`/`QuoteShare`. - **Host-editing surfaces** for the new data: a **Guest access** tab on the listing editor (check-in method/instructions, door code, Wi-Fi + a local-picks repeater), a guest-facing **welcome note** card on the booking detail page, and an **add internal note** action on the quote. - **Quote open-tracking**: the public quote page now bumps `quotes.view_count` and logs a coarse (device-only, no PII) `quote_view_events` row per open. - Help Centre articles for guest access + local picks, welcome notes, and quote tracking/internal notes.
    Changed
    - `/my-trips` and `/my-trips/[id]` are now permanent redirects into `/portal/trips`. Notification deep links + booking-confirmation links repointed to `/portal/trips/[id]`. - Trips list `detailHref` → `/portal/trips/${id}`.
    Security
    - Sensitive access details (door code, Wi-Fi password) live in a new **host-only** `listing_access` table — never on `listings` (which has a public `SELECT *` policy). Guests receive them server-side (service role) on their own booking only, with the code/password gated to ≤24h before check-in.
    Migrations
    - `20260603000001_listing_access_and_local_picks.sql` — `listing_access` (host-only) + `listing_local_picks` (public-read) tables. - `20260603000002_booking_host_message.sql` — `bookings.host_message`. - `20260603000003_quote_notes.sql` — host-only quote internal-notes thread. - `20260603000004_quote_view_events.sql` — per-open quote tracking. - `20260603000005_help_trip_quote_detail.sql` — Help articles.
    Notes
    - Honest adaptations vs the mock: real host stats instead of "<1h / 187 reviews", an "Open in Maps" deep link instead of a live map embed, payout shown as the full total (Wielo 0% commission) rather than an invented fee, and graceful empty/withheld states (local-picks card hidden when empty; access secrets gated by date). Local picks are text-only for now (image upload can be added later — they render a category tile when no image). - Page chrome adapts to each existing shell: the quote page uses the dashboard's global Topbar + an in-page breadcrumb; the trip page uses an in-content header (the portal shell has no Topbar and is scroll-based).
    Commit
    - _pending_ ---
  139. Guest enquiry → host pipeline inbox (Phase A of the comms feature)2026-06-03

    branch `feat/trip-quote-detail-design`

    Built
    - **Guest "Request a quote"** on every listing's Host section (`RequestQuoteButton` + canonical `FormModal`). A visitor submits dates/party/(rooms)/message + contact — no login. - **`requestQuoteAction`** (`app/listing/[slug]/actions.ts`): finds-or-creates a **passwordless lead** by email (`is_lead`), upserts a **`host_contacts`** row, opens (or reuses) an enquiry **conversation** at stage `new_quote`, and creates an **auto-priced draft quote** linked to the thread, with a **draft-quote card** message + a host notification (reuses the `new_message` event). - **Pipeline inbox**: collapsible-rail **Pipeline** section (New quote → Quote sent → Negotiating → Accepted → Declined → Lost) with per-stage counts + filtering; a **`PipelineControl`** in the thread's right rail (stage chips + the linked quote card with "Complete & send quote"). - **Auto-advance**: `sendQuoteAction` → `quote_sent` (+ sent card), decline → `declined`, mark-accepted → `accepted`; manual override via `setPipelineStageAction`. - Extracted canonical pricing into **`lib/pricing/quote.ts` `computeStayPricing`** (now shared by `priceQuoteAction` and the enquiry flow — no duplication). - Help Centre article for the enquiry pipeline.
    Migrations
    - `20260603000006_enquiry_pipeline_inbox.sql` — `conversations` (pipeline_stage, assigned_to, follow_up_at, pinned, lost_reason); `quotes.conversation_id`; `messages.quote_id`; `user_profiles.is_lead`; new `host_contacts` + `conversation_notes` tables (RLS). - `20260603000007_help_enquiry_pipeline.sql` — Help article.
    Notes
    - Phase A of the approved multi-phase plan. **Next — Phase B:** guest inbox thread viewer + composer (`sendGuestMessageAction`), account claim (set password), Contacts tab + CSV, email acknowledgement. Phases C/D add CRM polish + automation. - `pnpm build` + `pnpm lint` green; live-DB query sweep 0/381.
    Commit
    - _pending_ ---
  140. Comms Phase B: two-way guest inbox + Contacts/CSV2026-06-03

    branch `feat/trip-quote-detail-design`

    Built
    - **Guest inbox is now two-way.** New `/portal/inbox/[id]` thread viewer + composer (`GuestThread`) with realtime + mark-read; the messages list now links into it. New guest-side `sendGuestMessageAction` + `markGuestConversationReadAction` (ownership via `guest_id = auth.uid()`, RLS-scoped). - **Contacts tab + CSV.** New `/dashboard/inbox/contacts` page lists the host's auto-collected `host_contacts` (name/email/phone/last stage/last seen); `exportContactsAction` streams a CSV download. A "Contacts" link was added to the inbox folder rail.
    Notes
    - Phase B of the approved plan. **Deferred to Phase B.2** (depends on the transactional-email / Supabase magic-link path, which is itself deferred infra): the lead **account-claim** flow (set a password → `is_lead=false`) and the **email acknowledgement** to the guest on enquiry. Phases C (CRM polish) and D (automation) still to come. - `pnpm build` + `pnpm lint` green; live-DB query sweep 0/385. No schema changes this phase.
    Commit
    - _pending_ ---
  141. Comms Phase B.2: enquiry email ack + lead account claim2026-06-03

    branch `feat/trip-quote-detail-design`

    Built
    - **Enquiry acknowledgement email** (`lib/email/send.ts` `sendTransactionalEmail` via Resend, best-effort, never blocks the enquiry). New leads get a **magic link** (`admin.generateLink` → `/auth/confirm?...&next=/claim`); existing accounts get an inbox link. - **Account claim**: `/claim` page + `ClaimForm` + `claimGuestAccountAction` — a lead who arrives via the magic link sets a password (`auth.updateUser`) and `user_profiles.is_lead` flips to `false`, turning the lead into a full account. Already-claimed users see a "go to trips" state.
    Notes
    - Completes the comms plan's Phase B. Email **delivery** depends on the Resend sending domain being verified (currently `resend.dev`); the flow is code-complete and works once that's set. `pnpm build` + `pnpm lint` green; sweep 0/386. No schema changes.
    Commit
    - _pending_ ---
  142. Comms Phase C (part 1): thread CRM2026-06-03

    branch `feat/trip-quote-detail-design`

    Built (reuse-heavy CRM polish on the host inbox thread)
    - **Quote card upgrades** in `PipelineControl`: **expiry countdown** (from `quotes.valid_until`) + **"Seen N×" receipt** (from `quote_view_events`) so the host knows the guest opened the sent quote. - **Internal notes** on a conversation — `ConversationNotes` panel + `addConversationNoteAction` (host-only, `conversation_notes` table). Loaded into the thread context. - **WhatsApp quick-contact** button in the thread's guest panel (`wa.me/<phone>`). - **Pin** threads — `togglePinAction` + a star toggle; pinned conversations sort to the top of the list.
    Notes
    - Phase C part 1 of the comms plan. **Remaining (part 2):** read receipts on host messages (A), needs-reply folder (#12), waiting timer (C), inbox nav badge (#14), assign-to-staff (E), canned replies (#4), pipeline value (#6), source tag (#9), convert-direct (#20). Then Phase D (automation). - `pnpm build` + `pnpm lint` green; sweep 0/388. No schema changes (reused Phase A columns/tables).
    Commit
    - _pending_ ---
  143. Comms Phase C2 + D (part 1): receipts, timers, follow-ups, rate-limit2026-06-03

    branch `feat/trip-quote-detail-design`

    Built
    - **Read receipts (A):** host messages show "Seen" once the guest has read them (`messages.read_by_guest`). - **Waiting timer (C):** unanswered threads show a "Waiting Nh" pill in the conversation list. - **Needs-reply folder (#12):** new inbox folder for threads with unread guest messages. - **Follow-up reminders (Phase D):** `setFollowUpAction` + snooze controls (Tomorrow / In 3 days / Clear) in `PipelineControl`, plus a **Follow up** folder surfacing reminders that are due (`conversations.follow_up_at`). - **Enquiry rate-limit (#8):** `requestQuoteAction` silently caps a single email to 5 enquiries per host per hour.
    Notes
    - Remaining tail: inbox nav badge (#14), pipeline value per folder (#6), canned replies (#4), assign-to-staff (E), quiet-hours auto-reply (G), auto-archive cron (D), source tag (#9), convert-direct (#20). - `pnpm build` + `pnpm lint` green; sweep 0/389. No schema changes (reused Phase A columns).
    Commit
    - _pending_ ---
  144. Comms Phase C/D tail: nav badge, pipeline value, canned replies, auto-archive2026-06-03

    branch `feat/trip-quote-detail-design`

    Built
    - **Inbox nav badge (#14):** the dashboard sidebar Inbox item shows a count of conversations with unread guest messages (computed in `dashboard/layout.tsx`, passed to `Sidebar`). - **Pipeline value (#6):** each pipeline folder shows the summed value of its threads (latest quote total per conversation, by stage). - **Canned replies (#4):** the host composer's quick-reply row is now live — chips inserted from the host's `message_templates`, with a "Manage" link. - **Auto-archive (#19):** new pg_cron job archives Lost/Declined enquiry threads idle for 30 days (`…000008_auto_archive_cron.sql`). - **Source tag (#9):** already covered — the originating listing shows on the conversation row + booking pane.
    Notes
    - Completes the comms plan **except two deferred items**: **assign-to-staff (E)** — needs the host↔staff membership model wired into the inbox; and **quiet-hours auto-reply (G)** — needs a host-set auto-reply message + the quiet-hours prefs lookup. **Convert-direct (#20)** intentionally skipped (redundant — host converts from the quote detail page). These can be a small follow-up. - `pnpm build` + `pnpm lint` green; sweep 0/391. Migration `…000008` pushed (cron only).
    Commit
    - _pending_ ---
  145. Comms: assign-to-staff + quiet-hours auto-reply + fix enquiry honeypot2026-06-03

    branch `feat/trip-quote-detail-design`

    Built (the two previously-deferred items)
    - **Assign-to-staff (E):** assign an inbox thread to the host or a `staff_members` teammate (`assignConversationAction` + an assignee dropdown in the thread pane; `conversations.assigned_to`). Picker only shows when the host has staff. - **Quiet-hours auto-reply (G):** new `hosts.enquiry_auto_reply` (set on Settings → Notifications via `AwayAutoReplyCard`/`setEnquiryAutoReplyAction`). When an enquiry arrives during the host's notification quiet hours (`user_notification_settings`), Wielo posts the message into the thread automatically.
    Fixed
    - **Request-a-quote modal silently failed.** The honeypot field was declared as `z.string().max(0)`, so browser autofill of a field named "company" failed Zod validation and blocked the submit before the intended silent-drop ran. Honeypot is now permissive in the schema (renamed `hp`, neutral input name, autocomplete off) — real guests submit reliably; bots that fill it are still dropped. Verified the full enquiry write path end-to-end against the live DB.
    Migrations
    - `20260603000009_host_enquiry_auto_reply.sql` — `hosts.enquiry_auto_reply`. (Cron `…000008` already applied.)
    Notes
    - This completes the entire comms plan (A→D + all enhancements 1–20 + A–G) except convert-direct (#20), intentionally skipped as redundant. `pnpm build` + `pnpm lint` green; sweep 0/395.
    Commit
    - _pending_ ---
  146. Refund payout methods + Credit Notes + Finances sub-menu2026-06-02

    branch `feat/unified-pricing-engine`

    Built
    - **Refund payout-method selection.** When processing a refund, the host now picks how it's paid out — **Paystack / PayPal / EFT / Manual** — on both the Refunds queue (approve flow) and the booking-page **Issue refund** panel. The selector defaults to the booking's original payment method. EFT/Manual are flagged `is_manual = true` (host sends the money); Paystack/PayPal are provider transactions. The chosen rail is persisted on `refund_requests.refund_method` and shown on actioned refund cards. - **Credit Notes (new Finances feature).** A credit note records money credited back to a guest against an invoice. `credit_notes` table mirrors `invoices` (per-host `{handle}-CNYYYY-NNNN` numbering, frozen host/guest snapshots, jsonb line items, hosted token, PDF bucket). Created two ways: - **Auto** — a DB trigger issues one the moment a refund hits `completed`, linked to the booking's invoice (idempotent, one per refund). - **Manual** — "Create credit note" on the invoice detail page. List at `/dashboard/credit-notes`, detail at `/dashboard/credit-notes/[id]` (with cancel action). Invoice detail page now lists its credit notes. - **Collapsible "Finances" sub-menu** in the dashboard sidebar containing **Quotes → Invoices → Credit Notes** (in that order). Auto-expands when a child route is active. Added Credit Notes to the ⌘K quick-nav too.
    Changed
    - `approveRefundAction` + `hostInitiatedRefundAction` now take a `method` and derive `is_manual` / completion note from it (replaces the hard-coded "provider integration pending" manual flag). - Sidebar `TOOLS` no longer holds Quotes/Invoices (moved to the Finances group).
    Migrations
    - `20260602000000_refund_method.sql` — `refund_requests.refund_method` column. - `20260602000002_help_refund_methods_credit_notes.sql` — Help Centre article. - `20260602000003_credit_notes.sql` — `credit_notes` table + RLS + `next_credit_note_number()` + `host_counters.last_credit_note_number` + auto-create trigger on refund completion + `credit-note-pdfs` storage bucket. (Renumbered from `…001` to avoid colliding with the parallel `20260602000001_host_logo.sql` migration, which is committed here too.)
    Notes
    - Types in `packages/types/database.types.ts` were **hand-edited** (Docker bypassed): added `credit_notes`, `host_counters.last_credit_note_number`, `refund_requests.refund_method`, and the `next_credit_note_number` RPC. Regenerate properly against the linked remote after `supabase db push`. - **Not yet pushed to remote** — run `supabase db push --linked` then `supabase gen types typescript --linked > packages/types/database.types.ts`. - Credit-note **PDF + public hosted page deferred** — founder is supplying the invoice/quote/credit-note detail + PDF designs; current styling is minimal on purpose so the designs can be dropped in over working logic. - `pnpm build` + `pnpm lint` both green.
  147. Financial documents: branded PDFs, invoices, credit notes, quote sending2026-06-02

    branch `feat/financial-docs`

    Built
    - **Host logo + branded PDFs (Phase 1):** logo uploader on Settings → Business & banking (client-side canvas resize to ≤512px), stored in a public `host-logos` bucket with host-folder RLS. New shared `DocHeader` renders the logo (with a lettered fallback) on every invoice, quote and credit-note PDF; PDFs embed it as a data URI so there's no render-time fetch. - **Credit notes domain (Phase 2):** branded `CreditNoteDocument` PDF + public token-gated `/credit-note/[token]` page + PDF route, plus "Download PDF" / "Share link" on the host detail page. (Table, triggers, manual create and the list/detail pages were landed alongside a parallel agent — reconciled.) - **Invoice paid-sync + cross-links (Phase 3):** a trigger flips an invoice to `paid` whenever its booking's payment completes (covers EFT-confirmed-then-paid and any later capture). Cross-links wired across booking ↔ invoice ↔ payment ↔ credit-note detail pages. - **Quote send flows (Phase 4):** the quote "Share with guest" panel now sends via **WhatsApp** (wa.me deep link, SA numbers normalised), **Email** (mailto from the host's own client), **Wielo inbox** (`shareQuoteToInboxAction` posts into an existing host↔guest thread), and **Copy link**. - **Tests + help (Phase 5):** `pnpm test:flows` extended with Journey G (refund completion auto-mints a linked credit note) and Journey H (invoice paid-sync) — 33/33 checks green. Help Centre articles for Quotes, Invoices and Branding your documents.
    Changed
    - Sidebar: Payments and Refunds moved under the Finances group.
    Migrations
    - `20260602000004_invoice_paid_sync.sql` — `on_payment_completed_mark_invoice_paid` trigger. - `20260602000005_help_quotes_invoices_branding.sql` — three Help Centre articles. - (Phase 1/2 logo + credit-note migrations applied earlier in the reconciliation.)
    Notes
    - **Deferred:** the quote *builder* enrichment — engine-priced room multi-select (via `priceStay`), catalog add-on picker, and cancellation-policy snapshot into `quotes.policy_snapshot`. The backend/schema already support `scope: "rooms"` + catalog add-ons; only the builder UI + a `policy_snapshot` column + client-side engine wiring remain. Quotes are fully functional today with manual amounts and custom line items. Pick this up as a focused next session. - Provider (Paystack/PayPal) refund automation still optimistic/manual pre-MVP.
    Commit
    - `feat(finances): invoice paid-sync + cross-links` — c8eda50 - `feat(quotes): send via WhatsApp/email/inbox/copy` — 6eeb531 - `test+docs(finances): credit-note + paid-sync journeys, help articles` — (this commit) ---
  148. Quote builder enrichment + financial/booking hardening2026-06-02

    branch `feat/financial-docs`

    Built
    - **Enriched quote builder:** the New Quote form now pulls in the host's real rooms and catalog add-ons. Scope toggle (whole listing vs specific rooms), per-room guest counts, a **"Price from calendar"** button that prices through the canonical `priceStay` engine (seasonal/weekend aware, server-side via new `priceQuoteAction`) with host override, catalog add-on picker + custom lines. - **Cancellation policy on quotes:** `createQuoteAction` freezes the listing's policy into `quotes.policy_snapshot`; convert carries it onto the booking. - **Payment = finance overview hub:** the payment detail page now lists every related document — the quote it came from, invoices, credit notes and refunds — in one "Financial overview" panel. Payments moved to the top of the Finances nav.
    Changed
    - **Convert is now trigger-correct (bug fix):** `convertQuoteAction` inserted the booking straight as `confirmed`, but the invoice + calendar-block triggers are `AFTER UPDATE OF status` — so converted quotes silently got **no invoice and no calendar block** (double-booking risk). Now it inserts `pending`, attaches rooms/add-ons, snapshots policies, then UPDATEs to `confirmed` so both triggers fire exactly as a direct booking would. - New-quote listings are scoped to the logged-in host (was leaking all hosts' listings via public listing RLS).
    Migrations
    - `20260602000006_credit_note_cap.sql` — **bug fix:** the auto credit-note trigger credited the full `approved_amount` with no ceiling; an over-refund could mint a credit note exceeding its invoice. Now clamped to `LEAST(refund, invoice total)`. - `20260602000007_help_quotes_builder_update.sql` — refreshed the "Sending quotes" Help article for the new builder.
    Tests
    - `pnpm test:flows` now 49 checks (was 33). New journeys: **I** — confirm fires triggers only via UPDATE (regression guard for the convert bug, both ways); **J** — quote send soft-holds dates / convert clears them; **K** — a confirmed stay blocks every overlapping range (exact/partial/inner) + frees on checkout; **L** — over-refund credit note is capped at the invoice total. Engine units (22) + build + lint all green.
    Notes
    - The break-it sweep surfaced two real bugs (convert skipping invoice/blocks; credit note over-cap) — both fixed and now guarded by tests. ---
  149. Standardised document numbering2026-06-02

    branch `feat/financial-docs`

    Built
    - **One numbering convention across the app**, each with a prefix, a business/ property identifier, a short stable ID suffix, and a running count: - Quote `Q-{BIZ}-{ID5}-000001`, Invoice `INV-{BIZ}-{ID5}-00001`, Credit note `CR-{BIZ}-{ID5}-00001`, Refund `RF-{BIZ}-{ID5}-00001` — one continuous sequence **per business** (host_counters). - Booking `BK-{LISTING}-{ID5}-0001` — counted **per listing** (listing_counters). - `{BIZ}` = business/trading name (fallback handle); `{LISTING}` = listing name; `{ID5}` = 5-char slice of the host/listing id so two same-named businesses or listings can never collide on the global UNIQUE columns. - Refunds now carry a human `reference` (RF-…); generated on insert.
    Migrations
    - `20260602000010_doc_numbering_per_listing.sql` — `host_doc_code` / `listing_doc_code` helpers; rewrote `next_quote/invoice/credit_note_number`; added `next_refund_number` + `host_counters.last_refund_number`; `refund_requests.reference` + `bookings.reference` BEFORE INSERT triggers (dropped the old VILO- default); `listing_counters` table. - `20260602000008_quote_versions.sql` + `20260602000009_quote_addon_link.sql` — schema for upcoming quote editing/versioning + add-on→catalog link (quote_addons.addon_id).
    Tests
    - `test:flows` Journey M asserts every prefix/format (54 checks green). ---
  150. Quote editing + versioning, rich line items, payment history2026-06-02

    branch `feat/financial-docs`

    Built
    - **Editable quotes (incl. after sending):** Edit button on the quote detail page (draft + sent); new `/quotes/[id]/edit` route rehydrates the full builder — scope, rooms (selected/priced/guests), catalog add-ons (re-linked via addon_id), custom lines. - **Quote PDF version history:** editing a sent quote snapshots the prior state into `quote_versions` (bumping `quotes.version`); the detail page lists prior versions with date/time + total, each linking to its frozen PDF (`/quote/[id]/pdf?v=N`). The live quote is always the newest PDF. - **Rich quote line items:** "What's included" section with room cards (thumbnail, bed type, m², sleeps, short description) and add-on cards (thumbnail + description), pulled via `quote_addons.addon_id → addons` and `quote_rooms → listing_rooms` featured photo. - **Payment History:** the payment detail page's timeline is now a full financial audit trail across quote → booking → payment → invoice → refund → credit note, each event stamped with date + time. Plus the Financial overview anchor row with the Booking ID.
    Migrations
    - `20260602000008_quote_versions.sql`, `20260602000009_quote_addon_link.sql` (applied with the numbering batch). ---
  151. Host payment gateways: bring-your-own Paystack & PayPal2026-06-02

    branch `feat/host-payment-gateways`

    Built
    - **Per-host payment gateways (0% commission):** hosts connect their OWN Paystack and PayPal credentials so booking payments settle directly into their accounts — Wielo only ever charges a subscription. New `host_payment_gateways` table (one row per host+gateway), secrets encrypted at rest with a dedicated `PAYMENT_CIPHER_KEY` (AES-256-GCM, `lib/crypto/payments.ts`) and never returned to the client (UI shows `••••last4` only). - **Settings UI** under `/dashboard/settings/banking` → "Payment gateways": saved-data-card pattern (FormModal), per-gateway Connect/Edit + enable/disable + Remove, **live key validation on save** (Paystack `/balance`, PayPal OAuth token) — invalid keys are rejected. - **Statement descriptor** (Paystack): host-entered word shown on the guest's bank statement, stored per-host and forwarded on every transaction. - **Default currency** selector on the host (`hosts.default_currency`): ZAR→Paystack, USD→PayPal. Drives the default checkout gateway. - **"Request a payment"** — generates a shareable Paystack link on the host's own account so they can take a real payment today (pre guest-portal). - **FX conversion** (`lib/fx.ts`): ZAR→USD daily-cached rate (`fx_rates` table) from a free no-key API (open.er-api.com) with admin manual-override support. - **Gateway primitives:** `lib/paystack.ts` now accepts a per-host secret + statement descriptor (env key retained as fallback for Wielo subscription billing); new `lib/paypal.ts` (token/validate/createOrder/capture).
    Changed
    - `lib/paystack.ts` `initializeTransaction`/`verifyTransaction` gained optional per-host `secretKey` — existing platform-key callers unchanged.
    Migrations
    - `20260602000016_host_payment_gateways.sql` — `host_payment_gateways`, `hosts.default_currency`, `fx_rates`, `payment_gateways` plan-feature key. - `20260602000017_help_payment_gateways.sql` — Help Centre article.
    Notes
    - **Scope:** host side only (load/validate/accept). Guest checkout wiring (the currency↔gateway toggle at booking) is deferred to the dedicated guest-portal work, per founder direction. - **Not yet `db push`-ed.** `database.types.ts` hand-edited to match (Docker unavailable) — run `supabase db push --linked` + `supabase gen types typescript --linked` when ready. - **Add `PAYMENT_CIPHER_KEY`** to `.env.local` + Doppler before storing real keys (without it secrets are stored as plain text — see ENV_VARS.md §5a). - **To verify end-to-end:** paste Paystack test keys + a PayPal sandbox app and connect them in Settings → Banking & business → Payment gateways.
  152. Unified pricing engine + enterprise seasonal pricing2026-06-01

    branch `feat/seasonal-pricing-redesign`

    Built
    - **One canonical pricing engine** at `apps/web/lib/pricing` (`priceStay`) — a pure, fully-tested TypeScript module that is now the single source of truth for the server booking action, the client estimate, and the host seasonal preview. Preview, checkout, and invoice can no longer disagree. - **14 host/guest journey tests** asserting exact line-by-line totals — Vitest stood up in `apps/web` (script + config), per `TESTING.md`. These journeys double as the written "host configures X → guest does Y → system charges Z" narrative. - **Two seasonal-rule types:** **absolute** (set the exact nightly price; extra- guest fee still applies) and **percentage** (a +/- % that scales base + per-guest + extra-guest together, correct across multi-room and per-person listings). A percentage replaces the weekend rate on the nights it covers. - **Host-facing transparency:** a labelled per-night breakdown ("Festive season" / "Weekend" / "Standard") and an explicit discount line at checkout and on the invoice. New host help guide `docs/seasonal-pricing-guide.md` documents the 5-stage stack, the 3 golden overlap rules, absolute vs %, worked Rand examples, and common mistakes. - **Seasonal manager toggle** for choosing absolute vs percentage per rule (part of this change set).
    Changed
    - **Revenue-correctness fix:** seasonal and weekend pricing now actually reach the **charged total**. The authoritative booking path previously computed `base × nights` with no per-night seasonal/weekend resolution, so configured seasonal (and weekend) rates were **ignored** and guests silently paid base rate. They now flow all the way through. - **Weekend changed from Saturday + Sunday to Friday + Saturday** (DOW 5,6) — the industry-default leisure nights — and the whole stack was aligned to it, including the SQL `calculate_booking_price`, which was realigned (Fri+Sat + percentage) and kept as a DB-side cross-check against the TS engine.
    Migrations
    - `20260601000001_unified_pricing_engine.sql` — adds `discount_amount` and a `price_breakdown` JSONB audit snapshot to `bookings`; adds `adjustment_type` + `adjustment_value` to seasonal rules; realigns `calculate_booking_price` to Fri+Sat + percentage.
    Notes
    - ADR-020 records the decision (5-stage Pricing Stack; absolute + percentage rules; Sat+Sun → Fri+Sat weekend change; audit snapshot) and the deliberate deviation that the engine lives in `apps/web/lib/pricing` rather than a new `packages/utils` workspace package — avoids cross-package transpile setup in Next 14, every consumer is in `apps/web`, can be promoted later. - The `price_breakdown` snapshot is the frozen, auditable itemisation shared by checkout, invoices, refunds, and support. ---
  153. Discount coupons + invoice breakdown2026-06-01

    branch `feat/unified-pricing-engine`

    Built
    - **Enterprise discount-coupon system.** `coupons` + `coupon_redemptions` tables, `redeem_coupon()` atomic RPC, RLS, and a `coupons` feature gate (migration `20260601000004`). A coupon discounts the **whole order**, **accommodation only**, or **add-ons only**; can target one listing or one room; is percentage or fixed-amount; time-boxed; and capped by total + per-guest redemptions. Cleaning is never coupon-discounted. - **Engine integration:** `priceStay` applies a pre-validated coupon as the final discount stage; 5 new journey tests (J11–J15), **19 total green**. - **Server:** `resolveCoupon()` shared resolver, `validateCouponAction` (guest preview), and `createBookingAction` re-validates + re-prices + records the redemption atomically (rolls back on a cap race). `bookings` gain `coupon_id` + `coupon_discount`. - **Guest UI:** a coupon input on the checkout sidebar (apply / remove, live discount line, auto-clears when dates/rooms change). - **Host UI:** new `/dashboard/coupons` management page + nav entry (create / edit / toggle / delete, full targeting + limits). - **Invoice breakdown:** the invoice snapshot now carries `discount_amount` + the per-night `price_breakdown` (migration `20260601000003`); the PDF and the public HTML invoice show the discount line and an "includes N season-priced / weekend nights" note. - **Help Centre:** new published articles — "How seasonal pricing works" and "Discount coupons" (migrations `20260601000002` / `…005`), categorised under Listings.
    Changed
    - **New standing rule (`RULES.md` §9):** whenever a feature is added or its logic changes, create/update the matching Help Centre article in the same session, categorised correctly. Added to the Definition-of-Done checklist.
    Migrations
    - `20260601000003_invoice_breakdown_detail.sql` - `20260601000004_coupons.sql` - `20260601000005_help_coupons.sql` ---
  154. Add-ons redesign: archive grid + full editor page2026-05-31

    branch `feat/setup-wizard-rework`

    Built
    - **Add-ons archive** (`/dashboard/addons`) — redesigned to a card grid matching the "Add-ons Archive" design: stat tiles (Active / Drafts / Categories), category filter tabs + search + sort, and add-on cards (image, status pill, name, description, price, category). "New add-on" creates a draft and opens the editor. - **Add-on editor** (`/dashboard/addons/[id]`) — new full-page editor matching the "Add-on Editor" design: summary card, Details / Pricing / Availability / Photo sections, a "How is it charged?" picker (rich labels for each pricing model), category chips, VAT-included toggle, lead-time chips, daily capacity, guest-preview, "Ready to publish" checklist, Active toggle, delete, and a sticky save bar. - Replaces the old inline accordion (`AddonsManager` removed).
    Migrations
    - `20260531000001_addon_editor_fields.sql` — adds `addons.category`, `addons.vat_included`, `addons.daily_capacity` (single source of truth: DB → generated types → Zod schema).
    Notes
    - `pnpm build` + `pnpm lint` pass clean. Per-listing/room availability and the pre-arrival/ in-stay channels are surfaced read-only (managed in the listing editor / "Coming soon"), not faked.
  155. Checkout: editable dates + per-guest party manifest2026-05-31

    branch `feat/setup-wizard-rework`

    Built
    - **Editable check-in/check-out** on the checkout page — a `CheckoutDateEditor` lets the guest change dates; it navigates with updated `?from/?to` (preserving the other params), so the SERVER re-renders with fresh pricing + availability (nothing is computed on the client). Enforces min-stay and check-out > check-in. - **Optional party manifest** — the booker can name each additional guest (name + optional email/phone); persisted to `bookings.additional_guests` (jsonb), trimmed/capped to the guest count, so the host's booking card has the full party.
    Migrations
    - `20260531000002_booking_additional_guests.sql` — `bookings.additional_guests jsonb` (default []).
    Notes
    - `pnpm build` + `pnpm lint` pass clean. Date changes stay server-authoritative.
  156. Consolidated: checkout room picker/calendar + policies redesign2026-05-31

    branch `feat/setup-wizard-rework`

    Built (checkout)
    - **Compact month calendar** for changing check-in/out (range select, min-stay, navigates with new `?from/?to` so the server recomputes pricing + availability). - **Room picker always shows when the listing has rooms** (even whole_listing mode — a guesthouse can be booked by room or whole). Server relaxed to accept room-scope bookings whenever the rooms validate; whole-place toggle shows when the listing supports it. - **Manual per-room guest steppers**, clamped to each room's capacity; the count drives per-person/extra-guest pricing and the booking's `room_guests`.
    Built (policies — consolidated from the parallel rebuild)
    - Policies redesigned to the new "library" + editor: richer schema (default flag, house-rule flags, check-in method, versioning), `listing_policies` room assignment, legal presets (booking terms + POPIA), and a dark hero. Migration `20260531000003_policies_design_rework.sql`.
    Notes
    - Full `pnpm build` green (100/100 pages) — all agents' work compiles as one unit. - Still open (mine): wire the add-on editor's "Applies to rooms" to the listing's real rooms.
  157. Fix: host dashboard data not showing (ambiguous embeds), listings leak, robust account deletion2026-05-31

    branch `feat/setup-wizard-rework`

    Fixed (the big one — every host dashboard read was silently empty)
    - **Ambiguous PostgREST embeds returned zero rows.** `bookings` has two FKs to `user_profiles` (`guest_id` + `actioned_by`), so `guest:user_profiles!left(...)` threw *"more than one relationship found"*. The query error was swallowed (`const { data } = …`, no error check) → empty lists **and** all-zero KPI cards. Pinned the explicit FK in all five affected reads: - `dashboard/bookings/page.tsx` (list + Booked-revenue / New-bookings / Occupancy / Avg-nightly-rate cards) → `user_profiles!bookings_guest_id_fkey` - `dashboard/bookings/[id]/page.tsx` (detail page was silently 404-ing) - `dashboard/payments/page.tsx` (payments list + KPIs) - `dashboard/page.tsx` (home upcoming + recent bookings) → `…!bookings_guest_id_fkey!inner` - `dashboard/refunds/page.tsx` (`refund_requests` has 3 user FKs) → `user_profiles!refund_requests_guest_id_fkey`
    Fixed (data isolation)
    - **Listings portfolio leaked other hosts' listings.** `dashboard/listings/page.tsx` queried `listings` with no `host_id` filter, relying on RLS — but `listings` has a `public_read_published` policy, so every *published* listing from every host came back. Now resolves the host by `user_id` and filters `host_id = host.id` explicitly (with a comment warning never to drop it). Same pattern (relying on RLS where a `public_read` policy exists) may affect other dashboard reads of `listing_photos` / `seasonal_pricing` / `reviews` — flagged for the QA pass.
    Fixed (account deletion)
    - `deleteAccountAction` failed with *"Could not finalise account deletion"* — its pre-clear `.delete()` calls ignored returned errors and missed most of the host RESTRICT chain (bookings on own listings, payments, refunds, invoices, reviews, policy_snapshots). Rewrote to: (1) **safety-gate** — refuse while any *active* booking/refund exists, with a specific message telling the founder what to cancel first; (2) on a clear account, hard-delete historical rows in FK-safe order via new transactional RPC `app_purge_user_account`, then `auth.admin.deleteUser`.
    Migrations
    - `20260531000021_purge_user_account_fn.sql` — `app_purge_user_account(uuid)` SECURITY DEFINER teardown helper (service_role only). Applied to linked remote.
    Maintenance
    - Dropped stale test bookings/payments not belonging to the founder's host ("Wolie Se Plek") per founder request — demo-seed rows from past tests.
    Notes
    - `pnpm build` (100 pages) + `pnpm lint` green (only the pre-existing Help `aria-pressed` warning). Types regenerated from linked remote.
  158. Data-isolation sweep + a11y warning2026-05-31

    branch `main`

    Fixed (data isolation — sweep follow-up)
    - Read-only audit of every `dashboard/**` query for the two bug classes from the entry below. Pattern A (ambiguous embeds): clean. Pattern B (RLS public-read leaks) found 2 more unscoped `listings` reads, both now filtered by `host_id`: - `dashboard/page.tsx` — "your listings" preview (hoisted host resolution out of the parallel batch so the listings query can scope to `host_id`). - `dashboard/calendar/page.tsx` — the listing picker showed every host's published accommodation; now resolves the host and filters `host_id`.
    Fixed (a11y / lint)
    - `help/_components/PopularArticles.tsx` — `aria-pressed` on a `role="tab"` button → `aria-selected`. `pnpm lint` now clean, zero warnings.
  159. Calendar redesign: console + KPI layouts, month/timeline, drag-to-block2026-05-31

    branch `main`

    Built (from the `Calendar.html` design pack)
    - Rebuilt `/dashboard/calendar` to the mockup. Two layouts, switchable via a persisted **A⇄B toggle** (saved to localStorage, default **A**): - **A · Console** — calendar hero + right rail (occupancy ring, revenue/ADR, origin mix, today's arrivals/departures, upcoming check-ins). - **B · KPI-first** — 4-tile KPI strip, full-width calendar, horizontal upcoming rail. - **Month grid** with spanning, lane-packed booking bars (`+N more` overflow), per-day price (seasonal overrides), booking popover, and an add/block/edit popover; **Timeline view** (listings as rows, days across). - **Drag-to-block** across days (and single-day block via the popover), wired to a new `setManualBlocksAction` bulk block/unblock (listing-wide manual blocks; booked + quote-held days are protected). Optimistic UI with server resync on error. - **Filters** (status + origin) and **field toggles** (avatar/name/status/origin mark/price/rate/check-in time/guests); month nav + listing switcher.
    Data mapping
    - "Channel" → booking **origin** (Direct / Manual / From-quote), since Wielo is direct-booking. External **iCal** blocks render as a distinct hatch + source label — future-proofed (`reason` like `ical:airbnb`); no rows until the iCal import Edge Function ships. - All reads host-scoped; bookings use the `user_profiles!bookings_guest_id_fkey` hint; blocks scoped to the host's listing ids (blocked_dates is public-read). - Replaced the old basic month grid; removed `CalendarBoard`/`CalendarMonth`/ `ListingPicker`/`RoomPicker`/`IcalExportPanel` (iCal export lives on `/dashboard/calendar-sync`).
    Notes
    - New calendar files type-check + lint clean. (Repo-wide `pnpm build` currently blocked by an unrelated in-progress homepage edit in `_components/home/*` — not part of this commit.)
  160. Public homepage wired to live data (no more hardcoded stays/reviews)2026-05-31

    branch `main`

    Built
    - `apps/web/app/_components/home/home-data.ts` — single `getHomeData()` server loader that fetches the whole public homepage from Supabase in one parallel batch, mirroring the exact `listings` query shape used by `/explore` and `/c/[slug]`. Resilient: every empty/failed read yields a safe empty slice so the page never throws.
    Changed
    - `app/page.tsx` is now an `async` server component (`dynamic = "force-dynamic"`) and passes real data into every section. - **FeaturedListings** — real `is_featured` listings (falls back to top-rated then newest if too few are flagged); cards link to `/listing/[slug]`, price uses the shared rooms_only/experience logic, "Show all N stays" → `/explore` with the real published count. - **TrendingDestinations** — real cities aggregated from published listings (count + representative photo), cards link to `/explore?where=<city>`. - **RecentReviews** — real published, non-flagged reviews. Anonymised as "Verified guest" + listing name + month/year (user_profiles is not publicly readable — matches `/[handle]`). Dropped the fake "4.83 / 12 489" stat. - **BrowseByType** — real top-level accommodation categories with live counts + from-price + category hero image, linking to `/c/[slug]`. - **CategoryChips** — now a server component driven by the taxonomy; leaf categories link into `/explore?type=<slug>` (was a dead client toggle). - **Hero** — real property/host/province stats, badge count, and popular-city chips (link to `/explore?where=<city>`); "0% guest booking fees" kept. - **DealsBanner** — fixed two dead `href="#"` links → `/explore` and `/explore?guests=8`.
    Notes
    - Empty sections (no listings / destinations / reviews) render nothing rather than a broken grid, so a sparse pre-MVP DB still looks intact. - `pnpm build` + `pnpm lint` both green; `/` is now server-rendered (ƒ).
  161. Remove Experiences/tour-guide surface (MVP = accommodation only)2026-05-31

    branch `main`

    Changed
    - Scoped the whole app to **accommodation listings only**. Experiences / tour-guide operators are deferred until that separate track is built; this was a code-only removal — no migrations, the DB schema (the `experience` enum value, `experience_type`, and the experience-only listing columns) and the seeded "Experiences" taxonomy rows are all left intact for an easy re-enable later. - **Taxonomy** (`lib/taxonomy/*`): `CategoryKind` narrowed to `"accommodation"`; `getCategoryTree`, `getAllCategoriesForAdmin` and `getCategoryBySlug` now filter `kind = 'accommodation'`, so experience categories never load and `/c/<experience-slug>` 404s. - **Admin categories**: removed the kind dropdown + the two-section table; single Accommodation section, parent queries filtered to accommodation. - **Host signup + new-listing + setup**: removed the accommodation-vs- experience chooser, `experienceType`/`EXPERIENCE_TYPES`, and the experience-only editor tabs (Logistics, Schedule) + branches in Pricing/Policies/Basic. New listings always insert `listing_type = 'accommodation'`. - **Guest flow**: deleted `ExperienceBookingWidget`, `ExperienceBookingForm`, the editor `LogisticsTab`/`ScheduleTab`, and `scheduleSlots.ts`; collapsed every `listing_type === 'experience'` branch in the listing page, checkout (`book/`), booking actions, success page + `BookingConfirmation`, and the guest trip views to the accommodation path. Booking `scope` enum is now `whole_listing | rooms`. - **Discovery + profiles + admin lists**: explore, `/c/[slug]`, `/[handle]`, home-data, and the admin booking/listing/host views now hard-filter `listing_type = 'accommodation'` and dropped the experience chips/labels. - **Copy**: marketing pages, legal docs, and emails no longer mention "experience operators".
    Not touched (intentional)
    - The per-room **`experiences`** highlights field (`roomEnums.EXPERIENCES`, RoomDetailsForm) and the **"Experiences" add-on category** are unrelated to the Experiences product and were left as-is.
    Notes
    - `pnpm build` + `pnpm lint` both pass clean. - Re-enabling tour guides later means re-wiring UI only (and re-seeding the taxonomy rows if you ever delete them) — the data model is unchanged.
    Commit
    - `feat: scope app to accommodation only (remove experiences surface)` — [pending] ---
  162. Inbox full-bleed layout rule (host + guest)2026-05-31

    branch `main`

    Built
    - New `apps/web/lib/layout/fullBleed.ts` — single source of truth for which logged-in routes break out of the standard padded `max-w-[1280px]` shell and render full-width + full-height instead. `FULL_BLEED_ROUTES` = `/dashboard/inbox` + `/portal/inbox`; `isFullBleedRoute()` is exact-match.
    Changed
    - `app/dashboard/layout.tsx` now imports the shared rule instead of its own inline `FULL_BLEED_ROUTES` copy (no behaviour change on the host side). - `app/portal/layout.tsx` (guest dashboard) now applies the same full-bleed height chain (`h-[100dvh] overflow-hidden` shell, `min-h-0` main, no padding / no max-width cap) when on `/portal/inbox`. Previously the guest inbox was forced into the padded shell. - `app/portal/inbox/page.tsx` restructured into a full-height column with an internal scroll region so it fills the full-bleed canvas correctly. - `CONVENTIONS.md` §7.5 documents the rule so the inbox can't silently revert to the padded shell on one dashboard.
  163. Host booking-detail redesign2026-05-31

    branch `feat/listing-page-redesign`

    Built
    - Rebuilt `/dashboard/bookings/[id]` to the "Booking Details" design: dark gradient hero (status + proximity + channel chips, stay-journey tiles, booked→arrival→checkout progress bar), guest card with real returning-guest stats (stays + lifetime value with this host, member-since), reservation card (cover photo, occupancy, channel, cancellation, guest note, rooms, add-ons), payment & payout breakdown, real activity timeline from booking timestamps, and a sticky right rail (workflow actions, quick actions, stay policy, internal notes). - Internal-notes thread now reads/writes the real `booking_notes` table via a new `addBookingNoteAction` (host-only `InternalNotes` client component).
    Changed
    - `BookingActions` (status transitions) moved into the right-rail workflow card (amber "Awaiting your confirmation" treatment for pending bookings).
    Notes
    - All content is real DB data — no placeholder door codes / fake verification badges. Sections render conditionally when their data exists. - Built alongside a concurrent agent on the same branch (listing-page redesign); scoped edits to `dashboard/bookings/**` only. `pnpm build` + `pnpm lint` green with both sets of changes present.
  164. Guest portal "My trips" redesign2026-05-31

    branch `feat/listing-page-redesign`

    Built
    - Rebuilt `/portal/trips` to the "My Trips Page" design: page header with greeting + "Find a stay" button, a featured **Next trip** hero (cover image, days-to-go countdown ring, dates/nights/room facts, host + reference, view / message / directions actions), and an **Upcoming / Past / Cancelled** tab switcher over a 2-column card grid. - Trip cards carry a status badge (Confirmed / Awaiting host / Completed / Cancelled), location, dates, room + guests, host avatar, price (or refunded amount for cancelled), reference, and status-aware actions (View booking / View request / Leave a review / Book again / Rebook + message/receipt). - All data is real: bookings joined to listing cover photo (`listing_photos`), host avatar, booked room names (`booking_rooms`), and the guest's `reviews` to drive the "You rated"/"Leave a review" states; refunds use `bookings.refund_total`.
    Changed
    - `/portal/trips` split into a server `page.tsx` (data + bucketing) and a client `TripsClient.tsx` (tabs/featured/cards). Bucketing: cancelled set → Cancelled; live/pending with future check-out → Upcoming; else → Past. The soonest upcoming stay is featured.
    Notes
    - Sidebar/top chrome unchanged — the existing `PortalSidebar` already mirrors the mock. Reused existing tokens/animations (`shadow-glow`, `rounded-card`, `wielo-ring-pulse`, `wielo-step-enter`, `wielo-hide-sb`); no globals.css edits. - Trip detail still links to `/my-trips/[id]`; "Leave a review" to `/review/[bookingId]`. `pnpm build` + `pnpm lint` pass.
  165. Public listing page redesign (Listing Page template)2026-05-31

    branch `feat/listing-page-redesign`

    Built
    - Reworked the guest listing page (`apps/web/app/listing/[slug]/`) to match the provided "Listing Page" design as a fixed standard layout (no host edit-mode): breadcrumb, Superhost pill, standard verified-host trust card, 5-tile gallery, collapsible About, amenities "show all". - **Whole-guesthouse toggle + real discounts**: shared pure `pricing.ts` (`applyStayDiscounts`) used by the booking sidebar/widget/mobile bar AND `createBookingAction` (source of truth) — whole-listing combo % (all active rooms together) + weekly (7+) / monthly (28+) length-of-stay %. - **Rates & seasonal section** (live `listing_seasonal_pricing`: current-season callout, legend cards, per-room/whole rate table). - **Availability calendar** (two-month, live `blocked_dates`; interactive range picker wired to cart dates; read-only viewer for whole-listing). - **Full reviews section**: distribution, per-category bars (6 sub-ratings), trip-type filter pills, "Guests mention" themes, featured pull-quote, review grid with a real Helpful vote (`review_helpful_votes` + trigger). `trip_type` added to the guest review form. - **Location**: keyless Leaflet + OSM map (approximate-location circle) + host-curated Eat/Do/Travel neighbourhood (`listing_points_of_interest`). - **Meet-your-host** stats card, **Similar stays** grid (same province), and a **mobile sticky booking bar**. - **Host editors**: discount % fields in the listing Pricing tab; new `/dashboard/listing-extras` page (CRUD for neighbourhood POIs + review themes).
    Changed
    - `book/actions.ts` now applies combo + length-of-stay discounts server-side (charged total reflects them). Booking sidebar/widget show discount lines. - Listing query loads extra host fields, coords, seasonal rows, blocked dates, POIs; reviews now fully loaded (were aggregate-only).
    Migrations
    - `20260531000030_listing_page_redesign.sql` — discount cols on `listings`, `listing_points_of_interest`, `reviews.trip_type`/`helpful_count` + `review_helpful_votes` (+ sync trigger), `listing_review_themes`, feature-gate seeds (open on every plan pre-MVP). Types regenerated; demo seed enriched.
    Notes
    - New deps: `leaflet` + `@types/leaflet` (vanilla, keyless — no react-leaflet). - Whole-listing discount applies only to the rooms-combo (all active rooms), not whole-listing-scope bookings (those price off `base_price`); LOS applies to both. "Guests mention" counts are host-curated (can be auto-derived later). - Demo: guesthouse listing `the-vines-guesthouse-stellenbosch` exercises every new section (rooms, discounts, seasons, blocks, POIs, themes, 4 reviewers).
    Commit
    - `feat(listing): phases 0–9 — public listing page redesign` — branch `feat/listing-page-redesign` ---
  166. Seasonal pricing redesign (Seasonal Pricing template)2026-05-31

    branch `feat/seasonal-pricing-redesign`

    Built
    - Rebuilt `/dashboard/seasonal-pricing` (`SeasonalPricingManager.tsx`) to match the provided "Seasonal Pricing" design, fully wired to real data (`listing_seasonal_pricing`, `listings`, `listing_rooms`): - Per-listing **tab switcher** (replaces the stacked-cards layout) + a **year selector** derived from the rules' actual date spans. - **4 KPI cards**: base rate / night, weekend rate (+% vs base), seasons set (with covered-nights count + per-tier share bar), projected uplift (Σ over the year of effective price − flat base, weekend uplift included). - **Year rate-calendar timeline**: listing-wide active rules plotted by day-of-year, bar height vs a price scale, dashed base-rate line, today marker, Jan–Dec axis, tier legend. - **Pricing-rules sidebar** (base / weekend uplift / cleaning fee / peak min nights) + a real computed "Year at a glance" card (guest-facing price range + average — replaces the design's AI mock, no fabricated content). - **Seasons table**: All/Upcoming/Past filter, tier colour bar, derived sub-label (room name or tier descriptor), date range, nights, rate, vs-base %, status pill (Active / Starts tomorrow / Upcoming / Past / Inactive), and a kebab menu (edit / activate-deactivate / delete). - **Guest-preview strip** mirroring the public listing `RatesSection` (base + per-season groups + computed avg/night). - **Copy to listing**: new `copySeasonalRulesToListingAction` copies a listing's listing-wide seasons onto another listing (fulfils the deferred bulk-copy item); merges returned rows into client state. - **Export**: client-side CSV download of the selected listing's seasons.
    Changed
    - `page.tsx` now also loads `cleaning_fee` (listing + rooms) for the KPI / pricing-rules cards, and renders the manager full-bleed (it owns the page heading); the plain `Header`/empty/upgrade states are unchanged. - All create/edit/delete/toggle/overlap-warning/priority logic preserved via the existing `RuleDialog` + server actions — only the presentation changed.
    Migrations
    - **None.** The design's season "tier" (peak/high/shoulder/low) is **derived** from price-vs-base %, so no schema change was needed (also avoids the no-Docker type-regen path). `listing_seasonal_pricing` already carries every field the design needs.
    Notes
    - Tier thresholds: ≥ +40% peak, ≥ +15% high, ≥ 0% shoulder, < base low. - `season-*` palette isn't in the app Tailwind config; tier colours are applied via inline `style` hex to match the design exactly. - Demo data renders it: `pnpm seed:demo` seeds "December Peak" etc. on listing A.
    Commit
    - `feat(seasonal-pricing): redesign manager to template + wire real data` ---
  167. Fix: scope seasonal-pricing page to the logged-in host2026-05-31

    branch `feat/seasonal-pricing-redesign`

    Fixed
    - `/dashboard/seasonal-pricing` listed **every other host's** published listings. The page read `listings` relying on RLS alone, but the `public_read_published` policy returns the whole directory. Added an explicit `.eq("host_id", host.id)` filter (same fix already applied to the rooms/listings pages). - The seasonal rules read (`listing_seasonal_pricing`) was likewise unscoped and has a `public_read_seasonal_pricing` policy — now scoped to the host's listing ids via `.in("listing_id", hostListingIds)`. Write actions were already guarded by `assertListingOwnership` / `assertRuleOwnership`, so no mutation leak existed. ---
  168. Settings pages adopt the setup dark-hero + chip-tab design2026-05-30

    branch `feat/setup-wizard-rework`

    Built
    - **`components/settings/SettingsHero.tsx`** — standalone dark gradient hero shell (re-uses the shared `bg-brand-gradient-dark` + `setup-dotgrid` tokens, drops the wizard-only progress ring / publish button). Props: `title`, `subtitle`, `backHref`, `backLabel`, plus a `children` slot for the tab nav.
    Changed
    - Both settings areas now lead with the dark hero instead of a plain text header, matching the `/dashboard/setup` look: - Host `dashboard/settings/layout.tsx` (back → `/dashboard`). - Guest `account/settings/layout.tsx` (back → `/my-trips`). - Tab navs restyled from underline tabs to dark-surface pill chips inside the hero (`SettingsTabs.tsx`, `AccountSettingsTabs.tsx`) — markup only; `TABS`, `usePathname`, and active-state logic unchanged. - Profile tab brought in line with the Banking & business tab: the (bare) shared `HostProfileForm` is now wrapped at the page level (`dashboard/settings/page.tsx`) in the same white-card chrome (icon tile + title + divider), and `PasswordCard` swapped its shadcn `Card` for that same custom chrome. `HostProfileForm` itself was not edited (shared with the setup wizard) — the card wrapper lives in the page.
    Notes
    - Design-only change: no routes, forms, Server Actions, or schemas touched. Each tab stays its own routed page so every existing form keeps working. - Deliberately did NOT touch the in-flight setup wizard (`SetupWizard.tsx`, `steps/*`) or the public profile work (`[handle]/page.tsx`, `ProfileTabs.tsx`); `SettingsHero` is standalone and does not import from the wizard. - Setting forms keep their existing on-brand cards; the numbered-badge "SectionCard" wizard pattern was intentionally not applied (not requested, wizard-specific).
  169. Enterprise room management: bed-derived capacity + per-room pricing modes2026-05-30

    branch `feat/setup-wizard-rework`

    Built
    - **Bed editor + derived capacity** — one canonical `RoomDetailsForm` (used by the setup wizard, the standalone room page, and the listing-editor rooms tab) now manages a room's beds (add/remove, kind + qty, incl. the new **Futon**). A room's `max_guests` is **derived strictly from its beds** (Σ bed capacity × qty) and shown live as "Sleeps N" — never hand-typed. - **Three pricing modes per room** — `per_room` (flat + optional weekend), `per_person` (rate × guests/night), `per_room_plus_extra` (base covers `base_occupancy`, then `extra_guest_price` per extra guest). Flat cleaning fee in every mode. - **`roomBeds.ts`** — single source of truth for bed kinds + per-kind capacities + `roomCapacityFromBeds()`. **`roomDisplay.ts`** gains shared `roomNightlyBase` / `roomFromNightly` / `roomPriceLabel` used by the grid, cart, and server alike. - **Booking flow** — guests set guests *per room* (capped at each room's capacity); the cart, confirm page, and `createBookingAction` all price each room by its mode. Public room cards show the right label ("R900/night", "R300/person/night", "R900/night base").
    Changed
    - The inline `RoomRowEditor`'s duplicate Details/Beds tabs are retired — it renders the shared form now (no drift). Room flags + floor/inventory moved to its "Amenities & setup" tab. - `recomputeListingFromRooms` now uses each room's effective "from" price by mode. - `setRoomBedsAction` derives + writes `max_guests` and recomputes the listing.
    Migrations
    - `20260530000001_room_enterprise_pricing.sql` — adds `'futon'` to the `room_beds` bed-kind CHECK; adds `listing_rooms.pricing_mode` / `price_per_person` / `base_occupancy` / `extra_guest_price`; backfills `max_guests` from beds. Applied to cloud + DB types regenerated.
    Notes
    - Server is the price source of truth — `createBookingAction` recomputes per room and validates per-room guests against bed-derived capacity; the client never sets price. - Onboarding / finish-setup verified green throughout (the wizard reuses the same form).
    Commit
    - `feat(rooms): phase 1 — schema for bed capacities + pricing modes` — `ee97c6f` - `feat(rooms): phase 2a — canonical form gains bed editor…` — `1002678` - `refactor(rooms): phase 2b — listing editor uses the one canonical room form` — `4b8f01b` - `feat(rooms): phase 3 — booking flow honours per-room pricing modes` — `632203c` ---
  170. Canonical notification-modal system + full-app popup migration2026-05-30

    branch `feat/setup-wizard-rework`

    Built
    - **`<Modal>`** (`components/ui/modal.tsx`) — the one canonical popup shell from the design system's "Notification modals": `max-w-sm` card, icon chip, title, message, optional key/value detail box, right-aligned footer buttons. Six intents — `success | info | warning | error | confirm | destructive` — each with its own icon + tint. Brand backdrop `bg-brand-dark/60 backdrop-blur-sm`. Async action handlers with pending/disabled state. - **Imperative API** (`components/ui/modal-host.tsx`) — `modal.success/info/warning/error(...)` (→ `Promise<void>`) and `modal.confirm/destructive(...)` (→ `Promise<boolean>`), callable from anywhere. Dependency-free external store via `useSyncExternalStore`. `<ModalHost />` mounted once in the root layout. - **`<FormModal>`** (`components/ui/form-modal.tsx`) — same shell sized for forms (header + scroll body + pinned footer; `FormModalFooter`, `FormModalCancel`, `size` sm/md/lg). For popups that contain a form (e.g. "Add seasonal price").
    Changed
    - **Whole-app popup migration** — replaced every `window.confirm`/`window.alert` (13 files: booking/quote actions, policies, staff, add-ons, rooms, room photos, reviews, calendar-sync feeds, seasonal pricing, admin categories) with `modal.destructive` / `modal.confirm` / `modal.error|warning`. Converted the 4 shadcn-`Dialog` form popups (bank account, policy viewer, listing settings, seasonal-price rule) to `<FormModal>`. Side/bottom **sheets** intentionally left as sheets (separate design-system pattern). - Design system: new **Notification modals** section (+ action/form-modal example + nav link) in `Wielo Design System.html`, mirrored to `apps/web/public/DESIGN_SYSTEM.HTML`. New hard rule in `DESIGN_SYSTEM.md`: no raw `Dialog`/`AlertDialog`/`window.confirm` — every popup uses the modal shell.
    Notes
    - `pnpm build` + `pnpm lint` pass clean (one pre-existing unrelated a11y warning in `PopularArticles.tsx`). - Toasts (sonner) deliberately kept for non-blocking result notifications — they're a separate sanctioned component. Only blocking confirms/alerts/error popups moved to modals.
  171. Public profile/room redesign + setup-hero pills + profile schema2026-05-30

    branch `feat/setup-wizard-rework`

    Built
    - **Setup hero step pills** — the "Finish setting up" hero pills are now two-line (icon chip + label + status: "Done"/"In progress"/"To do"/"Final step"), matching the provided design; green check chip when done, rocket on the final step. - **Public host profile redesign** (`app/[handle]`) — matches the "Split host rail / tabs" design: Superhost + Verified badges, "Confirmed information" rows (Identity/Email/Phone/ Payout), host highlight pills, and a **review rating breakdown** (Cleanliness, Communication, Check-in, Accuracy, Location, Value averaged from sub-ratings). Reviews stay anonymised ("Verified guest") per privacy rules. - **Public room page redesign** (`app/listing/[slug]/rooms/[roomId]`) — breadcrumb, stats grid, About, room highlights, sleeping arrangement, amenities, "Good to know", "part of listing" cross-link, and a new interactive **RoomBookingWidget** (dates + guests + live client-side price breakdown; server still recalculates on the book flow). - **Editing UI** — host "Highlights" tag editor in profile settings; optional per-category star inputs in the guest review form so the breakdown populates.
    Migrations
    - `20260530000003_profile_review_enrichment.sql` — adds `reviews.rating_{cleanliness, communication,checkin,accuracy,location,value}` and `hosts.{highlights,is_superhost, phone_verified,payout_verified}`. Additive/nullable; types updated.
    Notes
    - Public pages read the new columns via **error-tolerant supplementary queries**, so they degrade gracefully (sections hidden) and never 500 even if the prod migration lags the deploy. `db-migrate.yml` runs before Vercel on push to `main`, so schema lands first anyway. - `pnpm build` + `pnpm lint` pass clean (one pre-existing unrelated a11y warning).
  172. Checkout flow redesign + guest account at checkout2026-05-30

    branch `feat/setup-wizard-rework`

    Built
    - **Checkout redesign** (`app/listing/[slug]/book` accommodation path) — matches the "Confirm and pay" design: 3-step progress stepper (Review → Payment → Confirmation), sectioned cards (Your rooms, Your trip with check-in/out tiles + guests, styled add-ons, Payment method, cancellation policy), a branded sticky price-summary sidebar with the full breakdown + "Wielo service fee FREE", and a mobile sticky reserve bar. All existing pricing / add-on / per-room / scope logic preserved; payment still goes through Paystack's hosted checkout (no raw card entry). - **Guest account at checkout** — anonymous visitors can now reach the accommodation checkout (no forced pre-login) and create a guest account inline (full name, email, password). On reserve, `createCheckoutGuestAccountAction` creates an auto-confirmed user + signs them in (reusing the proven signup/guest pattern), then the booking proceeds as that user. Existing-email collision shows a "sign in" prompt. Experiences still require login.
    Notes
    - `pnpm build` + `pnpm lint` pass clean (one pre-existing unrelated a11y warning). - Held on the feature branch (not yet on `main`) — it changes who can reach checkout (anonymous) and creates accounts, so it awaits a go-live confirmation.
  173. Checkout: listing context, room picker, add-ons, contact capture, payment methods2026-05-30

    branch `feat/setup-wizard-rework`

    Built
    - **Listing context** — the checkout summary now leads with the listing's **feature image**, type · city, name and ★ rating · reviews (with an Instant Book overlay) so the guest clearly sees where they're booking. - **Room selection** — the guest can pick which room(s) to book on the checkout page (all active rooms render as selectable cards with photo, beds, sleeps, features and live price); flexible listings get a "Book the whole place" toggle; rooms-only requires ≥1. Pricing recomputes live via the shared `roomNightlyBase`. - **Add-ons** — section shows the host's add-ons; seed migration adds 2 sample add-ons (Breakfast hamper, Airport transfer) per host, linked listing-wide, so it's testable. - **Full contact capture** — name, email, phone and message-to-host are collected and snapshotted onto the booking (`guest_name/email/phone`, `special_requests`) so the host's booking card is fully populated. Signed-in guests get a "Log out & use another account" link (browser sign-out + refresh, stays on checkout). - **Payment methods from the host's setup** — "Pay with card" (Paystack) always; "EFT bank transfer" appears only when the host has default banking. EFT creates a `pending_eft` booking (no Paystack hop) and sends the guest to their trip page.
    Migrations
    - `20260530000004_seed_sample_addons.sql` — idempotent sample add-ons per host.
    Notes
    - `pnpm build` + `pnpm lint` pass clean. Payment values use the DB-allowed `eft` (not a custom string). Follow-up: surface the host's bank details + reference on the guest's `/my-trips/[id]` page for the EFT flow (booking + host notification already work).
  174. Policy Manager (`/dashboard/policies`)2026-05-29

    branch `feat/policy-manager`

    Built
    - **Central Policies section at `/dashboard/policies`** managing three independent, separately-assignable kinds: **Refund terms** (`cancellation`), **Check-in / Check-out** (`check_in_out`), and **House rules** (`house_rules`). Each is created once and assigned to a whole listing or overridden per room. - **The 3 refund presets (flexible/moderate/strict + non-refundable) are locked** — materialised per-host as real `policies` rows by a new idempotent RPC `ensure_host_policy_presets()` (seeded lazily on first page visit / create). Locked = `preset <> 'custom'`; hosts **Duplicate** a preset to customise it. - **WYSIWYG full-policy editor** (reuses `components/editor/RichTextEditor`, TipTap) + a short `summary` for cards/checkout. Refund terms get a rules repeater (days-before → refund-% + label) and a non-refundable toggle. - **Guest-facing popup** — shared `components/policy/PolicyDialog` (read full terms) + server `components/policy/ListingPolicyBlock` rendered on the listing detail page (replacing the dead `href="#"` "Read full policy" link) and the checkout page (both stay/experience paths). Falls back to the legacy `CANCELLATION_BLURB` when no policy is assigned. - **Booking snapshot wired** — `book/actions.ts` now calls the pre-existing but never-invoked `snapshot_booking_policies()` RPC after the booking insert, so `calculate_policy_refund_amount()` finally has a snapshot to read.
    Changed
    - Migration `20260529000000_policy_manager_ui_support.sql`: extends the `type`/`policy_type` CHECKs (adds `check_in_out`, `house_rules`); adds `policies.summary`/`check_in_time`/`check_out_time`; adds `listing_policies.room_id` + NULL-safe partial unique indexes (mirrors `listing_addons`); `CREATE OR REPLACE`s `snapshot_booking_policies` + `get_listing_policy_summary` (new types + summary) and extends `sync_listing_policy_label` to keep `listings.check_in_time`/`check_out_time`/ `house_rules` in sync from the listing-wide assignment; seeds `plan_features` `'policies'` = true on all plans (pre-MVP, §3.4). - Listing editor `PoliciesTab` rewritten from the 3-preset radio to three policy pickers (listing-wide + per-room overrides) calling a new `setListingPolicyAction`; `edit/page.tsx` + `Editor.tsx` fetch/thread the new `availablePolicies`/`assignedPolicies` props. - Onboarding `StepPolicies` additionally assigns the matching preset listing-wide (best-effort) so onboarding listings are refund-ready. - Sidebar: new **Policies** link in Tools.
    Notes
    - The whole Domain-11 DB foundation (5 tables, RLS, functions, triggers, seed templates) already existed from `20260502000000..0008` and was unused — this session is mostly UI + per-room assignment + the one missing snapshot call. - `body_html` is sanitised at write time via `sanitiseListingHtml` so the shared client dialog renders trusted markup. - Not yet committed; pending `supabase db reset` + type regen + `pnpm build/lint` (Docker was down at code-time). To be merged into `main` later.
  175. Listing card single-source-of-truth: Amenities + Photos2026-05-29

    branch `feat/setup-wizard-rework`

    Built
    - **`components/listing/AmenitiesPicker.tsx`** — one grouped amenity selector + save (`replaceAmenitiesAction`), with optional per-room assignment. Rendered by the listing editor's Amenities tab AND the setup Listing card (listing-wide). Amenities now exist in the setup flow (was editor-only before). - **`components/listing/PhotosManager.tsx`** — one photo manager: multi-file upload, drag-to-reorder (first photo = cover), delete, with optional per-room assignment. The editor's `PhotosTab` and the setup Listing card are now both thin wrappers over it; setup gains multi-upload + reorder for free.
    Changed
    - Editor `AmenitiesTab` / `PhotosTab` reduced to thin wrappers (Card chrome + the shared component). - Setup `StepListing` drops its bespoke single-file photo grid; `SetupWizard` now passes a single `onPhotosChanged(next)` callback (was add/remove pair).
    Notes
    - This completes the Listing-card source-of-truth set: **Basics · Photos · Amenities · Rooms** are each now one component shared between `/dashboard/setup` and the listing editor / sidebar. - A concurrent agent's in-progress public-profile work (`app/[handle]/page.tsx` + `ProfileTabs.tsx`) was accidentally bundled into the amenities commit, then **split back out** (force-update of `main`); that work is preserved uncommitted in the tree and recoverable from old commit `f86aae5`. Other agent now stopped.
    Commit
    - `feat(listing): shared AmenitiesPicker …` — `ad14dd8` - `feat(listing): shared PhotosManager …` — `3eed730` ---
  176. Phase 1/22026-05-24

    Per-room bookings end-to-end (schema → editor → guest flow → calendar → iCal)

    Built
    - **`migrations/20260524000000_per_room_bookings.sql`** lands the per-room domain: `listing_rooms` + `booking_rooms` tables, `listings.booking_mode` (`whole_listing` / `rooms_only` / `flexible`), `bookings.scope` (`whole_listing` / `rooms`), nullable `room_id` on `blocked_dates`, `listing_photos`, `listing_amenities`, scope-aware unique indexes on blocked dates + amenities, `on_booking_confirmed` rewritten to block per-room or whole-listing, two new SQL helpers (`room_is_available`, `listing_is_available_whole`), RLS policies for the two new tables, and a `touch_listing_rooms_updated_at` trigger. - **Listing editor — Basic info tab** gains a **Booking mode** card (Whole place / Rooms only / Both). Switching to per-room is blocked until the host adds at least one room. - **Listing editor — Rooms tab** now hosts a `RoomsManager` (collapsible rows, per-room name / description / capacity / pricing / cleaning fee / active toggle) plus the existing whole-listing capacity form. Add / edit / soft-delete a room. Delete refuses if any active booking references the room. - **Listing editor — Photos tab** accepts the rooms prop and renders an overlay "Listing-wide / room name" picker on hover for each photo when the listing has rooms. Picker calls `assignPhotoToRoomAction`. - **Listing editor — Amenities tab** rewritten to accept the full `EditorAmenity[]` (with id + roomId) and a `rooms` prop. Per amenity, when rooms exist, a "Listing-wide / room name" select assigns the amenity to a specific room. - **Editor Server actions** (`actions.ts`): - `setBookingModeAction` — guards switching to per-room without rooms. - `createRoomAction` / `updateRoomAction` / `deleteRoomAction` — full CRUD with sort_order assignment and active-booking guard on delete. - `assignPhotoToRoomAction` / `assignAmenityToRoomAction` — update `room_id` on the join row. - `replaceAmenitiesAction` now snapshots the existing `amenity_key`→ `room_id` map before the wipe, re-applies it on the reinsert, and returns the new rows (with fresh IDs) so the per-room dropdown updates immediately after save without a page reload. - **Listing detail (`/listing/[slug]`) — cart pattern.** New `RoomsCartProvider` (React Context), `RoomsGrid` (left-column room cards with Add/Remove toggle, photo, capacity, price), and `RoomsCartSidebar` (shared dates, room picks, total, reserve CTA). - `whole_listing` mode → existing single `BookingWidget`. - `rooms_only` mode → room grid + cart sidebar. - `flexible` mode → cart sidebar with **Whole place / Specific rooms** pill tabs; switching tabs clears the room selection. - **Booking page (`/listing/[slug]/book`)** parses `?room_ids=A,B,C` from search params, refuses if scope/mode disagrees, fetches the picked `listing_rooms`, and surfaces them in a "Your rooms (N)" panel inside the `BookingForm` with per-row subtotal + remove button. Removing the last room redirects back to the listing. - **`createBookingAction`** now branches on `scope`: - `rooms` → validates every room_id belongs to the listing, server-recalculates price per room (never trusts the client per AGENT_RULES §1.2), runs `room_is_available` per room, refuses if any room is taken, inserts the `bookings` row + N `booking_rooms` join rows. - `whole_listing` → runs `listing_is_available_whole`, existing path otherwise. - Paystack init unchanged (the recalculated total goes through untouched). Failed insert paths roll back booking + booking_rooms + payment so retry is clean. - **Dashboard calendar (`/dashboard/calendar`)** gains a per-room sub-picker (`RoomPicker`) next to `ListingPicker` for listings whose `booking_mode` is not whole. Options: Any room (default) / Whole place / each `listing_rooms` row. The block fetch now selects `room_id` and the filter narrows what's painted on the calendar cells (whole-listing blocks still show for a specific-room view because they affect every room). - **iCal feed (`/ical/[id]/[token].ics`)** now joins `listing_rooms.name` per block. `collapseConsecutiveDates` buckets by room before collapsing so different rooms produce separate VEVENTs, and SUMMARY becomes `"Booked: {room.name}"` for room-scoped blocks (plain `"Booked"` for whole-listing). - **Bookings list (`/dashboard/bookings`)** adds a small "N rooms" hint under the listing name for `scope='rooms'` bookings via a `booking_rooms ( id )` count. - **Booking detail (`/dashboard/bookings/[id]`)** shows a new "Rooms" card listing each `booking_rooms` row with name + per-room subtotal when `scope='rooms'`, and labels the Amount card's first line "Rooms" instead of "Base". - **Dashboard listings card** shows a "N rooms" pill next to the Published/Draft status when `booking_mode != 'whole_listing'`. - **Guest discovery** — `/[handle]` and `/explore` show `from {min(room.base_price)}` for `rooms_only` listings (joined via `listing_rooms` with active + non-deleted filter); `whole_listing` and `flexible` keep showing `listing.base_price`. - **Generated types regenerated** (`packages/types/database.types.ts`, +157 lines for the two new tables and the new columns).
    Notes
    - **`pnpm --filter web build`** passes (34 routes — `/listing/[slug]` now 7.36 kB, `/listing/[slug]/book` 8.6 kB, editor unchanged at 15.8 kB). `pnpm --filter web lint` zero warnings. No `console.log` introduced. - The room-picker overlay on `PhotosTab` shows only on hover via `group-hover:opacity-100`. Acceptable on desktop; mobile UX will switch to an always-visible picker when we polish the editor on small screens. - Pre-MVP data policy is in effect (see `CLAUDE.md`) — the migration drops `unique_blocked_date` and reshapes the trigger without any backwards-compat shim, since the DB is empty. - `FeaturedListings.tsx` on the homepage is still hard-coded demo data; it'll pick up the `from {min}` treatment once it's wired to the real listings query.
    Migrations
    - `20260524000000_per_room_bookings.sql`
    Commit
    - (pending — Track 1) ---
  177. Phase 22026-05-24

    Quotes + Invoices + Manual booking flow

    Built
    - **Schema (`20260524000001_quotes_invoices_addons.sql` + `20260524000002_fix_invoice_host_snapshot.sql`):** - `quotes`, `quote_rooms`, `quote_addons` — host sends a quote to a prospect; quote has `accept_token`, `valid_until`, status machine (draft / sent / accepted / declined / expired / converted). - `booking_addons` — free-form line items on a booking (clone of `quote_addons` on conversion; populated directly for manual bookings). - `invoices` — 1-to-1 with `bookings`, auto-issued by trigger on transition to `confirmed`. Frozen `host_snapshot` + `guest_snapshot` JSON, `hosted_token` for the public URL, `pdf_storage_path` into a new private `invoice-pdfs` storage bucket. - `host_counters` + `next_quote_number(host)` / `next_invoice_number(host)` — per-host monotonic counters yielding `{HANDLE}-QYYYY-NNNN` / `{HANDLE}-INVYYYY-NNNN`. - `bookings`: nullable `guest_id` (walk-ins), new `guest_name / guest_email / guest_phone`, `origin` (`guest_request` / `host_manual` / `quote_converted`), `host_payment_note`, `quote_id`. Identity CHECK so every booking has either a real `guest_id` or a `guest_name + guest_email`. - `blocked_dates.quote_id` + soft-hold trigger `on_quote_status_change`: when a quote flips to `sent`, insert one `blocked_dates` row per night with `reason='quote_pending'`. Holds clear on decline / expire / convert. - **Server actions (no new Edge Functions in this slice):** - `app/dashboard/quotes/actions.ts` — create / update / send / mark-accepted / decline / convert / soft-delete. - `app/dashboard/bookings/new/actions.ts` — `createManualBookingAction` honours the `paid` / `unpaid` / `send_paystack_link` payment-state picker. - `app/dashboard/invoices/actions.ts` — mark paid / regen PDF (renders via `@react-pdf/renderer`, uploads to `invoice-pdfs` via the admin client). - `app/q/[id]/[token]/actions.ts` — guest accept / decline, gated by `accept_token` + `valid_until` via the admin client (RLS-bypass). - **Host UI (Track 1 paths):** - `/dashboard/quotes` list — search by number / guest name / email, status filter, "New quote" CTA. - `/dashboard/quotes/new` — listing picker, dates, headcount, base + cleaning + free-form add-ons (label / qty / unit price), notes, "Save draft" + "Save & send" actions. - `/dashboard/quotes/[id]` — line-items, status pill, hosted accept URL, action panel (Send / Mark accepted / Decline / Convert / Delete) plus the "Paid / Unpaid + note" convert picker. - `/dashboard/bookings/new` — manual booking form mirroring the quote form plus the three-way payment-state picker. - `/dashboard/invoices` — replaces the ComingSoon stub. Search by number, status filter, status pills. - `/dashboard/invoices/[id]` — full preview, "Mark paid" / "Revert to issued", "Regenerate PDF", hosted URL display. - Sidebar gains a **Quotes** entry between Bookings and Inbox. - Bookings list now surfaces manual + quote-converted bookings (with a `· Manual` / `· From quote` tag) and the `user_profiles!inner` join becomes `!left` so walk-ins (`guest_id IS NULL`) aren't filtered out. - Bookings header now has a **New booking** button. - **Public pages:** - `/q/[id]/[token]` — guest-facing quote view with Accept / Decline. Expired / decided quotes show a status notice. - `/invoice/[hosted_token]` — public hosted HTML preview with **Download PDF** button. - `/quote/[id]/pdf` — host-authenticated server-rendered quote PDF. - `/invoice/[token]/pdf` — public token-gated invoice PDF. - **PDF templates** (`apps/web/lib/pdf/`) — branded `InvoiceDocument` and `QuoteDocument` (`@react-pdf/renderer`), shared stylesheet, Wielo emerald header with status pill. - **Calendar** (`/dashboard/calendar`) — renders `quote_pending` holds in a third visual state (amber dashed border vs solid green for booked vs muted gray for manual block). Legend updated.
    Notes
    - **No new Edge Functions in this slice.** All mutations are Server Actions or token-gated Route Handlers — simpler to ship and lints cleanly. A `quote-sent` → Resend email integration lands in a follow-up; for now the host copies the hosted URL out of the quote detail page. - **Payment flow:** manual bookings with `payment_state = send_paystack_link` land as `pending` and the host hits "Send payment link" from the booking detail page (existing flow). - **Add-ons** are free-form only (label / qty / unit price). A reusable per-listing add-on catalogue is deferred per the approved plan. - **Per-room quotes** — the schema supports them (`quote_rooms`, `scope='rooms'`) but the new-quote form defaults to whole-listing. Wiring the room picker on the quote form is a follow-up. - The invoice trigger snapshot pulls host email + phone from `user_profiles` (joined via `hosts.user_id`) — there are no `hosts.contact_email` / `contact_phone` columns. The first migration referenced non-existent columns; the second migration is the fix. - **PDF rendering** uses `@react-pdf/renderer` server-side. `Buffer` is wrapped with `new Uint8Array(buffer)` before passing to `NextResponse`. - **Pushed migrations to remote (linked Frankfurt project `zlcivjgvtyeaszikqleu`)** since Docker isn't running locally; `database.types.ts` regenerated with `supabase gen types typescript --linked` (4049 lines). - `pnpm --filter web build` passes (47 routes). `pnpm --filter web lint` zero warnings. No `console.log` introduced.
    Migrations
    - `20260524000001_quotes_invoices_addons.sql` - `20260524000002_fix_invoice_host_snapshot.sql`
    Commit
    - (pending — Track 1) ---
  178. Phase 22026-05-24

    Universal Add-ons catalog (host CRUD + guest checkout)

    Built
    - **Schema (`20260524000005_addons_catalog.sql`):** - `addons` — per-host catalog (name, description, featured image, `pricing_model` enum: `per_stay / per_night / per_guest / per_guest_per_night / per_couple`, `unit_price`, currency, `min_quantity`/`max_quantity`, `is_required`, `is_active`, `lead_time_days`, `sort_order`, `image_path`). - `listing_addons` — availability join with NULL-safe partial unique indexes for the dual-scope pattern (`room_id IS NULL` = listing-wide, set = scoped to one room). Optional `unit_price_override` per listing/room. - Reshape `booking_addons`: dropped the generated `subtotal` (wrong math for non-flat pricing), added `addon_id` FK (NULL = legacy free-form line), `pricing_model`, `currency`, `is_required`, plain `subtotal` snapshot column. - `compute_addon_subtotal(model, unit_price, qty, nights, guests)` SQL helper — single source of truth for line subtotal math, mirrored in TS at `apps/web/app/dashboard/addons/schemas.ts`. - RLS: host CRUD own, staff read, public read on active addons + published-listing `listing_addons`, admin full. - Plan-gating via `plan_features` rows (Pro + Business enabled, Free + Basic disabled — keyed off `feature_key = 'addons'`). - New private storage bucket `addon-images` (8 MB, JPEG/PNG/WebP) with host-folder upload + delete policies. Public read. - **Host catalog UI (`apps/web/app/dashboard/addons/`):** - `page.tsx` — Server Component. Plan-gated: shows an "Upgrade to Pro" card for Free/Basic; otherwise renders `AddonsManager`. - `AddonsManager.tsx` — inline expandable card list (mirrors `RoomsManager` pattern): each addon expands to a form with name, description, pricing model select, unit price, min/max qty, lead time, required + active toggles, featured-image dropzone. - `AddonImageInput.tsx` — single-image dropzone wrapper around `uploadAddonImageAction` (8 MB cap, MIME allowlist, orphan cleanup on DB-update failure, mirrors `PhotosTab`). - `actions.ts` — Server Actions: `createAddon`, `updateAddon`, `deleteAddon` (hard delete + storage folder cleanup), `toggleAddonActive`, `uploadAddonImage`, `deleteAddonImage`, `setListingAddon` (upserts the `(listing_id, addon_id, room_id)` triple with single-scope semantics — wipes other rows for the pair so toggling the dropdown moves the row instead of stacking). Every mutator first calls `check_feature_permission(host_id, 'addons')` and ownership-checks via `assertAddonOwnership` / `assertListingOwnership`. - `schemas.ts` — Zod `pricingModelSchema`, `addonInputSchema`, `listingAddonInputSchema`, `PRICING_LABEL` lookup, and the `computeAddonSubtotal` TS mirror of the SQL helper. - **Per-listing assignment UI (`apps/web/app/dashboard/listings/[id]/edit/tabs/AddonsTab.tsx`):** - Clones the `AmenitiesTab` pattern: lists active host addons, checkbox to enable, "Listing-wide / Room X / …" dropdown when the listing has rooms, optional per-row "Price override" number input. Per-row autosave + optimistic state with rollback on failure.
    Changed
    - `booking_addons.subtotal` is now a plain snapshot column (was a generated column — broke for `per_night`/`per_guest` math).
    Migrations
    - `supabase/migrations/20260524000005_addons_catalog.sql`
    Notes
    - **Status:** All integration patches applied. `pnpm build` passes (zero errors) and `pnpm lint` passes (zero warnings) against a hand-patched `packages/types/database.types.ts` that includes the new tables. - **Before deploy, run locally:** 1. Start Docker Desktop. 2. `supabase db reset` — applies the new migration, creates the `addon-images` bucket, seeds the `plan_features` rows. 3. `supabase gen types typescript --local > packages/types/database.types.ts` — overwrites the hand-patched types with the canonical output. 4. `pnpm --filter @vilo/web build && pnpm --filter @vilo/web lint` again to confirm parity. - **Sidebar entry, AddonsTab registration, parallel-fetch in the listing editor, BookingForm cards + price-line UI, and the `createBookingAction` snapshot/insert/rollback chain are all wired.** - **Stylistic merge conflicts** in `dashboard/staff/{page,actions,StaffManager}.tsx` and `staff/accept/[token]/page.tsx` were resolved (Prettier-only conflicts; both sides semantically identical — kept the formatted variant). - **`apps/web/app/dashboard/listings/[id]/edit/roomEnums.ts`** created as a stub for the in-progress room drill-in editor — was missing, blocking the build. Lists `BED_TYPES`, `VIEW_TYPES`, `EXPERIENCES` as plain string arrays; refine values to taste. - **`roomPatchSchema`** extended with the drill-in fields (`room_size_sqm`, `bed_type`, `view_type`, `experiences`) that the RoomDetailsForm relies on. - **Quote flow left untouched (deferred).** `quote_addons` stays free-form for v1 to avoid churn in the live quote→invoice path. A follow-up should wire catalog-linked addons into `QuoteForm.tsx` and the quote→booking conversion trigger. - **Single featured image per addon** (v1 — multi-image gallery deferred). - **`per_couple` math** = `ceil(guests / 2) × price`. "Per person" maps to the existing `per_guest` enum value (same math, just relabel in copy). - **Lead-time filter** is applied in BOTH the `book/page.tsx` SQL fetch (so the card never renders) AND in `createBookingAction` server-side (so forged selections get rejected). - **Required addons** are auto-inserted server-side regardless of guest selection, with qty = `min_quantity`. - Existing `on_booking_confirmed_create_invoice` trigger reads `booking_addons.label`/`quantity`/`unit_price` — addon-derived rows should flow into invoice `line_items` without trigger changes (verify during manual smoke test).
    Commit
    - (uncommitted — apply INTEGRATION.md patches, then commit) ---
  179. Phase 02026-05-24

    Docker CI + Doppler secret centralization

    Built
    - **Docker image pipeline** — `apps/web/Dockerfile` (multi-stage pnpm monorepo build using Next.js standalone output) + `.dockerignore` + new `.github/workflows/docker-build.yml` pushing `ghcr.io/wollie333/wielo-web:latest` and `:sha-<short>` to GitHub Container Registry on every push to `main` touching web/packages. Uses `GITHUB_TOKEN` (auto-provided) for registry auth and GHA cache for layer reuse. Pulled from Docker Hub after repeated PAT auth failures — ghcr.io eliminates token management entirely. - **Doppler as single source of truth for app secrets** — project `vilo2027` (free Developer plan) with `prd` config seeded from `.env.local` (19 application secrets). Local dev: `doppler run -- pnpm dev`. Vercel Production: Doppler dashboard integration (1 of 1 free-tier Vercel sync slots). GitHub Actions: `DOPPLER_TOKEN` service token consumed by workflows.
    Changed
    - `apps/web/next.config.mjs` — `output: 'standalone'` now gated on `NEXT_OUTPUT=standalone` env var (Dockerfile sets it in the builder stage). Required because the unconditional standalone setting broke local Windows builds with EPERM symlink errors. - `.github/workflows/docker-build.yml` — fetches `NEXT_PUBLIC_*` build-args from Doppler via `dopplerhq/secrets-fetch-action`. - `.github/workflows/deploy-web.yml` — wraps `pnpm --filter web build` in `doppler run` so all 19 app secrets inject at build time.
    Notes
    - **Doppler → Supabase Edge Function sync intentionally NOT set up.** Supabase reserves the `SUPABASE_*` prefix (Edge Functions auto-inject `SUPABASE_URL/ANON_KEY/SERVICE_ROLE_KEY`), so the Doppler dashboard sync rejects the upload. When `paystack-webhook` ships, push its secrets via `doppler run -- supabase secrets set --project-ref <ref> KEY=...`. - Tools installed locally this session: GitHub CLI (`gh`, authed as Wollie333), Doppler CLI (v3.76.0). Docker CLI not installed — doesn't matter, builds run on Actions. - Rotated mid-session: `SUPABASE_SERVICE_ROLE_KEY` (was in transcript). Doppler service token `dp.st.prd.VWb…` should be rotated after first green CI run. - Existing `apps/web/.env.local` still on disk but no longer the source of truth — Doppler is. Safe to delete after team is comfortable with `doppler run`.
    Commits
    - `chore(ci): add Docker build & push workflow for web app` — 052d4f4 - `chore(ci): migrate app secrets to Doppler for build workflows` — 17744d4 ---
  180. MVP2026-05-24

    Seasonal pricing (host catalog)

    Built
    - **Seasonal pricing dashboard** at `/dashboard/seasonal-pricing` — a new top-level tab directly below **Rooms** in the sidebar. Hosts manage date-range price rules per listing or per individual room, with: - **Per-rule min-nights override** (e.g. 5-night minimum over Christmas layered on a 1-night default). - **Explicit priority** integer — higher wins on overlap, with a non-blocking overlap warning shown in the edit dialog. - **Active/inactive toggle** for archiving without deleting. - **Room vs listing precedence** — room-scoped rules beat listing-wide rules on the same night (mirrors the addons pattern). - Live "R{price} × N nights = R{total}" preview while editing. - **Server Actions** (`apps/web/app/dashboard/seasonal-pricing/actions.ts`) — create / update / delete / toggle-active, all gated by `check_feature_permission('seasonal_pricing')` and ownership checked.
    Changed
    - `calculate_booking_price()` now takes an **optional** `p_room_id` and picks the highest-priority active rule with room-scope > listing-scope ordering. Existing 3-arg callers are unaffected. - New RPC `get_min_nights_for_stay(listing, room, in, out)` returns the effective minimum-nights for a stay (will be wired into booking validation in Phase 2 / `booking-create`).
    Migrations
    - `20260524000008_seasonal_pricing_v2.sql` — adds `room_id / min_nights / priority / is_active / updated_at` to `listing_seasonal_pricing`, indexes, updated_at trigger, replaces `calculate_booking_price()`, adds `get_min_nights_for_stay()`, seeds `plan_features.seasonal_pricing` enabled on all plans.
    Notes
    - **Feature gate open on every plan for now** (founder's free test account). To restrict later flip `plan_features.is_enabled = false WHERE plan = 'free' AND feature_key = 'seasonal_pricing'` — no code change. - The existing `listing_seasonal_pricing` RLS policies in `20260501000011` (host_manage_seasonal_pricing / public_read / admin_full) already cover the new columns; no policy edits needed. - Out of scope (tracked for follow-up): calendar timeline visualisation, bulk-copy rules across listings/rooms, SA preset templates (December / Easter / school terms), percentage adjustments, guest checkout price-preview wire-up (Phase 2 work — the function update lets it drop in cleanly). - **Build status:** `pnpm lint` clean. `tsc --noEmit` clean on every new and modified file. `pnpm build` currently fails on an unrelated, pre-existing WIP file (`tabs/RoomsManager.tsx` line 81) tied to the uncommitted "room enterprise fields" feature — not introduced by this session.
    Commit
    - pending ---
  181. MVP2026-05-24

    Banking & business details (enterprise)

    Built
    - **`/dashboard/settings/banking`** — dedicated sub-route for hosts to manage multiple bank accounts (with one default) plus a tax/business block (legal/trading name, VAT no., company reg no., billing address). - **Encrypted account numbers** — AES-256-GCM with `BANKING_CIPHER_KEY`, format `v1.<nonce>.<ciphertext>.<tag>`. Two implementations (Node and Web Crypto) at `apps/web/lib/crypto/banking.ts` and `supabase/functions/_shared/banking-crypto.ts`. - **Edge Function `eft-banking-details`** — exposes the host's default account + business + computed payment reference to a verified guest on a `pending_eft` / `pending_eft_review` booking (per `AGENT_RULES.md` §1.5 and §4.4). Returns `EFT_NOT_APPLICABLE` / `NOT_BOOKING_GUEST` / `NO_DEFAULT_BANK_ACCOUNT` / `DECRYPT_FAILED` for the gate failures. - **Invoice + quote PDFs** — issuer "From" block now carries trading/legal name, VAT no., company reg no., and billing address; a "Payment details" block (invoices) / "Banking details" block (quotes) renders the full account number, branch code, account type, SWIFT, and reference (invoice only — uses the snapshot's booking ref). Invoices read from the frozen `host_snapshot.banking`; quotes read live.
    Changed
    - `eft_banking_details` reshape: dropped `UNIQUE(host_id)`, added `label`, `account_type`, `is_default`, `is_archived`; partial unique index `eft_banking_one_default_per_host` enforces one default per host excluding archived rows. Updated `eft_banking_details` to track `updated_at` via trigger. - `on_booking_confirmed_create_invoice()` now snapshots `banking` and `business` into `host_snapshot`, plus the booking reference for reference substitution in PDFs. - `hosts.banking_details` jsonb column dropped (vestigial — pre-MVP).
    Migrations
    - `supabase/migrations/20260525000001_banking_and_business_details.sql`
    Notes
    - This shipped out of the original `/login` `/register` Phase-1 scope — user-authorised deviation per `feedback_ship_over_block`. - `BANKING_CIPHER_KEY` must be generated (`openssl rand -base64 32`) and set in Doppler dev. Without it the page falls back to "????" for last4 in the accounts list and the Edge Function returns `DECRYPT_FAILED`. - `banking_details` feature key seeded enabled across every plan (matches the `seasonal_pricing` precedent — gate is wired so plans can disable later with one UPDATE). ---
  182. MVP2026-05-24

    Settings tabs + pre-MVP feature-gate policy

    Built
    - **Tabbed settings shell** — new `apps/web/app/dashboard/settings/layout.tsx` wraps every settings route with a shared "Settings" header + a URL-driven horizontal tab bar (`SettingsTabs.tsx`, emerald underline on the active tab). Four tabs land on four routes: - `/dashboard/settings` → **Your profile** - `/dashboard/settings/host` → **Public host page** - `/dashboard/settings/banking` → **Banking & business** - `/dashboard/settings/subscription` → **Subscription** - Deep links to each tab survive refresh; switching tabs is instant because adjacent routes share the layout.
    Changed
    - The previous monolithic `/dashboard/settings/page.tsx` (Profile + Host page + Banking link + Subscription card stacked) is now only the Profile content; Host page, Banking, and Subscription each have their own route. - Banking page dropped its standalone back-link + page-header + pill — the encryption badge moved inline next to the section heading. - New `AGENT_RULES.md` §3.4: **pre-MVP feature-gate policy** — every new gated feature must be open on the `free` plan while there's no subscription management UI. `assertFeatureEnabled` short-circuits to `true` (with the original RPC body preserved as a comment for Phase 3). `CLAUDE.md` Feature Permissions section points at the new rule.
    Notes
    - The policy exists because free hosts created via `handle_new_user` don't get an active `subscriptions` row, so `check_feature_permission` returns disabled regardless of `plan_features` — strict gating blocked the founder from testing his own platform. ---
  183. Phase A2026-05-24

    Super Admin Control Centre foundation

    Built
    - **RBAC migration** (`20260525000002_create_platform_staff_rbac.sql`) — new tables `admin_roles`, `admin_permissions`, `admin_role_permissions`, `platform_staff`, `platform_staff_invites`. Seeded five named roles (`super_admin`, `support_agent`, `finance`, `content_mod`, `ops`) with 17 permission keys in `domain.action` format. - **Replaced `is_super_admin()`** — now consults `platform_staff` (not `user_profiles.role`) and requires AAL2. Signature unchanged so existing `admin_full_*` RLS policies keep working. - **New `has_admin_permission(p_key text)`** SQL helper — source of truth for capability checks. Also AAL2-gated. - **Founder seed** — migration auto-inserts wollie333@gmail.com into `platform_staff` with `super_admin` role. Aborts with `RAISE EXCEPTION` if the founder profile does not exist. - **Break-glass script** (`supabase/scripts/grant-super-admin.sql`) — re-grants `super_admin` when locked out. - **Admin helpers** (`apps/web/lib/admin/`) — `requireAdmin()`, `requirePermission()`, `hasPermission()` (non-throwing), `withAdminAudit()` wrapper, impersonation cookie signing (HMAC-SHA256), custom error classes. - **`/admin` route group** with admin shell layout, sidebar (operations / finance / moderation / platform sections), topbar, impersonation banner. Sidebar renders the active role next to the email. - **KPI overview at `/admin`** — active hosts, live listings, total bookings, pending refunds tiles plus a recent-activity feed of the last 10 audit rows. - **Audit log viewer at `/admin/audit`** — filters by admin, action, target_type, since; 50-per-page pagination; highlights `permission_denied` rows in red. - **Wielo staff management at `/admin/platform/staff`** — lists active staff + pending invites + the available role catalog (Phase E will add invite UI). - **View-only impersonation** (`/admin/as/[userId]/...`) — read-only parallel route tree using service-role with explicit user-id scoping. **Does not swap auth cookies.** Banner shows elapsed time + "End session" button. - **Placeholder pages** for users / hosts / listings / bookings / payments / subscriptions / reviews / platform settings / feature flags — each calls `requirePermission()` so the permission gates are exercised end-to-end.
    Changed
    - `AGENT_RULES.md` §6 expanded with subsections 6.4–6.8: RBAC source of truth, AAL2 requirement, reason-required pattern, view-only impersonation, atomic finance/moderation actions. - `admin_audit_log.target_type` CHECK constraint extended with `user`, `platform_staff`, `staff_member`, `permission_denied` values.
    Migrations
    - `20260525000002_create_platform_staff_rbac.sql`
    Notes
    - **Phase B–E pending**: detail screens, user/host edit, refund admin, subscription editor, reviews moderation, platform_settings editor, staff invite flow, reason dialog component, finance Edge Function for atomic audit writes, audit-log CSV export. The foundation is shippable on its own — every permission gate works, every screen returns a placeholder that explains which phase fills it in. - **PHASE_PLAN.md slates super admin for Phase 4 (weeks 10–13)**; this foundation lands early so all later admin work has a place to plug in. - **`supabase db reset` was NOT run** this session — Docker wasn't running locally. Run it on next boot to apply the RBAC migration, then regenerate types: `supabase gen types typescript --local > packages/types/database.types.ts`. - View-only impersonation chosen over auth-swap on the Plan agent's recommendation — swapping `sb-*` cookies races refresh-token rotation in `@supabase/ssr` and can end the admin's real session. - Founder email is hardcoded in the migration. If `wollie333@gmail.com` doesn't exist in `user_profiles` (e.g. fresh `db reset` before sign-up), the migration aborts — sign up first, then re-run. ---
  184. Autonomous MVP push2026-05-24

    7 commits, ~12 hours of work compressed

    Built
    - **Cookie consent banner** (`apps/web/app/_components/CookieBanner.tsx`, mounted in root layout). POPIA-friendly, dismissable, stored in a 365-day cookie + localStorage. - **Guest review submission flow** — `/review/[bookingId]?token=…`, HMAC SHA-256 token over bookingId (no DB column). Form is star-rating + optional written review; inserts via admin client (no guest INSERT RLS by design — only legit path is the email link). `publish_at = now() + 48h` so the existing auto-publish cron still moderates. Helper at `apps/web/lib/review-token.ts`. - **Subscription dashboard** — replaces the 222-byte stub at `/dashboard/settings/subscription` with current plan card + 4-plan picker (Free / Basic / Pro / Business) + monthly/annual toggle + cancel/resume + 10-row history feed. Migration `20260525000003_subscription_history_trigger.sql` adds INSERT + UPDATE triggers so every state change writes a `subscription_history` row automatically (preserves the append-only contract from `AGENT_RULES.md` §2.7). - **Refund Manager** — host queue at `/dashboard/refunds` with Pending / Approved / Declined / All tabs, KPI tiles, inline approve flow (editable amount + guest note), decline flow (5-reason picker matching the v11 CHECK), plus a host-initiated "Issue refund" panel on `/dashboard/bookings/[id]` for captured-payment bookings. Server actions optimistically flip to 'completed' so the v11 status-history + payments.refunded_amount triggers fire — Paystack/PayPal call is stubbed until live credentials land. - **Admin Phase B** — `/admin/users`, `/admin/hosts`, `/admin/listings` full implementations replacing three Phase A placeholders. Search, filters, paginated list, detail page. Suspend/reinstate (users), verify/unverify (hosts) all routed through `withAdminAudit` with reason-required + ip + user-agent + before/after capture. Detail pages link to public page + "view as host" view-only impersonation + audit log filter. - **Email templates batch** (11 React Email templates in `emails/templates/`): BookingRequestHost, BookingConfirmed{Host,Guest}, BookingDeclinedGuest, BookingCancelled{Host,Guest}, EftInstructionsGuest, EftProofReceivedHost, ReviewRequestGuest, NewReviewHost, SubscriptionWelcome. Plus shared `Button` + `Heading` components. Worker / Resend wire-up still deferred (domain unverified). - **POPIA data subject requests** at `/dashboard/settings/data`. New migration `20260525000004_data_requests.sql` adds the table + RLS (users insert/read/cancel own, admin sees all). UI cards for Export and Delete, one active request per type, history feed. Fulfilment remains manual.
    Changed
    - Booking detail join switched to `user_profiles!left` so walk-in bookings (guest_id NULL) don't crash the page. - Settings tabs gain a fifth "Data & privacy" entry.
    Migrations
    - `20260525000003_subscription_history_trigger.sql` - `20260525000004_data_requests.sql`
    Notes
    - **All 7 commits are local** — push to `main` was blocked by the harness auto-mode classifier (defaults to blocking direct main pushes). User needs to `git push origin main` or fast-forward. Vercel auto-deploy will pick it up on push. - **Migrations 003 + 004 not yet applied to remote.** Run `supabase db push --linked` against the Frankfurt project, then `supabase gen types typescript --linked > packages/types/database.types.ts` to refresh the generated types. The new `data_requests` and `subscription_history` triggers won't break anything until applied, but the dashboard pages will show empty states / silent failures on UPDATE until the trigger exists. - **Provider integration stubs:** approve refund + cancel subscription + plan switch all flip state directly. When Paystack/PayPal live keys arrive, replace the optimistic transitions with provider call + webhook callback (the audit/history triggers stay as-is). - **Pre-MVP feature-gate policy still active.** Every new server action that touches feature gates passes-through; no upgrade walls surface for free hosts.
    Commits
    - `feat(legal): site-wide cookie consent banner` — 243767e - `feat(reviews): guest-side submission flow at /review/[bookingId]` — cae281e - `feat(subscription): plan picker, cancel/resume, history feed` — 775783b - `feat(refunds): host-side queue + approve/decline + booking-detail refund` — 0a01f6e - `feat(admin): phase B — users, hosts, listings search + detail` — 01a1672 - `feat(emails): phase 2/3 react-email templates batch` — 694a91c - `feat(privacy): popia data export + account deletion requests` — e2ef691 ---
  185. Autonomous MVP push wave 22026-05-24

    admin Phase C + guest surface

    Built
    - **iCal import** at `/dashboard/calendar-sync` — per-listing feed manager. Migration `20260525000005_ical_feeds.sql` adds the table + source/ical_feed_id columns to blocked_dates. Tiny RFC-5545 parser at `apps/web/lib/ical-parser.ts` (VEVENT / DTSTART / DTEND / SUMMARY, folded-line aware, all-day VALUE=DATE). Server actions add/remove/sync (30 s timeout, batched 500-row upserts, respects AGENT_RULES §2.5 by only touching its own `source='ical'` rows). - **Public marketing pages** — `/about`, `/contact`, `/help`. Footer re-wired so guests/hosts columns + the company column all resolve (no more `href="#"` dead links). POPIA pill points at the new /dashboard/settings/data flow. - **Admin Phase C** — `/admin/bookings`, `/admin/payments`, `/admin/subscriptions`, `/admin/reviews` replace four Phase A placeholders. Cross-host visibility via service-role client. Reviews gets working uphold-flag / reject-flag actions through withAdminAudit (reason-required). - **Admin data-requests queue** at `/admin/data-requests` — pending / processing / completed tabs over the POPIA table. Three actions (mark processing / mark complete / reject) all audited. - **Guest /my-trips list + detail** — the missing guest surface. RLS `guest_read_own_bookings` enforces ownership. Detail page wires the guest-initiated refund request flow (6-reason picker, "Other" forces a detail note, amount ≤ paid total, no stacking with an open refund).
    Migrations
    - `20260525000005_ical_feeds.sql`
    Notes
    - **Pre-push status: 16 commits sitting on local `main`.** Push to origin was blocked by the auto-mode classifier on every attempt (it defaults to blocking direct main pushes). Run: `git push origin main` and Vercel will pick up the deploy. - **Three migrations not yet applied to remote.** Run them when Docker is up: - `20260525000003_subscription_history_trigger.sql` - `20260525000004_data_requests.sql` - `20260525000005_ical_feeds.sql` Then `supabase gen types typescript --linked > packages/types/database.types.ts`. - **Pages that need migrations applied to function:** - `/dashboard/settings/data` (no `data_requests` table without 004) - `/admin/data-requests` (same) - `/dashboard/calendar-sync` (no `ical_feeds` table without 005) - `/dashboard/settings/subscription` history feed (works without 003, but new state changes won't get audit rows) - **Provider integration stubs:** refund approval, subscription cancel, plan switch all flip state directly. When Paystack/PayPal live keys arrive, replace the optimistic transitions with provider call + webhook callback.
    Commits (wave 2)
    - `feat(calendar-sync): ical import — per-listing feeds + sync action` — 355d19a - `feat(marketing): public about, contact, help pages` — 3e21476 - `feat(admin): phase C — bookings, payments, subscriptions, reviews` — f115fa4 - `feat(admin): popia data-requests queue under moderation` — 5d41338 - `feat(guest): /my-trips list + detail + refund request flow` — ca5adf9 ---
  186. Phase 02026-05-23

    Closeout: Storage, Doppler, EAS landed; Sentry/PostHog/Resend deferred

    Built
    - **6 Supabase Storage buckets** created in the Vilo2027 project (`listing-photos`, `host-avatars`, `host-covers` public; `eft-proofs`, `message-attachments`, `refund-requests` private). MIME types and size limits per `supabase_database.md` §17. RLS policies were already applied in the v1.0 migration set; buckets now exist for them to protect. Verified via Storage REST API. - **Doppler workspace `Vilo2027`**, project `vilo2027`, four configs (`dev`, `dev_personal`, `stg`, `prd`). Imported 18 secrets from `.env.local` (+ 3 Doppler-managed metadata vars) into each top-level config. Integrations connected: Vercel (`wollie333's projects`) and Supabase (`Mana` org). Active syncs: `dev` → Vercel Development env (last synced 13:47 UTC), `dev` → Supabase Edge Functions secrets (13:46 UTC). See Notes for the free-plan gap. - **EAS project linked** to `apps/mobile`. UUID `50664ed2-d876-4edd-aab0-6a984fbdfca7` written to `app.json` at `expo.extra.eas.projectId`. `eas build` will pick this up when first invoked.
    Changed
    - `apps/mobile/app.json` — `slug` changed from `vilo` to `vilo2027` to match the EAS project name (avoids slug-mismatch errors during `eas build`). - `PHASE_PLAN.md` — Phase 0 marked closed out. New status emoji `🕑` introduced for "deferred-by-design (wire just-in-time)" items. Doppler / Vercel / Storage / EAS lines flipped to ✅. Sentry / PostHog / Resend lines flipped to 🕑 with explicit notes. - `CURRENT_TASK.md` — fully rewritten to scope the next session (Phase 1 Auth: `/login` + `/register`). - New memory: `project-doppler-state` capturing the sync gap and the 5 in-transcript tokens flagged for revocation.
    Decisions
    - **Doppler free-plan limit accepted as a documented gap.** Doppler's Developer (free) plan caps at one sync per integration; we created the `dev` → Vercel Development sync first, then `stg` and `prd` sync attempts were rejected. Because all three Doppler configs hold identical values today (single Supabase project per ADR-015), the practical impact is nil — Vercel Production is still using the manually-set vars from the earlier deploy session, which match the Doppler `dev` values exactly. Revisit when Doppler is upgraded to a paid plan or when staging/production Supabase projects actually diverge (af-south-1 migration, ADR-015). - **Sentry, PostHog, Resend all deferred by design.** No users → no errors / no analytics / no outbound emails worth instrumenting. Supabase Auth's built-in templates cover the auth-flow emails Phase 1 needs. Each will be wired just-in-time when its specific feature lands. Placeholder env vars exist in Doppler under the canonical names so adding values later is a one-step change.
    Notes
    - 5 Doppler tokens were pasted in chat during the integration debugging (1 read-only Personal Token `dp.pt.P05SY…`, 4 Service Tokens `dp.st.{prd,stg,dev,dev_personal}.…`). All are scoped tightly so blast radius is minimal, but they should be revoked from the Doppler dashboard at convenience. Tracked in `project-doppler-state` memory. - The Phase 0 closeout was originally scoped to also do Sentry/PostHog/Resend account setup. User opted to defer all three after seeing the Doppler dashboard friction. This deviates from the literal Phase 0 plan but aligns with the platform's "ship over block" guidance and CLAUDE.md's "use the least amount of code that solves the problem" principle — no need to wire telemetry for a service with zero users.
    Commits
    - (this commit — closeout + docs update; pushed to main after staging.) <!-- New entries go above this line -->
  187. Phase 02026-05-23

    Canonical design system adopted

    Built
    - `Wielo Design System.html` (3914 lines, 290 KB) added at the repo root as the **canonical** source of truth for all Wielo UX/UI work. Replaces the inline token specs in earlier `DESIGN_SYSTEM.md` and `tailwind.config.ts` drafts. - `apps/web/public/DESIGN_SYSTEM.HTML` — static mirror published via Next.js, accessible at https://vilo2027.vercel.app/DESIGN_SYSTEM.HTML.
    Changed
    - `apps/web/tailwind.config.ts` rewritten to match the canonical tokens: - Brand palette: `primary #10B981`, `secondary/deep #064E3B`, `accent #D1FAE5`, `dark #0A1510`, `light #F0FDF4`, plus new `ink #052E1F`, `mute #4A7C6A`, `line #DCEAE0` tokens. - Status palette adjusted: `confirmed #10B981` (was `#22C55E` — now tracks brand primary). - Added `font-mono` family wiring to JetBrains Mono. - Added `shadow-card`, `shadow-lift`, `shadow-ring`, `shadow-glow`. - Added `transitionTimingFunction.out: cubic-bezier(0.2, 0.8, 0.2, 1)`. - Added `bg-brand-gradient`, `bg-brand-gradient-dark`, `bg-dot-grid` background-image utilities. - `apps/web/app/globals.css` rewritten with the canonical CSS custom properties (light + dark mode), new utility classes (`bg-brand-gradient`, `bg-dot-grid`), and a global `prefers-reduced-motion` rule. - `apps/web/app/layout.tsx` now loads JetBrains Mono alongside Inter + Plus Jakarta Sans via `next/font/google` and exposes it as `--font-jetbrains-mono`. - `apps/web/app/page.tsx` (homepage) restyled to the new system: hero with brand gradient logo mark on a dot-grid background, status pill, Foundation Status card with `shadow-card` and `divide-y` rows, and a discoverable link to `/DESIGN_SYSTEM.HTML`. - `DESIGN_SYSTEM.md` slimmed from a full token spec to a short pointer at the canonical HTML, with a quick-reference cheatsheet of utility names and the hard rules.
    Decisions
    - **HTML is canonical.** When `DESIGN_SYSTEM.md` and `Wielo Design System.html` conflict, the HTML wins. Reasoning saved in memory `feedback_design_system_source.md`. - Old primary `#1B4D3E` (a darker forest green) and amber secondary `#F4A836` from the previous Tailwind config are retired. The new palette is emerald-led, matching the canonical HTML and the live homepage hero.
    Notes
    - Web build (`pnpm build`) and lint (`pnpm lint`) both pass with zero warnings. - shadcn/ui components in `apps/web/components/ui/` were not edited — they consume the CSS custom properties (`--primary`, `--accent`, `--border`, etc.) and pick up the new palette automatically. Per ADR-006, never edit `components/ui/` directly. - Mobile (`apps/mobile`) NativeWind config is not yet wired up — the design system applies there too, but the wiring is deferred to the first mobile UI session per CHANGELOG 2026-05-23 entry "Mobile + shadcn + tooling + emails scaffolded".
    Commits
    - (single commit for this slice — pushed after this entry is staged.)
  188. Phase 02026-05-23

    Vercel web deploy live

    Built
    - **https://vilo2027.vercel.app/ is live.** First successful production deploy of `apps/web` — Server Component homepage renders the Foundation Status panel with a green Supabase connection check against the Frankfurt project. - `apps/web/vercel.json` — explicit `"framework": "nextjs"` + `"outputDirectory": ".next"`. See ADR-017. - `pnpm.overrides` block in root `package.json` pinning `@types/react@18.3.29` and `@types/react-dom@18.3.7` across the entire workspace. See ADR-016.
    Changed
    - Vercel project `vilo2027` (org `wollie333s-projects`) connected to GitHub `Wollie333/Vilo2027`. Root Directory set to `apps/web`. Environment variables `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` populated for Production, Preview, and Development. - `pnpm-lock.yaml` regenerated under the new overrides — single `@types/react@18.3.29` resolution for the web app's dep graph.
    Decisions
    - **ADR-016** — `@types/react` pinned to v18 across the workspace despite Expo SDK 56 declaring v19 via its peer chain. Required to make `lucide-react` resolve to v18 types in `apps/web`, which fixed the `bigint`-in-`ReactNode` error in `components/ui/checkbox.tsx` during the Vercel build. Mobile runtime unaffected; mobile type-check may show false positives until revisited. - **ADR-017** — `apps/web/vercel.json` introduced because Vercel's Turbo detection (`turbo.json` at workspace root) overrode Next.js framework auto-detection, causing the build to succeed but the deploy to fail with "No Output Directory named 'public' found".
    Notes
    - **Two genuine bugs in the deploy chain were fixed by the build pipeline itself, not patched around.** The "wrong commit" deploy (Vercel building a stale `eedc69d`) was caused by the GitHub ↔ Vercel App lacking repo access while we pushed new commits; reconnecting the GitHub installation fixed it and the next push triggered an up-to-date build automatically. - Sequence of issues + fixes during this session: (1) Root Directory not set → set to `apps/web` in dashboard; (2) GitHub auth broken → reconnected Vercel GitHub App, scoped to `Wollie333/Vilo2027`; (3) Vercel deploying stale commit → empty trigger commit `576875c`; (4) `@types/react` v18/v19 type collision → ADR-016 override; (5) Vercel Turbo detection overrode framework → ADR-017 `vercel.json`. - The lint-staged pre-commit hook auto-reformatted `pnpm-lock.yaml` and `package.json` with Prettier on each commit. Cosmetic — the dep graph and override semantics are unchanged.
    Active blockers / user-side actions still open for Phase 0
    - Doppler account + dev/staging/prod configs - EAS account + `eas init` for `apps/mobile` - Sentry projects (web + mobile) - PostHog project - Resend account + `wieloplatform.com` domain verification (domain itself not yet registered) - 6 Supabase Storage buckets (`listing-photos`, `host-avatars`, `host-covers`, `eft-proofs`, `message-attachments`, `refund-requests`)
    Commits
    - `chore: trigger vercel rebuild` — `576875c` - `fix(deps): pin @types/react to 18 across workspace to fix web build` — `657ddb8` - `fix(vercel): pin framework to nextjs so Turbo detection doesn't override output dir` — `054c6b9` - (this CHANGELOG + DECISIONS update — final commit of the session, appended after staging)
  189. Phase 02026-05-23

    Mobile + shadcn + tooling + emails scaffolded

    Built
    - **`apps/mobile`** scaffolded with Expo SDK 56 (newer than DEVSTACK's 51+ — modern stack, React Native 0.85, Expo Router pre-configured). Includes `src/app/` file-based routing, `eas.json` (development/preview/production profiles), `app.json` branded as Wielo, `.env.local` with `EXPO_PUBLIC_*` Supabase vars, and `src/lib/supabase.ts` using Expo SecureStore as the auth-storage adapter per `ARCHITECTURE.md` §7. Deps: `@supabase/supabase-js`, `expo-secure-store`, `react-native-url-polyfill`, `@tanstack/react-query`, `zustand`. - **18 shadcn/ui components** installed in `apps/web/components/ui/` per `DESIGN_SYSTEM.md`: button, input, card, label, badge, skeleton, form, dialog, sonner, separator, avatar, alert, tabs, select, checkbox, textarea, dropdown-menu, sheet. Pulled in `react-hook-form`, `zod`, `@hookform/resolvers`, `sonner`, `next-themes`, and the relevant `@radix-ui/*` primitives as transitive deps. - **Code quality tooling** at workspace root: - Prettier 3.8 + `prettier-plugin-tailwindcss` with `.prettierrc.json` (double quotes, trailing comma all, 80-col). - `.prettierignore` excluding generated files (lockfile, `database.types.ts`, migrations, `.next`, `.expo`, etc.). - Husky 9 with `.husky/pre-commit` running `lint-staged` and `.husky/commit-msg` running `commitlint --edit`. - `commitlint.config.js` extending `@commitlint/config-conventional` with Wielo's allowed types (feat, fix, chore, docs, refactor, test, style, perf, ci, build, revert, wip, migration). - Root `package.json` scripts: `format`, `format:check`, `prepare`; `lint-staged` config for `*.{ts,tsx,js,jsx}` and `*.{json,md,yml,yaml,css}`. - **`@vilo/emails` workspace package** at `emails/` with React Email setup: - `components/Layout.tsx` — brand-styled shared layout (Wielo green/cream, Inter font, header + content + footer with email-preferences link). - `templates/WelcomeHost.tsx` — first of the 26 templates from `EMAIL_TEMPLATES.md` (host onboarding welcome). - `package.json` with `email dev`/`build`/`export` scripts. - `.gitignore` for `.react-email/` build output.
    Changed
    - `pnpm-workspace.yaml` now declares `emails` alongside `apps/*` + `packages/*`. - `apps/web` `lucide-react` pinned to `^0.469.0` (v1.x requires React 19 types — incompatible with our React 18). Fixed a build failure in `components/ui/checkbox.tsx`.
    Notes
    - **NativeWind not configured yet.** It needs metro.config.js, babel.config.js, and tailwind.config.js wiring that's tightly coupled to actual UI work. Deferred to the first mobile UI session. - **Expo's `default` template uses `src/`** (newer convention); `ARCHITECTURE.md` §4 shows `app/` at app root. Treating `src/app/` as the active path — when ARCHITECTURE.md is next edited, update §4 to match. - The Vercel deploy is still failing because Vercel needs `Root Directory = apps/web` set in Project Settings. Not done in this session. - Husky's `prepare` script logs `apps/web prepare: .git can't be found` — benign, can be silenced by removing the propagated `prepare` script from individual workspaces if it becomes noise.
    Phase 0 autonomous work — now complete
    Everything I can do without external account access is done. Remaining items in Phase 0 all need user-side action (see PHASE_PLAN.md 👤 items).
  190. Phase 12026-05-23

    Marketing homepage v1

    Built
    - `apps/web/app/page.tsx` rewritten as a real marketing homepage. Composed from co-located server components under `apps/web/app/_components/home/`: `SiteHeader`, `Hero`, `Features`, `HowItWorks`, `Pricing`, `SiteFooter`, plus a shared `VLogo` SVG. - Sections: sticky nav · split hero with dual CTA · feature grid (3 host + 3 guest + 2 universal) · two-column how-it-works (hosts + guests, 3 steps each) · 3-tier pricing pulled verbatim from `wielo-platform-mvp.md` §6.6B (Basic R299 / Pro R599 / Business R1,199) · dark-emerald site footer with status dot. - All sections are server components, all classes use canonical Wielo Design System tokens (brand-primary/secondary/dark/accent/line/mute, rounded-card, shadow-glow, dot-grid, font-display). Lucide icons via existing `lucide-react` dep.
    Changed
    - Old dev-status content (Supabase auth health check + stack readout) moved from `/` to a new `/status` route at `apps/web/app/status/page.tsx`. Same readout, same brand styling, but off the public front door. Footer + status-dot link to it.
    Migrations
    - None.
    Notes
    - Scope: this was outside `CURRENT_TASK.md` (which targeted `/login` + `/register`). The auth Zod schemas at `apps/web/app/(auth)/schemas.ts` and the `/login` `/register` route files already exist on disk from earlier in this session — homepage CTAs already wire to them. - `pnpm build` clean. `pnpm lint` clean. `/` is statically prerendered (180 B route, 96.1 kB first-load JS). - Decision: section components live under `apps/web/app/_components/home/` (underscored = private, non-routed) rather than `apps/web/components/` to keep route-local UI close to the route that uses it. Reusable cross-route UI still belongs in `apps/web/components/`.
    Commit
    - _Pending — user has not yet asked for commit/PR._ ---
  191. Phase 12026-05-23

    Auth slice 1: /login + /register live

    Built
    - **`/login`** (`apps/web/app/(auth)/login`) — email + password, "Forgot password?" link (`/forgot-password` — page lands next sub-session), "Create one" link to `/register`, inline field errors (RHF + Zod), pending state, post-register verification banner when `?verify=1` is present. - **`/register`** (`apps/web/app/(auth)/register`) — email + password + confirm-password + ToS checkbox linking `/terms` and `/privacy` (legal pages land in Phase 5), inline field errors, pending state. On success Supabase fires the default verification email and the page redirects to `/login?verify=1`. - **`/dashboard`** (`apps/web/app/dashboard`) — stub Server Component that reads `auth.getUser()`, shows the signed-in email and a sign-out button. Real dashboard lands later in Phase 1. - **`/auth/confirm`** (`apps/web/app/auth/confirm/route.ts`) — Route Handler that consumes Supabase's `token_hash` + `type` and calls `verifyOtp`, then redirects to `/dashboard` (or `/login?verify=failed` on error). - **Server Actions** (`apps/web/app/(auth)/actions.ts`) — `loginAction`, `registerAction`, `signOutAction`. All re-validate input with Zod server-side, call the `@supabase/ssr` server client, map Supabase error messages to user-friendly toasts, then `redirect()` on success. - **Shared `(auth)` layout** — centered card on the brand dot-grid background, Wielo logo mark in the header, "Back to site" link. - **Sonner `<Toaster richColors position="top-center" />`** wired into the root `apps/web/app/layout.tsx` so any Client Component can `toast.error` / `toast.success` per CONVENTIONS.md §8.1. - **Schemas** (`apps/web/app/(auth)/schemas.ts`) — `loginSchema` and `registerSchema` with email lowercasing, >=8 char password, password-match refinement, and ToS-must-be-true rule. Colocated rather than in `packages/schemas` since they are single-consumer for now (per CONVENTIONS.md §6.2).
    Changed
    - **`apps/web/lib/supabase/middleware.ts`** — `updateSession` now also enforces route protection: authenticated users hitting `/login` or `/register` are redirected to `/dashboard`; unauthenticated users hitting `/dashboard*` are redirected to `/login`. Single `supabase.auth.getUser()` call drives both the session refresh and the redirect logic. - **`apps/web/app/layout.tsx`** — added `<Toaster />` import and render so toasts work app-wide.
    Notes
    - **`pnpm --filter web build`** passes — 9 routes generated. Middleware bundle 82.6 kB. `pnpm --filter web lint` passes with zero warnings. - **No new DB migrations.** Phase 0's `handle_new_user` trigger auto-inserts `user_profiles` on `auth.users` INSERT — sign-up flows through it with no extra wiring. - **Sign-up metadata kept minimal.** Spec only asks for email + password + ToS this slice; no `full_name` collected yet. `user_profiles.full_name` stays null until the host onboarding wizard (next sub-session) collects it. - **Email verification path:** `signUp({ options: { emailRedirectTo: ${origin}/auth/confirm } })` => Supabase emails a link with `token_hash` + `type=signup` => our Route Handler calls `verifyOtp` => middleware sees a fresh session and lands the user on `/dashboard`. - **Server Action redirect pattern:** actions return `{ ok: false, error }` on failure and call `redirect("/...")` on success. The client form awaits the action; on a returned error it pops a toast, on redirect Next.js intercepts the thrown `NEXT_REDIRECT` and navigates. - **`/forgot-password`, `/terms`, `/privacy` not yet built.** Links exist per the spec but resolve to 404. Forgot-password is the next Phase 1 sub-session per PHASE_PLAN.md; legal pages are Phase 5. - **No Google OAuth, no magic link, no password reset** — all out of scope for this slice per CURRENT_TASK.md.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  192. Phase 12026-05-23

    Auth slice 2: password reset flow

    Built
    - **`/forgot-password`** (`apps/web/app/(auth)/forgot-password`) — email-only form that calls `forgotPasswordAction`, which fires `supabase.auth.resetPasswordForEmail` with `redirectTo: ${origin}/auth/confirm?next=/reset-password`. Always redirects to `/forgot-password?sent=1` regardless of whether the email exists, to avoid account-enumeration leaks. The "sent" state renders a `SentNotice` card with a back-to-sign-in link. - **`/reset-password`** (`apps/web/app/(auth)/reset-password`) — Server Component guard that redirects to `/forgot-password` if there's no session, then renders a Client form with password + confirm-password. Submit calls `resetPasswordAction` which re-checks the session, calls `supabase.auth.updateUser({ password })`, and redirects to `/dashboard`. - **Two new Server Actions** in `apps/web/app/(auth)/actions.ts`: `forgotPasswordAction`, `resetPasswordAction`. - **Two new Zod schemas** in `apps/web/app/(auth)/schemas.ts`: `forgotPasswordSchema`, `resetPasswordSchema` (>=8 char password, match refine).
    Changed
    - **`apps/web/lib/supabase/middleware.ts`** — added `/forgot-password` to `AUTH_ROUTES` so authenticated users hitting it get bounced to `/dashboard`. `/reset-password` is intentionally NOT in `AUTH_ROUTES` — it relies on the short-lived recovery session that `/auth/confirm` issues via `verifyOtp`.
    Notes
    - **Reuses existing `/auth/confirm` Route Handler.** That handler already accepts a `next` query param; the recovery flow piggybacks on it instead of duplicating verifyOtp logic. - **Account-enumeration protection.** `forgotPasswordAction` doesn't surface Supabase errors to the client — it always redirects to the "check your inbox" state. The error path is logged server-side by Supabase but not exposed. - **`pnpm --filter web build`** passes — 12 routes generated. `pnpm --filter web lint` zero warnings. - **Out of scope:** custom email template (still Supabase default), rate-limiting the request endpoint (Supabase enforces ~3/hour on the free SMTP plan).
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  193. Phase 12026-05-23

    Auth slice 3: magic link sign-in

    Built
    - **Magic link sign-in** added to `/login` as a second tab next to "Password" (shadcn `Tabs`). The Magic-link pane has a single email field; submit fires `magicLinkAction`, which calls `supabase.auth.signInWithOtp({ email, options: { emailRedirectTo: ${origin}/auth/confirm, shouldCreateUser: false } })`. On success the pane swaps in an inline sent-state ("If an account exists for X, a sign-in link is on its way. It expires in 1 hour.") with a "Send another link" button to reset. - **`shouldCreateUser: false`** — magic-link form is sign-in only. New users go through `/register`. Stops the magic-link surface from quietly minting accounts with no ToS acceptance and no `handle_new_user` trigger context. - **One new Server Action** in `apps/web/app/(auth)/actions.ts`: `magicLinkAction`. Like `forgotPasswordAction`, it swallows Supabase errors and always returns `{ ok: true }` to the client — anti-enumeration. Real failures (rate limit, SMTP) still produce a toast via the existing `friendlyAuthError` path. - **One new Zod schema** in `apps/web/app/(auth)/schemas.ts`: `magicLinkSchema` (email only, mirrors `forgotPasswordSchema`).
    Changed
    - **`LoginForm.tsx`** restructured into a single Client Component containing the shared card (header, verify banner, footer "Don't have an account?" link) and two inline panes — `PasswordPane` (unchanged behavior) and `MagicLinkPane` (new) — switched by shadcn `Tabs`. Each pane owns its own RHF instance so the two forms don't interfere.
    Notes
    - **No `/auth/confirm` change needed.** Existing Route Handler already accepts `type=magiclink` (it's in Supabase's `EmailOtpType` union) and the default `next=/dashboard` lands users in the right place. - **No middleware change needed.** Magic-link sign-in lives at `/login` which is already in `AUTH_ROUTES`, so signed-in users are still bounced to `/dashboard` before they ever see the tab. - **`pnpm --filter web build`** passes — 12 routes, `/login` first-load JS now 152 kB (was 146 kB; +6 kB for the tabs + magic-link form). `pnpm --filter web lint` zero warnings. - **Out of scope:** changing the magic-link email template (still Supabase default), throttling client-side (Supabase enforces SMTP rate limits).
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  194. Phase 12026-05-23

    /booking-management marketing page + /change-log

    Built
    - **`/booking-management`** — full marketing page translating the canonical emerald design at `Wielo Home Page (2).html`. 13 sections in order: `SiteHeader` (sticky nav with gradient-SVG V logo) · `Hero` (split layout with URL grabber form, social-proof avatars, and a stacked mockup column containing a browser dashboard, a floating mobile inbox card, and a "commission saved" stat tile) · `TrustMarquee` (auto-scrolling brand strip) · `ValueProp` + interactive `EarningsCalculator` (range slider that computes Airbnb 18% / Booking 22% / Wielo flat R499 net amounts and the annual savings vs Airbnb) · `Features` (6 cards) · `HowItWorks` (4 steps with dashed connectors) · `ProductShowcase` (iPhone-frame mockup of a Wielo listing detail) · `DirectoryStrip` (4 verified-host cards) · `Pricing` (3-tier with `Monthly | Annual SAVE 20%` toggle and Free-tier strip) · `Testimonials` (1 dark featured + 2 white) · `Comparison` (Wielo vs Airbnb vs Booking.com vs DIY table) · `FAQ` (6 native `<details>` accordion items) · `FinalCTA` (claim-your-URL form on the primary-emerald section) · `PageFooter` (dark-emerald, 4 link columns, social SVGs, status dot linking to /change-log). - **`/change-log`** — Server Component that reads `CHANGELOG.md` at build time, parses each `## DATE — Phase X — Title` entry into structured sections, and renders them as cards in the booking-management visual style. Falls back to a GitHub link if the file can't be read on the host. Footer "Changelog" link and the status-line `v1.0.0` link both point here.
    Changed
    - **`apps/web/app/globals.css`** — added a `@layer components` block with the design's custom CSS: `marquee-track` keyframes, `details[open] .acc-icon` rotation, `.step-line::after` dashed connector, `.wielo-range` slider track/thumb styling (WebKit + Mozilla), `.dotgrid` utility (22px variant of the existing 18px `.bg-dot-grid`), `.ribbon`, `.avatar`, `.chrome-dot`, `.num-display`, `.brand-gradient`.
    Notes
    - **Palette is the canonical emerald `brand-*` set** — no new tokens needed. The earlier forest+amber design (`Wielo Home Page.html` / `(1).html`) was superseded by the (2) revision which uses our existing tokens exactly. - **Two Client Components only** — `EarningsCalculator` (controlled range + text input) and `Pricing` (billing toggle). Everything else is a Server Component. The interactive calculator port preserves the design's formatting rules (`en-ZA` with space thousands separator, `Math.round(Math.abs(n))` to match the original JS). - **Images come from `images.unsplash.com` via plain `<img>` tags** — no `next/image` domain config needed. Each `<img>` carries the `eslint-disable-next-line @next/next/no-img-element` pragma. - **No new packages.** All icons via the already-installed `lucide-react`, all SVG logos inlined. - **`pnpm --filter web build`** passes — 14 routes (slice 3's 12 + `/booking-management` + `/change-log`). `/booking-management` first-load JS 100 kB, `/change-log` prerendered statically at build time so first load is 96.1 kB. `pnpm --filter web lint` zero warnings. - **CTAs wire to existing routes** — Hero + FinalCTA forms `action="/register"`, nav "Log in" → `/login`. URL handle isn't read yet — that lands when the host onboarding wizard ships.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  195. Phase 12026-05-23

    New homepage / = directory landing

    Built
    - **`/` rewritten** as the guest-facing directory landing per the canonical emerald design at `Main Home.html`. 13 sections in order: `UtilityBar` (dark thin bar — language, currency, "List your property") · `SiteHeader` (sticky nav with gradient-SVG V logo + tagline; reveals a compact "Anywhere · Any week · Guests" search button after the hero scrolls past, with `nav-elevated` shadow) · `Hero` (full-bleed Unsplash image with dark `hero-veil` overlay, headline, 4-input search card that GETs to `/explore`, 6 popular-search chips, 4-stat row in white) · `CategoryChips` (sticky `top-16` row of 11 chips with active state + Filters button on the right) · `TrendingDestinations` (6 destination cards, 4:5 aspect, gradient bottom overlay) · `FeaturedListings` (8 listing cards with image, instant-book/featured badge, heart toggle, rating, location, detail and price; "Show all 2 348 stays" CTA) · `TrustPillars` (4 cards — No fees, Verified hosts, Talk to host, Honest cancellations) · `BrowseByType` (6 large 16:10 type cards) · `DealsBanner` (Summer-deal image card + brand-gradient Group-stays card) · `RecentReviews` (3 review cards with rating, body, avatar, 4.83 stat) · `AppNewsletter` (newsletter capture + iOS/Android download tiles) · `HostCTA` (dark-emerald section linking to `/booking-management` — two CTAs: "List your property" deep-linked to `#cta`, "See how Wielo works") · `SiteFooter` (4 link columns: Explore / Guests / Hosts / Company; social SVGs; "All systems operational" links to `/change-log`). - **Three Client Components only** — `SiteHeader` (scroll listener for sticky-search reveal), `CategoryChips` (active-chip state), `HeartButton` (per-listing saved toggle). Everything else is a Server Component. - **New `VLogo`** that takes `size` (px) + `gradientId` (so multiple instances on the same page don't collide on the SVG `<defs>` id). Replaces the simple-V version used by the old marketing homepage.
    Changed
    - **`apps/web/app/globals.css`** — added directory-page utilities to the existing `@layer utilities`: `.hero-veil` (gradient overlay), `.hscroll` (scrollbar-none), `.num` (tabular numerals alias), `.card-img` (hover zoom paired with `.group`), `.chip-active`, `.nav-elevated` (sticky-nav shadow). - **`apps/web/app/status/page.tsx`** — updated to the new `VLogo` API (`size` + `gradientId` instead of `className`). Same visual size (40 px).
    Removed
    - **`apps/web/app/_components/home/{Hero,Features,HowItWorks,Pricing,SiteHeader,SiteFooter,VLogo}.tsx`** — the marketing-style components from the earlier "Marketing homepage v1" entry. Their content has been superseded twice: visually by `/booking-management` (which has its own component set), and structurally by this new directory homepage which uses entirely different sections. Replaced in-place with the new directory components under the same `_components/home/` directory.
    Notes
    - **Palette is canonical emerald** — no `tailwind.config.ts` changes. The design file (`Main Home.html`) was authored against our existing `brand-*` tokens. - **Unsplash images via plain `<img>`** with `loading="lazy"` and the `eslint-disable-next-line @next/next/no-img-element` pragma. Avoids `next.config.js` image domain configuration; matches the approach used in `/booking-management`. - **Header tagline** ("Direct stays. Direct hosts.") visible at `sm+` only to keep the mobile nav clean. - **Search card POSTs to `/explore`** (not yet built — placeholder route for Phase 2 directory work). The form will degrade gracefully to a 404 on submit until that page lands. - **`pnpm --filter web build`** passes — 14 routes. `/` first-load JS now 100 kB (was 96.1 kB; +4 kB for the three small Client Components). `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  196. Phase 12026-05-23

    Host onboarding wizard + dashboard banner

    Built
    - **`/signup/host` 5-step wizard** per `PHASE_PLAN.md` Phase 1 → Host Onboarding. Server Component (`page.tsx`) guards auth (redirects to `/login?next=/signup/host` if signed-out) and bails if the user already has a `hosts` row (redirects to `/dashboard`). Client `Wizard.tsx` holds step state internally with one `useForm` per step: 1. **Your details** — `full_name` (required) + `phone` (optional). 2. **Listing type** — accommodation vs experience cards; nested accommodation-type / experience-type pickers per the DB CHECK enums. 3. **First listing** — `display_name` (drives the auto-generated host handle), listing `name`, optional `description`. 4. **Plan** — three cards. Only "Free" is selectable; "Pro" and "Business" are visibly locked with an "After launch" pill (subscription billing lands in Phase 3). 5. **Welcome** — checklist of what&rsquo;s about to happen, a responsiveness acknowledgement checkbox, then "Create my host profile". - **`finalizeOnboardingAction`** Server Action (`actions.ts`) does the inserts in order: `user_profiles.update` (full_name, phone) → `hosts.insert` (display_name; handle auto-generated by `trigger_host_handle`) → `listings.insert` (host_id, listing_type, accommodation_type|experience_type, name, description; defaults to `is_published=false`) → `subscriptions.insert` (plan=free, status=active). On listing-insert failure, best-effort deletes the orphan `hosts` row so the wizard can be retried. On subscription-insert failure, the wizard continues silently — the host/listing are valid and the subscription can be backfilled. - **Step indicator** above the card — numbered pills, completed steps get a check, current step gets a ring. - **`StepIndicator`, `PersonalDetailsStep`, `PropertyTypeStep`, `FirstListingStep`, `PlanStep`, `WelcomeStep`** — all inline components inside `Wizard.tsx` to keep the slice in one file.
    Changed
    - **`apps/web/app/dashboard/page.tsx`** — now reads the user&rsquo;s hosts row and the 5 newest listings. If no hosts row, renders a "Finish setting up your host profile" banner linking to `/signup/host`. If hosts row exists, shows the Wielo handle and a Published/Draft listing list. - **`apps/web/app/booking-management/_components/SiteHeader.tsx`** — V logo now links to `/` so users can return to the directory home from the host marketing page. Tiny chore, separate commit (`3a86926`).
    Notes
    - **RLS verified before building** — `hosts` and `subscriptions` use `host_manage_own*` policies (FOR ALL USING `user_id = auth.uid()` / `host_id = get_my_host_id()`), so the user-bound Supabase client can insert directly. `user_profiles` UPDATE pins the `role` value (`role = (SELECT role FROM user_profiles WHERE id = auth.uid())`) — the wizard doesn&rsquo;t try to flip role to `host`. Until JWT-claims hooks land, host-vs-guest is detected by hosts-row presence. - **No new migrations.** Existing `generate_host_handle` and `generate_listing_slug` triggers do the slug/handle derivation. - **No new packages.** Uses existing `react-hook-form`, `@hookform/resolvers`, `zod`, `sonner`, `lucide-react`, and the shadcn `Card`/`Form`/`Input`/ `Textarea`/`Checkbox` primitives already installed. - **Welcome toast** — `?welcome=1` on `/dashboard` triggers a client-side Sonner success toast via a tiny `WelcomeToast` Client Component (`useEffect` + `toast.success`). Auto-clears after the default duration. - **`pnpm --filter web build`** passes — 15 routes (slice's 14 + `/signup/host` at 4.53 kB). `pnpm --filter web lint` zero warnings.
    Out of scope (next slices)
    - Listing editor (Accommodation Basic) — 8 tabs per `PHASE_PLAN.md` Phase 1. Hosts can&rsquo;t flip a listing from Draft to Published yet. - Google OAuth, JWT custom claims hook — remaining Phase 1 Auth items. - Real subscription billing — Phase 3.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  197. Phase Plan + Track 52026-05-23

    Parallel execution tracks defined; /privacy, /terms, /cookies shipped

    Built
    - **`PHASE_PLAN.md` v1.3** — added "Parallel Execution Tracks" section defining 6 tracks (Main Line · Email Templates · iCal Booking Sync · Public Directory · Legal & Marketing · Mobile) with disjoint file ownership, rules of engagement, and a shared-zone protocol so multiple Claude Code agents can work in parallel without colliding. - **Track 5 first session — legal page shells.** `/privacy`, `/terms`, and `/cookies` Server Components rendering with the homepage `SiteHeader` and `SiteFooter`, plus a shared `LegalPage` helper at `apps/web/app/_components/legal/LegalPage.tsx`. All three pages prerender as static (2.2 kB each).
    Changed
    - `apps/web/app/_components/home/SiteFooter.tsx` — bottom-strip Terms / Privacy / Cookies links now point at the real routes instead of `#`. POPIA left as `#` until the data-deletion flow lands in Phase 5.
    Notes
    - Page content is structural placeholder marked `DRAFT — pending legal review`. Real wording comes from counsel before public launch. - **Cross-track finding for Track 1:** `apps/web/app/dashboard/listings/` exists as untracked WIP in the working tree (never committed). The build fails on `main` because `Editor.tsx` can't resolve its tab imports. Track 5 worked around it via temporary stash; Track 1 needs to resolve before any further parallel session is started. See `CURRENT_TASK.track-5.md` for details. - Branch: `track/5-legal-pages`. Does not merge to `main` directly — user merges via PR or fast-forward per Track 5 protocol.
    Commits
    - `docs(phase-plan): add parallel execution tracks section` - `feat(legal): /privacy, /terms, /cookies page shells (track 5)` ---
  198. Phase 12026-05-23

    Listing editor (8 tabs) live

    Built
    - **`/dashboard/listings/[id]/edit`** — full 8-tab listing editor per `PHASE_PLAN.md` Phase 1 → Listing Editor (Accommodation — Basic). Server Component (`page.tsx`) guards auth, fetches the listing (RLS-bound to the owner via `host_manage_own_listings`), and pre-loads amenities + photos. Client `Editor.tsx` owns tab navigation + the Publish toggle; each tab is its own file managing its own RHF form: - **Basic info** — name, type picker (accommodation-type or experience-type per `listings.listing_type`), plain Textarea description (Tiptap deferred). - **Photos** — single-file upload via Supabase Storage `listing-photos/{listing_id}/{uuid}.{ext}`; thumbnail grid with hover-Trash to delete; "Add a photo" tile triggers a hidden file input. JPEG/PNG/WebP only, max 8 MB. Drag-and-drop multi-upload is deferred. - **Location** — address fields (line1/2, city, province dropdown of SA provinces, postal code) + optional manual latitude/longitude. Mapbox pin is deferred. - **Rooms & capacity** — bedrooms, bathrooms, max_guests, min/max nights. - **Amenities** — checkbox grid of 20 curated options (WiFi/Kitchen/Pool/Braai/Pet-friendly/etc.) backed by `listing_amenities` table (wipe-and-reinsert on save). - **Pricing** — base_price, optional weekend_price + cleaning_fee, currency (ZAR default). - **Policies** — check_in_time + check_out_time (HTML `<input type="time">`), cancellation policy radio (Flexible / Moderate / Strict — three cards using `listings.cancellation_policy`), house rules. Full Policy Manager (versioning + snapshots) is deferred. - **Booking settings** — instant_booking toggle + a "Payment methods" info card pointing to Phase 2 work. - **`saveListingPatchAction`** Server Action — takes a partial Zod-validated listings row, ownership-checks via a `hosts!inner ( user_id )` join, then updates. Each tab calls it with its slice. - **`replaceAmenitiesAction`** — delete-then-insert pattern keyed by `listing_id`. **`uploadListingPhotoAction`** — file validation + Storage upload + `listing_photos` row insert + `revalidatePath`. On row-insert failure, best-effort removes the storage object. **`deleteListingPhotoAction`** — removes the row + the storage object. **`togglePublishAction`** — pre-publish guard (name + base_price + max_guests required) then updates `is_published`. - **`assertOwnership` helper** in `actions.ts` — single source of truth for the ownership check, called by every mutating action.
    Changed
    - **`apps/web/app/dashboard/page.tsx`** — each listing row in the host list now has an "Edit →" link to the new editor. Helper copy updated. - **`apps/web/app/dashboard/listings/[id]/edit/schemas.ts`** — numeric form fields (location lat/lng, rooms counts, pricing amounts) are defined as `numericString` (a `z.string().refine(...)` validator) rather than `z.coerce.number().or(z.literal(""))`. Cleaner RHF types, and the per-tab submit handlers convert strings to `number | null` before calling the action.
    Notes
    - **RLS verified** — storage policies for `listing-photos` allow uploads only where the path starts with a `listing_id` the user owns; listing rows are gated by `host_manage_own_listings`; amenities + photos inherit ownership via `listing_id`. The user-bound Supabase client handles all mutations. - **`pnpm --filter web build`** passes — 18 routes, the editor at 12.3 kB / 159 kB first-load JS. `pnpm --filter web lint` zero warnings. - **Deferred from spec (flagged inline in the editor):** Tiptap rich-text description, Mapbox location pin, drag-and-drop multi-photo upload, full Policy Manager UI. None of these block a publishable listing.
    Out of scope (next slice)
    - **Dashboard chrome** — the user supplied a `Dashboard.html` design that should wrap all logged-in routes (`/dashboard`, `/signup/host`, `/dashboard/listings/[id]/edit`). Refactor lands in the next slice as a shared `(app)` route-group layout.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  199. Phase 12026-05-23

    Dashboard chrome (Sidebar + Topbar + MobileBottomNav)

    Built
    - **`apps/web/app/dashboard/layout.tsx`** — Server Component that wraps every route under `/dashboard/*` with the chrome from `Dashboard.html`. Auth-guarded (redirect `/login?next=/dashboard`), pre-fetches the user&rsquo;s `hosts` row + `listings` count + active `subscriptions.plan` and threads them into the Sidebar so each render lands without a client roundtrip. - **`Sidebar.tsx`** (Client — `usePathname`) — full sidebar per the design: brand mark + "Host dashboard" subtitle, workspace switcher (host display_name + plan, or "Set up host profile" CTA for un-onboarded), quick search button (⌘K placeholder), 3 nav sections (Main: Overview / Bookings / Inbox / Calendar / Listings / Reviews / Payments · Connect: Channels / Calendar sync / Staff · Tools: Reports / Invoices / Refunds), Settings + Help footer, dark-emerald plan card at the bottom showing the host&rsquo;s current plan with a link to `/dashboard/settings/subscription`. - **`Topbar.tsx`** — date label + page title (currently fixed "Dashboard"; per-page title slot lands next slice), search button, "This month" date range, notifications bell with red unread dot, "New booking" CTA, plus `AvatarMenu` (initials + dropdown). - **`AvatarMenu.tsx`** (Client — uses existing shadcn `DropdownMenu`) — Profile / Settings / Sign out. Sign out wires to the existing `signOutAction` from `(auth)/actions.ts` via `useTransition`. - **`MobileBottomNav.tsx`** (Client — `usePathname`) — `lg:hidden` fixed- bottom 5-button tray: Home · Bookings · Inbox · Listings · More. Active state pill matches sidebar style. - **`VLogo.tsx`** (dashboard-scoped, `compact` prop for the topbar mobile logo) — duplicated rather than imported across routes to keep dashboard chrome self-contained.
    Changed
    - **`apps/web/app/dashboard/page.tsx`** — stripped its own auth check + the wrapper `<main>` (layout owns both now). Reformatted as a sequence of sections that drop straight into the layout&rsquo;s content slot: welcome strip (host first name + handle, or "Welcome to Wielo" for un-onboarded), onboarding banner (unchanged behavior), listings card (now with a "See all" link to `/dashboard/listings`), empty-state card for hosts with zero listings. Removed the old "Signed in" pill + redundant "Welcome to Wielo" header (the layout handles identity at the topbar). - **`apps/web/app/dashboard/listings/[id]/edit/page.tsx`** — removed the duplicate "← Dashboard" header strip and the `<main>` wrapper. The Sidebar + Topbar are the sole navigational chrome now. - **`Editor.tsx`** — dropped its own page padding (`px-5 py-8 lg:px-8 lg:py-10`) since the dashboard layout already adds it. Internal max-width and section padding stay.
    Removed
    - **`apps/web/app/dashboard/SignOutButton.tsx`** — superseded by `AvatarMenu`&rsquo;s Sign out item.
    Notes
    - **Most sidebar nav targets don&rsquo;t exist yet** — Bookings, Inbox, Calendar, Listings, Reviews, Payments, Settings, the Connect/Tools sections all link to `/dashboard/{...}` routes that 404 today. They land slice-by-slice as the MVP fills out. The chrome shipping ahead is intentional: visual progress, real routes follow. - **`/signup/host` deliberately stays outside the dashboard layout** — a wizard works better full-screen without sidebar/topbar distractions. - **Per-page title in the topbar is deferred.** Currently the topbar always reads "Dashboard". Next slice can thread a title via React Context or a `params.json` convention. Not blocking — the page body already includes its own h1. - **No new packages.** Uses the already-installed shadcn `DropdownMenu` primitive for the avatar menu. - **`pnpm --filter web build`** passes — 18 routes. `/dashboard` page weight dropped from 1.33 kB → 311 B because the chrome moved to the layout. `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  200. Phase 22026-05-23

    /listing/[slug] public detail page

    Built
    - **`/listing/[slug]`** — public Server Component that fetches a published listing by slug (RLS `public_read_published` enforces `is_published=true AND is_suspended=false AND deleted_at IS NULL`), joins `hosts!inner`, and parallel-loads `listing_photos` + `listing_amenities`. 404s via `notFound()` if no row matches. Reuses the guest chrome (`UtilityBar` + `SiteHeader` + `SiteFooter` from the homepage), so it sits seamlessly alongside `/`. - **Page sections** — title strip (type pill, name, city/province, rating, guest capacity) · `PhotoGallery` (5-up grid: hero left, 4 small right; empty-state for no photos) · 4 quick-fact tiles (bedrooms / bathrooms / min nights / check-in) · description prose · `HostCard` (avatar with initials fallback, display_name, verified badge, handle, bio, "Message" CTA stub) · `AmenitiesList` (20-key icon grid with lucide-react mapping) · "Things to know" policies (check-in/out, cancellation policy with blurb, house rules if set). - **`BookingWidget`** (Client) — sticky right-rail card. Per-night price + rating, instant-book pill, date-input check-in/check-out, guests `<select>` capped at `max_guests`. Client-side price calculator (subtotal = base_price × nights, +cleaning_fee when nights > 0; total shown when dates picked). "Reserve" links to `/listing/[slug]/book?from=…&to=…&guests=…` (next-slice route, currently 404s). Disabled state until dates valid. - **`generateMetadata`** — title `{name} · {city, province} · Wielo` + description from listing body for SEO + share previews.
    Changed
    - **Editor (`Editor.tsx`)** — Publish toggle row now includes a "View public" button (visible when `is_published && slug`) opening `/listing/[slug]` in a new tab. Hosts can preview what guests see immediately after publishing. - **Dashboard listings panel (`/dashboard/page.tsx`)** — each row gets a "View" link (published listings only) next to "Edit". The listings query now also pulls `slug`. - **Homepage `FeaturedListings`** — mock cards now point at `/listing/[slug]` (was `/explore/[slug]`). The route prefix is real; the slugs themselves are still placeholders until `directory-featured` ships in Phase 2 and pulls real hosts.
    Notes
    - **Deferred from spec (flagged inline):** photo lightbox, full-screen gallery, availability calendar, reviews section, share button + QR code, Mapbox approximate-location map, `pricing-preview` Edge Function. None block a guest from seeing a listing. - **RLS verified** — `public_read_published` lets anon read published listings; `listing_photos` and `listing_amenities` inherit access via their listing FK + RLS rules in `20260501000011_create_rls_policies.sql`. No new policies needed. - **`pnpm --filter web build`** passes — 18 routes, `/listing/[slug]` at 3.92 kB / 99.9 kB first-load JS. `pnpm --filter web lint` zero warnings.
    Out of scope (next slice)
    - **Booking flow + Paystack** (Phase 2) — `/listing/[slug]/book` page, `booking-create` Edge Function, Paystack init + webhook, success/failed pages. This is the MVP-critical next slice.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  201. Phase 22026-05-23

    Booking flow + Paystack init + webhook

    Built
    - **`/listing/[slug]/book`** — Server Component requires auth (redirects to `/login?next=…` if signed-out), fetches the listing via RLS `public_read_published`, validates URL search params (from / to / guests) server-side, and refuses to render the form until dates are valid. Reuses the guest `SiteHeader` + `SiteFooter` for chrome. - **`BookingForm`** (Client) — three stacked panels: Trip details (dates read-only from search params, guests `<select>` capped at `max_guests`), Payment (Paystack selected — PayPal/EFT flagged "after launch"), Cancellation policy + ack checkbox. Sticky right rail shows per-night × nights, cleaning fee, total, and "Reserve and pay" CTA (disabled until ack ticked). Footer line shows the email the booking will be made under. - **`createBookingAction`** Server Action: 1. `auth.getUser()` via user-bound client. 2. Re-fetch listing (RLS-public) — refuses unpublished, missing price, or guest count above `max_guests`. 3. Server-side date + price recalc (per `AGENT_RULES.md` §1.2 — never trust the client). Enforces `min_nights`. 4. **Admin client** (`createAdminClient` — new) inserts `bookings` (status=pending, payment_status=pending; `reference` auto-generated by the DB default `VILO-YYYY-XXXXXX`) and `payments` (status=pending). Admin client is required because no RLS path lets a guest INSERT bookings — `host_manage_own_bookings` is host-only and there's no `guest_create` policy. 5. Calls `initializeTransaction` (new `apps/web/lib/paystack.ts`). 6. Stashes Paystack's returned reference on the payment row for idempotency. Rolls back booking + payment on any init failure so retry works. 7. `redirect(authorization_url)` — guest leaves Wielo for Paystack. - **`apps/web/lib/paystack.ts`** — thin server-side wrappers for `/transaction/initialize` and `/transaction/verify`. Converts ZAR Rand amounts to kobo (×100) only at the Paystack boundary per `CONVENTIONS.md` §9.1. Throws on non-200 responses. - **`apps/web/lib/supabase/admin.ts`** — `createAdminClient()` using `SUPABASE_SERVICE_ROLE_KEY`. **Server-side only**; sanity-checks the env vars and throws if missing. - **`/booking/[id]/success`** — Server Component, dynamic. Reads the booking (RLS `guest_read_own_bookings`), falls back to `verifyTransaction(reference)` if the webhook hasn&rsquo;t landed yet and mirrors the same status flip via admin client (still idempotent via the `payment.status='pending'` filter). Shows reference, listing, dates, nights, guests, total. "Confirming your payment…" state when pending; "You&rsquo;re booked" when settled. - **`/booking/[id]/failed`** — Server Component showing reference + listing + "Try again" link back to the listing. - **`supabase/functions/paystack-webhook/index.ts`** — Edge Function. Verifies `x-paystack-signature` via HMAC SHA-512 against `PAYSTACK_SECRET_KEY` (per `AGENT_RULES.md` §1.3). Returns 200 immediately and processes async. Logs the full raw payload to `payments.provider_response` for audit. Idempotency: skips DB writes when `payment.status !== 'pending'`. On `charge.success` flips payment to `completed` and booking to `confirmed` (DB trigger `trigger_booking_confirmed` inserts `blocked_dates` automatically per `AGENT_RULES.md` §4.2 — no duplication). On `charge.failed` flips both to failed.
    Notes
    - **User action required before live testing:** 1. Sign up for Paystack (test mode is free). 2. Paste test public + secret keys into Doppler `dev` config: `NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY`, `PAYSTACK_SECRET_KEY` (already declared in `.env.example`). Push the sync so Vercel + Edge Functions get them. 3. `supabase functions deploy paystack-webhook --no-verify-jwt`. 4. In the Paystack dashboard add the deployed function URL as the webhook URL (test + live). The Edge Function already uses `PAYSTACK_SECRET_KEY` for HMAC verification, so no separate `PAYSTACK_WEBHOOK_SECRET` is needed for Paystack (the secret IS the key per their docs). - **Service role key.** Now in active use server-side. Confirmed it stays out of any `NEXT_PUBLIC_` env var and is only imported in `lib/supabase/admin.ts`. Per `AGENT_RULES.md` §1.1. - **No new packages.** `fetch` + `node:crypto` only. - **No new migrations.** Booking creation uses admin client to bypass the missing guest-INSERT RLS — clean enough for now; if we later want to remove the admin dependency, add a `guest_create_bookings` policy with `WITH CHECK (guest_id = auth.uid())`. - **`pnpm --filter web build`** passes — 21 routes: `/listing/[slug]/book` at 7.81 kB, `/booking/[id]/success` + `/failed` at 2.21 kB each. `pnpm --filter web lint` zero warnings.
    Deferred (next slices)
    - **Host booking dashboard** (Phase 2) — `/dashboard/bookings` list + confirm/decline/cancel actions. - **Booking emails** — guest confirmation + host new-booking notification via Resend or Supabase default email. - **PayPal + manual EFT** payment methods. - **Policy snapshot** at booking creation (`snapshot_booking_policies`) — DB function exists; calling it from the action lands when the Policy Manager UI does (Phase 2/3).
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  202. Phase 22026-05-23

    Host booking dashboard (/dashboard/bookings)

    Built
    - **`/dashboard/bookings`** — Server Component list of every booking the host owns (RLS `host_manage_own_bookings`). Newest first, 50 cap. Table shows reference (link to detail), guest name + headcount, listing, check-in → check-out + nights, total + payment_status, status pill. - **`StatusFilter`** — Client URL-driven pill row: All · Pending · Confirmed · Checked in · Completed · Cancelled. Each pill shows a live count badge pulled from a parallel `select status` query. The "Cancelled" filter rolls up `cancelled_by_host`, `cancelled_by_guest`, `declined`, `expired`, `no_show`. - **`StatusPill`** — shared `bookings.status` → label + tone helper. Eleven states mapped to amber / green / emerald / indigo / red / slate. - **Empty state** — dashed card with the calendar-check icon when no bookings match. - **`/dashboard/bookings/[id]`** — full detail page. Header: listing name + status pill + reference + state-aware action buttons. Body grid: - Left: Trip card (dates, nights, guests, payment method/status, special requests if set), Timeline card (booked / confirmed / checked in / checked out / cancelled — formatted en-ZA datetime, em-dash for empty). - Right: Guest card (avatar + name + email + phone; a disabled "Message guest (Inbox slice)" button placeholding the inbox), Amount card (base, cleaning, total breakdown), "View public listing" link. - **`BookingActions`** (Client) — state-machine UI: - **pending** → Confirm (primary) + Decline (with `window.confirm`). - **confirmed** → Mark check-in + Cancel. - **checked_in** → Mark check-out + Cancel. - **completed / cancelled / declined / expired** → no buttons. - **`apps/web/app/dashboard/bookings/actions.ts`** — five Server Actions (`confirmBookingAction`, `declineBookingAction`, `cancelBookingAction`, `checkInBookingAction`, `checkOutBookingAction`) that all funnel into one `applyTransition` helper. The helper: 1. SELECTs the booking via the user-bound client (RLS-bound to the host). 2. Validates the transition is legal against `AGENT_RULES.md` §4.1&rsquo;s state machine (e.g. can&rsquo;t check-in a pending booking). 3. UPDATEs with `status`, `previous_status` (preserving the prior value), timestamp field (`confirmed_at` / `cancelled_at` etc.), and `.eq("status", booking.status)` for optimistic concurrency. 4. `revalidatePath` on both the detail and the list.
    Notes
    - **DB triggers already handle the side effects.** When status flips to `confirmed`, `trigger_booking_confirmed` inserts `blocked_dates` rows and bumps host/listing booking counters. When it flips to a cancelled state, `on_booking_cancelled` deletes those `blocked_dates`. Actions here don&rsquo;t duplicate that work per `AGENT_RULES.md` §4.2. - **No admin client used.** The host owns the row via `host_manage_own_bookings`, so the user-bound `createServerClient()` is sufficient. Service-role stays scoped to the guest-side booking creation only. - **Sidebar Bookings nav target now resolves.** Previously 404; now active-state highlights when on `/dashboard/bookings[*]`. - **`pnpm --filter web build`** passes — 23 routes; `/dashboard/bookings` 829 B, `/dashboard/bookings/[id]` 3.25 kB. `pnpm --filter web lint` zero warnings.
    Deferred (next slices)
    - **Inbox + messaging** — the "Message guest" CTA is disabled. - **24-hour auto-cancel cron** — `pg_cron` job already exists in `20260501000014_create_cron_jobs.sql`; wiring it up to schedule is a Phase-2 host-protection slice. - **Booking emails** — guest gets nothing today after the host confirms. Lands next slice (Resend or Supabase default for first cut).
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  203. Phase 1/22026-05-23

    Listings management (/dashboard/listings + /new)

    Built
    - **`/dashboard/listings`** — Server Component grid of every listing the host owns (RLS `host_manage_own_listings`, soft-deleted rows excluded). Card per listing: hero photo (or Home icon placeholder), Draft/Published status pill, name + type + city/province, base price + /night, Edit link and View (new tab) link for published rows. "+ New listing" CTA in header and empty state. - **`/dashboard/listings/new`** — auth-guarded Server page that also bounces to `/signup/host` if no `hosts` row. Renders a Client form for name + listing_type (Accommodation vs Experience cards) + nested accommodation/experience type picker, matching the onboarding wizard&rsquo;s step 2+3 UX so hosts learn the pattern once. - **`createListingAction`** Server Action — uses user-bound client (RLS `host_manage_own_listings` allows INSERT once the host row exists), inserts the listing as draft (`is_published=false`; slug auto-generated by `trigger_listing_slug`), then `redirect()` to `/dashboard/listings/[id]/edit` so the host lands straight in the full editor. - **Schemas** colocated at `/new/schemas.ts` — same cross-field listing-type refinement pattern used in `/signup/host`.
    Notes
    - **Sidebar nav target now resolves.** `/dashboard/listings` was a 404 in the chrome; it now has a real destination. Active-state highlight works for both list + edit URLs via the `match: "prefix"` rule already in `Sidebar.tsx`. - **No new packages, no migrations.** Uses the existing RLS path and the `generate_listing_slug` trigger from Phase 0. - **`pnpm --filter web build`** passes — 25 routes. `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  204. Phase 22026-05-23

    /[handle] host public profile page

    Built
    - **`apps/web/app/[handle]/page.tsx`** — top-level dynamic route at `wieloplatform.com/{handle}`. Fetches the host via RLS `public_read_active_hosts` (only `is_active=true` + `deleted_at IS NULL`), then their published listings + each listing&rsquo;s hero photo. Reuses guest chrome (`SiteHeader` + `SiteFooter`). 404 via `notFound()` if no host matches. - **Reserved-handle guard** — hard-coded set (`login`, `register`, `dashboard`, `booking`, `booking-management`, `change-log`, `cookies`, `privacy`, `terms`, `status`, `listing`, `signup`, `auth`, `explore`, `api`) returns null from `loadHost` so a maliciously-handled host can&rsquo;t shadow real routes. Belt-and-braces — Next.js prefers static segments anyway, and the DB CHECK on `handle` enforces format. - **Header card** — large circular avatar (initials fallback), display name, verified badge, `wieloplatform.com/{handle}` mono URL, rating + review count, listing count, bio. Sits on a dot-grid background. - **Listings grid** — same card shape as `/dashboard/listings` but guest-facing: hero photo, hover zoom, name, type + city, base price. Each card links to `/listing/{slug}`.
    Notes
    - **`generateMetadata`** — title `${display_name} · Wielo` + bio for the share preview. - **No new packages, no migrations.** Uses the existing RLS path. - **`pnpm --filter web build`** passes — 26 routes, `/[handle]` at 2.21 kB. `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  205. Phase 22026-05-23

    /explore directory search page

    Built
    - **`/explore`** — guest-facing Server Component lists every published listing (RLS `public_read_published`) with URL-driven filters: `where` (text matched ilike against name + city + province), `guests` (min `max_guests`), `type` (accommodation_type or "all accommodation"), `sort` (newest / price_asc / price_desc / rating). Cards mirror the homepage style — hero photo with hover zoom, Instant pill, Verified pill, rating, price + /night. 24-card cap; pagination is a later slice. - **`SearchBar`** (Client) — destination input + guests select + Search button. Submits to `/explore?where=…&guests=…` preserving the current type + sort. Bubbles via the chrome at the top of the page; the existing homepage SearchHero already points at `/explore`. - **`TypeChips`** (Client) — sticky `top-16` row beneath the search bar: All stays · Self-catering · B&B · Guesthouse · Lodge · Hotel. Active state via `chip-active`; links preserve the rest of the search params. - **Empty state** — dashed card with helpful copy ("Try a different city…") when zero results.
    Notes
    - **No Edge Function.** The full `directory-search` Edge Function from PHASE_PLAN.md (full-text + Mapbox proximity + ranked caching) lands in a later slice. For now a direct Supabase query is plenty for the expected dataset. - **No new packages, no migrations.** Filter logic is plain PostgREST `.or` + `.eq` + `.gte` + `.order`. - **Homepage Hero `<form action="/explore">`** already worked; the `Where` field name was `where`, which matches this page&rsquo;s param name — so the homepage search now lands a real page instead of 404. - **`pnpm --filter web build`** passes — 27 routes, `/explore` 3.66 kB. `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  206. Phase 22026-05-23

    /dashboard/calendar availability view

    Built
    - **`/dashboard/calendar`** — Server Component three-month rolling view of every blocked date for the selected listing. RLS-bound. Empty state with "New listing" CTA when the host has none. - **`CalendarMonth`** — Server presentational. Mo-first weekday layout, 7×N grid. Per-cell colouring: booking dates render with `bg-brand-primary` (and the booking_id is tooltipped), manual blocks render with `bg-brand-line`. Today gets a `ring-2 ring-brand-dark`. - **`ListingPicker`** (Client) — `<select>` of the host&rsquo;s listings; navigates to `/dashboard/calendar?listing={id}` on change. Picks the first listing if none specified. - **Legend** card at the bottom describes the three states and notes that manual block/unblock UI lands later (this slice is read-only; bookings auto-block via the existing `trigger_booking_confirmed`).
    Notes
    - **Sidebar Calendar nav target now resolves.** Was a 404 before. - **No new packages.** No `react-big-calendar`; the calendar is a ~120-line plain Tailwind grid. Lightweight, no client JS needed for rendering (Server Component). - **`pnpm --filter web build`** passes — 28 routes, calendar 621 B. `pnpm --filter web lint` zero warnings.
    Deferred
    - Drag-to-block dates / manual unblock UI — next slice once we wire the block/unblock Server Actions. - Year view, multi-listing overlay.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  207. Phase 1/22026-05-23

    /dashboard/settings (profile + host + subscription)

    Built
    - **`/dashboard/settings`** — Server page composes three sections: Profile, Public host page, Subscription. - **`ProfileForm`** (Client) — `full_name` + optional `phone`. Email is shown read-only ("change via auth flow"). Saves via `saveProfileAction` which updates `user_profiles` via the user-bound client (RLS `users_update_own`). - **`HostForm`** (Client) — `display_name` + optional `bio` + optional `website_url`. Subtitle shows the live `wieloplatform.com/{handle}`, Verified pill if applicable, and a "View public" external link to `/{handle}`. Saves via `saveHostAction` which updates `hosts` via the user-bound client (RLS `host_manage_own`). - **Subscription card** — Free/Pro/Business label + status text + "See plans" link to `/booking-management#pricing`. Notes that paid plans + billing controls land in Phase 3. - **Onboarding nudge** — if the user has no `hosts` row yet, the Host section shows a "Finish setting up" link to `/signup/host` instead of the form.
    Notes
    - **Handle is read-only.** Changing it is a separate Phase-3 slice that needs old→new redirect handling per PHASE_PLAN.md "Handle redirect". - **Sidebar Settings target now resolves.** Was a 404 before. - **No new packages, no migrations.** - **`pnpm --filter web build`** passes — 29 routes, settings 4.22 kB. `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  208. Phase 22026-05-23

    iCal export per listing

    Built
    - **`/ical/[listing_id]/[token].ics`** Route Handler — public endpoint that serves an RFC 5545 calendar of every blocked date for the listing over the next 24 months. Token-gated (HMAC SHA-256 verified with `timingSafeEqual`). Returns `text/calendar; charset=utf-8` with a 5-minute `Cache-Control` so consumer calendars don&rsquo;t hammer the origin. Strips an optional trailing `.ics` so both `/ical/{id}/{token}.ics` and `/ical/{id}/{token}` resolve. - **`apps/web/lib/ical.ts`** — three helpers: - `signListingToken(listingId)` / `verifyListingToken(id, token)` — HMAC SHA-256 over the listing id with `ICAL_TOKEN_SECRET` (falls back to `SUPABASE_SERVICE_ROLE_KEY` if unset). Token is the first 22 base64url chars (~128-bit entropy). - `buildIcalFeed({calendarName, events})` — hand-rolled RFC 5545 output. `BEGIN:VCALENDAR` … `END:VCALENDAR` with proper escaping (`,`, `;`, `\n`), CRLF line endings, `X-WR-CALNAME` for Apple Calendar. - `collapseConsecutiveDates(rows)` — folds the per-day rows that `blocked_dates` stores into multi-day spans. Most consumers (Airbnb, Booking.com, Apple Calendar) read one VEVENT per stay better than one VEVENT per night. - **`IcalExportPanel`** (Client) on `/dashboard/calendar` — shows the full URL with a Copy button. Toast on success, 2s confirmation state, fallback "copy it manually" toast if `navigator.clipboard` fails. - **`/dashboard/calendar` page** — threads `headers()` to build an absolute URL (works in any environment, no `NEXT_PUBLIC_BASE_URL` needed) and signs a token for the selected listing.
    Changed
    - **`.env.example`** — added `ICAL_TOKEN_SECRET` slot with a note that it falls back to the service role key and that rotation invalidates every active feed URL at once.
    Notes
    - **No `ical_feeds` table.** Per `AGENT_RULES.md` §7.5 ("ask before creating new tables"), this slice opts for the HMAC-derived token pattern. The per-listing rotation that the spec describes (each row in `ical_feeds` holds its own token) lands when we need it — likely with the iCal **import** slice, which does need the table for external-feed URLs anyway. - **Service role used for the read.** The route handler is unauthenticated (the token is the only auth), so the user-bound client has no session. Admin client only reads `listings.name` + `blocked_dates` which are public surface area anyway. - **`pnpm --filter web build`** passes — 30 routes, calendar 2.06 kB (was 621 B before the panel + sign helper). `pnpm --filter web lint` zero warnings.
    Deferred
    - iCal **import** (Wielo pulling Airbnb/Booking blocked dates) — needs the `ical_feeds` table + a 15-minute cron + per-feed parse error handling. Bigger slice. - Per-listing token rotation UI — needs `ical_feeds`. - "Add to Google / Apple / Outlook" deep links — small follow-up.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  209. Phase 22026-05-23

    /dashboard/payments — read-only host payments list

    Built
    - **`/dashboard/payments`** — Server Component listing every payment the host has received (RLS `host_read_own_payments` filters by `bookings.host_id = get_my_host_id()`). 100-row cap, newest first. - **Three KPI tiles** — Collected (sum of `status='completed'`), Pending count (awaiting webhook), Failed count. - **Table columns** — When (captured_at or created_at, en-ZA datetime), Booking ref (link to `/dashboard/bookings/{id}`), Listing name, Method (paystack/paypal/eft → friendly label), Amount, Status pill, Provider ref (first 14 chars). Sidebar Payments nav target now resolves.
    Notes
    - **Read-only first cut.** Refund actions + manual reconciliation land in Phase 3 with the Refund Manager. The KPI tiles compute on the 100-row fetch — when payment volume grows we&rsquo;ll move them to a server-side aggregate. - **No new packages, no migrations.** - **`pnpm --filter web build`** passes — 31 routes, payments 186 B (pure server render). `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  210. Phase 22026-05-23

    /listing/[slug] photo lightbox

    Built
    - **`PhotoGallery`** upgraded from Server presentational to Client. Each photo is now a `<button>` that opens a fullscreen lightbox. The 5-up grid stays unchanged; tap any cell to open at that index. - **Lightbox** — fixed-overlay (`bg-black/90`), centred image (`max-h-[90vh] object-contain`), Close button (top right), Prev/Next arrows (when >1 photo), `{i} / {n}` position counter at the bottom. Keyboard: `Esc` closes, `ArrowLeft` / `ArrowRight` navigate. Click outside the image closes too. `document.body.style.overflow="hidden"` while open so the page doesn&rsquo;t scroll behind. - **"Show all N photos" pill** — bottom-right of the grid when there are more than 5 photos; opens the lightbox at the first photo. Phase 2 paginated "show all photos" page lands when we need it.
    Notes
    - **No new packages.** Pure React state + `useEffect` keyboard handler. - **`pnpm --filter web build`** passes — `/listing/[slug]` 4.98 kB (was 3.92 kB; +1 kB for the lightbox client). `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  211. Phase 1/22026-05-23

    Sidebar stub pages + soft-delete listing

    Built
    - **`/dashboard/inbox`** — "Coming in Phase 3" stub via a new shared `ComingSoon` component. Lists what&rsquo;s coming (enquiries, system messages, attachments, push, saved replies). - **`/dashboard/reviews`** — same shape. Bullets: review request email (24h post-checkout), 48h auto-publish, inline reply, flag for moderation. - **`/dashboard/help`** — real content, not a stub. "Email a real person" card pointing to `hello@wieloplatform.com`, plus shortcuts to `/booking-management`, `#pricing`, `#faq`, and `/change-log`. - **`apps/web/app/dashboard/_components/ComingSoon.tsx`** — reusable honest-stub component (icon + tagline + "Coming in Phase X" + bullets of what to expect). - **Soft-delete listing** at the editor: - `softDeleteListingAction` Server Action sets `deleted_at` (per `AGENT_RULES.md` §2.1 — never hard-delete listings) and forces `is_published=false`. Pre-deletion guard rejects when the listing has bookings in any active status (`pending`, `pending_eft`, `confirmed`, `checked_in`) — error message says how many to cancel/complete first. - 9th editor tab **"Danger zone"** (`DangerTab.tsx`) — Card with AlertTriangle, type-the-listing-name confirmation pattern, red destructive Button. On success: toast + redirect to `/dashboard/listings`. - Existing surfaces already filtered deleted rows: `/dashboard/listings`, `/[handle]`, `/explore`, and `/listing/[slug]` (RLS `public_read_published` enforces it).
    Notes
    - **Three sidebar 404s closed** — Inbox, Reviews, Help. - **Bookings outlive the listing.** Soft-deleting keeps the related rows intact for the guest&rsquo;s booking history and host records. - **`pnpm --filter web build`** passes — 34 routes; editor up from 12.5 kB → 13.2 kB with the new tab. `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  212. Phase 1/22026-05-23

    Last sidebar 404s closed (refunds, staff, channels, calendar-sync, reports, invoices)

    Built
    - **`/dashboard/refunds`** — `ComingSoon` Phase 4. Refund Manager, policy calculator, Paystack/PayPal refund + EFT mark-as-sent, guest escalation. - **`/dashboard/staff`** — `ComingSoon` Phase 3. Email invites, scoped roles (co-host, cleaner, assistant), 3 seats on Pro / unlimited on Business, audit trail. - **`/dashboard/channels`** — `ComingSoon` Post-launch. Push to Airbnb + Booking.com, one-way pricing+availability sync, pull external bookings into Inbox. Pro+ only. - **`/dashboard/calendar-sync`** — not a stub: explains that export is live (links to `/dashboard/calendar`) and import (Airbnb/Booking feeds) lands Phase 2. - **`/dashboard/reports`** — `ComingSoon` Phase 4. Revenue / occupancy heatmap / booking funnel / CSV export. - **`/dashboard/invoices`** — `ComingSoon` Phase 4. Per-booking + monthly subscription invoices, bulk PDF export, hosted invoice URLs.
    Notes
    - **Every sidebar nav target now resolves.** Overview, Bookings, Inbox, Calendar, Listings, Reviews, Payments, Channels, Calendar sync, Staff, Reports, Invoices, Refunds, Settings, Help — all 15 of them. - All six stubs are 100–200 B each — single import + ComingSoon call. - **`pnpm --filter web build`** passes — 40 routes total. `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  213. Phase 22026-05-23

    Dashboard overview redesigned with real KPIs

    Built
    - **`/dashboard` body** rewritten to match the `Dashboard.html` mock&rsquo;s shape with live data: - **Welcome strip** — first-name greeting, pending bookings count in the subtitle ("You have N pending booking(s) to review"), plus "View public page" + "New listing" CTAs in the right rail. - **4 KPI tiles**: **Revenue this month** (sum of `total_amount` where status is confirmed/checked_in/completed), **Bookings this month** (count + confirmed/pending split), **Occupancy** (proxy: booked nights ÷ total available nights × 100; "—" if no published listings), **Avg rating** (from `hosts.avg_rating` + review count). - **Two-column row**: **Recent bookings** (latest 5 with guest + listing + dates + total + Open link) and **Upcoming check-ins** (next 7 days, dated tile + guest + listing). Empty states for each. - **Listings card** — your 5 most recent listings with Draft / Published pill + View (public) + Edit links. - Onboarding banner stays for hosts without a `hosts` row; `EmptyListings` card for hosts with zero listings. - **`KpiTile` / `EmptyState` / `EmptyListings`** — small inline components keeping the file self-contained.
    Notes
    - **All data fetched in parallel** (6 queries) via one `Promise.all`. Pending count uses `select("id", { count: "exact", head: true })` so no rows are returned, just the count. - **No new packages, no migrations.** - **`pnpm --filter web build`** passes — dashboard page weight unchanged at 311 B (the new components compile-time-only; the queries are server-side). `pnpm --filter web lint` zero warnings.
    Commit
    - (single commit for this slice — pushed to `main` after staging.) ---
  214. Phase 02026-05-22

    DB schema live + CI workflows scaffolded

    Built
    - **27 SQL migrations** applied to live Supabase (`zlcivjgvtyeaszikqleu`): - 18 v1.0 migrations (extensions, 9 domains, RLS helpers/policies, functions, triggers, cron, storage RLS, seed) - 9 v1.1 migrations (Policy Manager + Refund Manager domains, ALTERs, RLS, functions, triggers, cron, storage, seed) - Full schema: 46 tables, 4 RLS helper functions, 8+ business functions (`check_feature_permission`, `calculate_booking_price`, `calculate_policy_refund_amount`, `snapshot_booking_policies`, `recalculate_listing_ranking`, etc.), 13+ triggers, 15 pg_cron jobs. - Realtime publication enabled for `messages`, `conversations`, `bookings`. - Storage RLS policies for 6 buckets (`listing-photos`, `host-avatars`, `host-covers`, `eft-proofs`, `message-attachments`, `refund-requests`) — buckets themselves still need to be created in the Supabase dashboard. - `packages/types/database.types.ts` regenerated (3479 lines) — covers full schema. - All 5 GitHub Actions workflows written per `CI_CD.md`: - `ci.yml` — PR validation (typecheck, lint, tests, E2E) - `db-migrate.yml` — auto-apply schema on push + auto-regen + auto-commit types - `deploy-functions.yml` — Edge Functions deploy - `deploy-web.yml` — Vercel deploy - `mobile-preview.yml` — EAS OTA on `develop`
    Fixed
    - `gen_random_bytes()` calls qualified with `extensions.` schema in `staff_invites.token` and `reviews.review_token` defaults — Supabase puts pgcrypto in the `extensions` schema, not `public`, so unqualified calls fail.
    Notes
    - **DB verified live:** queried `platform_settings` via PostgREST, all 10 seeded keys returned. - Migrations follow the spec exactly except for one deviation: `blocked_dates` moved from the listings migration to the bookings migration to resolve a forward FK to `bookings(id)`. - Single Supabase project (no staging yet) per ADR-015. The Frankfurt → af-south-1 migration is still required before public launch. - **Vercel deploy failing:** the first push triggered a Vercel build that compiled cleanly but reported "No Output Directory named public found". Fix: in Vercel Project Settings → Build & Development Settings, set **Root Directory** to `apps/web`. Then redeploy. (Not done in this session — user-side action.) - **Storage buckets still need to be created** by hand in the dashboard (Storage → New bucket). The RLS policies are already in place; they only activate once buckets exist.
    Active blockers / user-side actions for Phase 0
    - Doppler account + dev/staging/prod configs - Vercel root-dir fix + first successful deploy - EAS account + `eas init` for `apps/mobile` - Sentry projects (web + mobile) - PostHog project - Resend account + `wieloplatform.com` domain verification (domain itself not yet registered) - 6 Supabase Storage buckets
    Still TODO (autonomous in next session)
    - Scaffold `apps/mobile` (Expo + NativeWind + Expo Router) - Install shadcn/ui component set from `DESIGN_SYSTEM.md` - Prettier + Husky + Commitlint config - `emails/` directory + React Email setup - Tighten Vercel monorepo config (`vercel.json` or root-dir setting)
    Commits
    - `feat(db): add v1.0 schema migrations` — `7c1ec14` - `feat(db): add v1.1 schema migrations (Refund + Policy Manager)` — `9fa4e67` - `feat(db): apply 27 migrations + generate database.types.ts` — `c623cba`
  215. Phase 02026-05-22

    Monorepo scaffold + Next.js web app

    Built
    - pnpm monorepo: root `package.json` (private), `pnpm-workspace.yaml` declaring `apps/*` + `packages/*`, `turbo.json` with build/dev/lint/type-check tasks, `tsconfig.base.json` for shared TS strict settings. - `apps/web` — Next.js 14.2.35 App Router, TypeScript strict, Tailwind 3.4, no `src/` dir, `@/*` import alias. `tsconfig.json` extends the root base. - Brand-token Tailwind config (`apps/web/tailwind.config.ts`): Wielo primary/secondary/accent/dark/light per `DESIGN_SYSTEM.md` §2 + status palette, custom border-radius (DEFAULT 10px, card 16px, pill, sm), Inter (sans) + Plus Jakarta Sans (display) via CSS variables, shadcn semantic tokens layered on top. - `apps/web/app/globals.css` — shadcn-style HSL CSS variables tuned to Wielo brand (background = brand.light, foreground = brand.dark, primary = brand.primary). - `next/font/google` wiring in `apps/web/app/layout.tsx` for Inter + Plus Jakarta Sans (zero layout shift, auto self-hosted). - shadcn/ui configuration: `components.json` + `lib/utils.ts` (cn helper). Component installs (`pnpm dlx shadcn@latest add ...`) can proceed in any future session. - Supabase SSR wiring per `ARCHITECTURE.md` §7: - `lib/supabase/client.ts` — `createBrowserClient` for Client Components. - `lib/supabase/server.ts` — `createServerClient` with Next.js cookie store for Server Components and Server Actions. - `lib/supabase/middleware.ts` — `updateSession` helper that refreshes the JWT cookie on each request. - `middleware.ts` — wires the helper into Next.js middleware with the standard matcher (skips `_next/static`, `_next/image`, favicon, common image asset paths). - `apps/web/app/page.tsx` — Server Component homepage that fetches `/auth/v1/health` on the linked Supabase project; renders "OK — GoTrue v2.189.0" in green when reachable. Confirms the env vars load and the network path to Supabase works end-to-end. - `packages/types` — workspace package with placeholder `database.types.ts`. Populated by `supabase gen types typescript` after DB migrations land.
    Changed
    - Removed scaffold-default Geist fonts (`apps/web/app/fonts/`). - Replaced the default Next.js boilerplate `page.tsx` and `globals.css` with brand-aligned versions. - Copied root `.env.local` to `apps/web/.env.local` so Next.js can resolve `NEXT_PUBLIC_*` vars; both stay gitignored. Flagged in session notes — when `apps/mobile` lands, switch to a shared loader (dotenv-cli or `next.config.mjs` env merge) to avoid duplication.
    Notes
    - **Verified end-to-end:** `pnpm --filter web build` and `pnpm --filter web lint` both pass with zero errors / zero warnings. Started dev server, curled `http://localhost:3000`, confirmed HTTP 200 and the rendered HTML contains the Supabase project URL plus a live "OK — GoTrue v2.189.0" connection signal from `/auth/v1/health`. - **Node 22.17.1 in use.** `DEVSTACK.md` §1.4 locks Node 20 LTS; Next.js 14.2 is compatible with Node 22 so no blocker, but flagged for revisit. - Minimal dep set installed — only what the homepage needs (`@supabase/supabase-js`, `@supabase/ssr`, `clsx`, `tailwind-merge`, `class-variance-authority`, `tailwindcss-animate`, `lucide-react`). The remaining `DEVSTACK.md` §6 deps (Mapbox, PayPal, Tiptap, react-big-calendar, Resend, react-email, Sentry, PostHog, sonner, react-dropzone, qrcode.react) will be added in the session that first uses each, per CLAUDE.md "least amount of code that solves the problem". - Husky / lint-staged / Commitlint / Prettier are still pending — pick up in a polish session.
    Commits
    - (Single commit for this slice — pushed to `main`.)
  216. Phase 02026-05-22

    Specs added: product, schema, customer journey

    Built
    - `wielo-platform-mvp.md` (85 KB) added — full v1.2 product spec with 10 core modules including Refund Manager (6.9) and Policy Manager (6.10). - `supabase_database.md` (137 KB) added — complete DB architecture: 11 domains, RLS, functions, triggers, pg_cron, Realtime, Storage, seed data, migration strategy. Requires extensions `uuid-ossp`, `pgcrypto`, `pg_trgm`, `postgis`, `pg_cron`. - `customer_journey.md` (86 KB) added — 6 personas across ~50 end-to-end journeys (guest, host free/pro/business, staff, admin, subscriptions).
    Changed
    - `CURRENT_TASK.md` Session Notes: missing-specs blocker removed from "Blockers carried into the next session". - Decided next session focus: scaffold monorepo + Next.js web app (`apps/web`) per `DEVSTACK.md` §1.1 + §6.
    Notes
    - Phase 0 Database section is now **unblocked** — migrations 000000 → 000017 and the v1.1 migration set (20260502000000 → 20260502000017) can be applied in a future session. - `RULES.md` §2 and `AGENT_RULES.md` §2 ("read `supabase_database.md` before any DB-related work") can now be satisfied. - Active blockers remaining: Supabase region migration to `af-south-1` (see ADR-015), `wieloplatform.com` domain ownership not confirmed.
  217. Phase 02026-05-22

    Bootstrap: git, GitHub, Supabase link

    Built
    - Local `git` repository initialized on `main` with a Node/Next/Expo/Supabase `.gitignore`. - Private GitHub repo `Wollie333/Vilo2027` (created in dashboard by user); `main` pushed. - `.env.example` created from the `ENV_VARS.md` §9 template (keys only — no secrets). - Supabase project `Vilo2027` provisioned (ref `zlcivjgvtyeaszikqleu`, region `Central EU (Frankfurt)`). - `supabase init` + `supabase login` (CLI access token) + `supabase link --project-ref zlcivjgvtyeaszikqleu` completed and verified. - `.env.local` populated with Supabase project URL + new-format API keys (`sb_publishable_…`, `sb_secret_…`); confirmed untracked. - `CURRENT_TASK.md` populated as the session contract. - `gh` CLI 2.92.0 installed via winget; `supabase` CLI 2.101.0 installed via direct binary release (no winget package exists).
    Changed
    - Local-only git identity set for this repo: `user.email=wollie333@gmail.com`, `user.name=Wollie333`. No global config touched. - `PHASE_PLAN.md` Phase 5 line "Supabase region confirmed: af-south-1" annotated with the current Frankfurt provisioning + migration requirement.
    Decisions
    - **ADR-015** added: Supabase deployed to Central EU (Frankfurt) rather than `af-south-1` (Cape Town). `af-south-1` was unavailable in the Supabase dashboard for this account at provisioning time. The region MUST be migrated before public launch for POPIA compliance.
    Migrations
    - None this session — DB schema work begins once `supabase_database.md` lands.
    Notes
    - Supabase keys are the newer `sb_publishable_` / `sb_secret_` format (replacements for legacy `anon`/`service_role` JWTs). They work transparently with `@supabase/supabase-js` ≥2.43.x — no SDK bump required. - Only **one** Supabase project exists. The plan originally called for production + staging; staging deferred to a future session. - An earlier Vilo2027 project (ref `ddexrmfuqtvmumgvzqxz`, West EU / Ireland) was created and deleted by the user when neither it nor a re-attempt offered `af-south-1`. Both attempts confirmed `af-south-1` is not currently available for this Supabase account. - `wieloplatform.com` domain ownership and Resend / Doppler / Vercel / EAS / Sentry / PostHog / Mapbox / Paystack / PayPal accounts are NOT set up yet — placeholders remain in `.env.local`. - `supabase_database.md`, `wielo-platform-mvp.md`, and `customer_journey.md` are still missing from the repo. The Phase 0 Database section is blocked until at least `supabase_database.md` is added.
    Commits
    - `chore: initial commit with project documentation` — 2ec4dd9 - `chore: add .env.example from ENV_VARS.md template` — 62b37aa - `chore: bootstrap supabase config, session contract, and changelog` — 969ea79 - (final commit appended after this update is staged.)