J-01 — Discover & Compare on Meta Layer
One-liner: A guest planning a weekend in Kabul finds a guesthouse that matches their constraints and shortlists three candidates before deciding.
1. Purpose
A guest planning a weekend in Kabul wants to find a guesthouse with a halal kitchen, prayer room, and parking, in the AFN 1,500 - 4,000 per-night band, then compare three candidates before deciding. Outcome: the guest holds a shortlist they can act on, and exits this journey to J-02 (handoff into the chosen tenant's booking site).
2. Persona Context
- Persona: Guest (browser or mobile).
- Surfaces: Consumer Meta Web (
apps/web-meta) and Consumer Mobile (apps/mobile). - Primary BFF:
bff-consumer-service. - Backing services (server side, behind BFF):
search-aggregation-service,pricing-service(FX),file-storage-service(signed image URLs),notification-service(rating projection). - Preconditions:
- PWA shell or native app loaded with latest design tokens from
@ghasi/ui-melmastoon. search-aggregation-servicehas indexed published properties (Phase 0: Afghan beachhead tenants).- Locale is auto-detected (
ps-AF/fa-AF/fa-IR/en-US); currency display defaults to device locale with manual switcher.
- PWA shell or native app loaded with latest design tokens from
- Trigger: Guest opens
https://melmastoon.com(or launches the mobile app) and taps "Find a stay".
3. Entry Points
| # | Entry | Surface | Notes |
|---|---|---|---|
| 1 | Direct URL https://melmastoon.com | Web | Default landing |
| 2 | Mobile app launch | Mobile | After splash + permissions |
| 3 | Deep link https://melmastoon.com/search?q=kabul&checkIn=... | Web/Mobile | Via universal/app links |
| 4 | Saved-search push notification (Phase 2) | Mobile | "New halal stays in Kabul match your filter" |
| 5 | Marketing campaign UTM | Web | Lands on /search pre-populated |
4. Screen-by-Screen Flow
4.1 MetaSearchHero (Home — search input)
- Layout (text wire): Centered hero with location autocomplete, paired date inputs (check-in / check-out), guest+room steppers, price-band slider, amenity chip multi-select, "Search" CTA.
- Components:
LocationAutocomplete,DateRangePicker(Solar Hijri presentational forps-AF/fa-*),OccupancyStepper,PriceBandSlider,AmenityChipGroup,Button(variant=emphasis). - Offline: Full inputs render from PWA shell; submitting "Search" while offline shows offline banner + queues nothing (search is deferred until reconnect).
- AI: Optional location autocomplete enhanced by
ai-orchestrator-service /suggestLocations(Phase 2); Phase 1 uses local edge top-100-cities cache. - Errors: Empty location -> inline error; impossible date range -> stepper auto-corrects.
- Loading: Inputs always interactive; "Search" CTA spinner if request in flight > 200 ms.
- A11y: Each input has visible label +
aria-describedbyfor hints; date picker keyboard-navigable per WAI ARIA combobox pattern; chips behave as toggle buttons (aria-pressed). - RTL: Logical layout flips;
DateRangePickerhonours locale calendar direction; numerals follow tenant numerals rule (Latin for displayed date). - Perf: LCP element = hero illustration (preloaded); first interaction < 100 ms.
- Telemetry:
view.page(path=/);frontend.meta.search_input_focused(debounced);frontend.meta.search_submitted { criteria }.
4.2 MetaResultsList (default results view)
- Layout: Top filter bar (sticky on web) + 20 result cards in a virtualised list; each card has hero photo (lazy WebP/AVIF with LQIP), provider name, min/max nightly rate, availability badge, rating, distance from search anchor; a "Compare" toggle in card meta row; "View on map" pill (top-right) toggles to 4.3.
- Components:
FilterBar,ResultCard,LoadingSkeletonList,EmptyState(when 0 results),Pagination. - Offline: PWA service worker serves last successful result page from IndexedDB; pill = "Offline - cached results". Filters re-apply locally if subset matches cache.
- AI: None in Phase 0/1 (privacy: no per-user ranking on meta layer until guest signs in).
- Errors:
MELMASTOON.SEARCH.TIMEOUT-> banner "Slow connection - showing recent results";MELMASTOON.SEARCH.NO_RESULTS_IN_REGION-> empty state with waitlist email capture. - Loading: 8 skeleton cards on first paint; subsequent re-queries fade-swap.
- A11y: Each card is a single tab stop with aria-label including provider, price range, rating; "Compare" toggle is a separate tab stop with
aria-pressed. - RTL: Card meta row flips (rating / distance leading vs trailing); price range never re-orders min/max.
- Perf: Card image lazy-loaded on viewport intersection; FlashList on mobile; window virtualisation on web.
- Telemetry:
frontend.meta.results_rendered { count, latency_ms };frontend.meta.card_clicked { propertyId, position }.
4.3 MetaResultsMap (map view)
- Layout: Full-viewport map with clustered pins; bottom sheet (mobile) / right rail (web) showing the currently-tapped property's quick-info; toggle back to list.
- Components:
MapCanvas(Leaflet web;react-native-mapsmobile),MarkerCluster,PinInfoSheet,MapToggle. - Offline: Map tiles cached for last viewport; new pan/zoom shows cached pins only; "Offline - panning disabled" affordance.
- AI: None.
- Errors: Tile load failure -> grey-out tile with retry CTA.
- Loading: Pins faded in over 200 ms after viewport debounce.
- A11y: Map has fallback list link for screen-reader users; pins are not interactive via screen reader; the "View list" affordance gives keyboard-accessible parity.
- RTL: Map content does not mirror; chrome (sheet, toggles) flips.
- Perf: Bounding-box re-query debounced 350 ms; max 200 pins rendered at once before clustering kicks in.
- Telemetry:
frontend.meta.viewport_changed { boundsHash }(debounced);frontend.meta.pin_tapped { propertyId }.
4.4 MetaCompareDrawer (comparison view)
- Layout: Side-by-side table for 2-3 properties; columns are properties, rows are attributes (price band, amenities checklist, rating, cancellation policy, distance, halal-certified flag, prayer-room availability).
- Components:
CompareTable,CompareCard(sticky property header),Button(primary "Continue with this hotel"). - Offline: Comparison is computed locally from cached property summaries; CTA disabled with explanation.
- AI: None in P1; P2 introduces "Best fit for you" highlight with provenance.
- Errors: Missing comparison data field -> "n/a" cell, never blank.
- Loading: Skeleton table while fetching
GET /api/v1/search/compare?ids=.... - A11y: Table uses
<th scope="col">for property headers,<th scope="row">for attribute rows; each "Continue" button has full property name in aria-label. - RTL: Column order reverses for RTL locales; the "selected" property visual cue follows reading direction.
- Perf: Comparison opens in <= 300 ms; table render <= 100 ms.
- Telemetry:
frontend.meta.compare_opened { ids };frontend.meta.compare_continue_clicked { propertyId }.
4.5 PropertyDetailScreen
- Layout: Hero gallery (5+ photos, thumbnails), title block, amenities grid, policies accordion, room-type preview cards, reviews block (lazy), embedded map, sticky "Book on hotel site" CTA bottom.
- Components:
Gallery,AmenitiesGrid,PolicyAccordion,RoomTypeCard,ReviewBlock,MapEmbed,Button(sticky CTA). - Offline: Last 20 viewed property detail responses served from IndexedDB / MMKV; sticky CTA shows offline state and disables.
- AI: None on consumer surface in P1; P2 may insert AI-generated summary chips.
- Errors: Image load failure -> skeleton placeholder, never broken-image icon; description missing -> hide row.
- Loading: Skeleton matches final layout; gallery thumbnails first, hero second.
- A11y: Heading levels respected (h1 = property name); gallery is keyboard-navigable carousel; reviews use
aria-live="polite"for "load more" announcements. - RTL: Gallery scroll direction follows reading direction; price layout does not flip.
- Perf: LCP target <= 2.5 s on Slow 4G / Moto G4; first network call yields hero photo srcset; below-the-fold lazy.
- Telemetry:
frontend.meta.property_viewed { id };frontend.meta.gallery_image_viewed { index };frontend.meta.cta_book_clicked { propertyId, tenantId }(transitions to J-02).
5. State Machine
6. Data Requirements
6.1 Server state (TanStack Query / RN Query)
| Query | Key | BFF endpoint | staleTime | gcTime | Notes |
|---|---|---|---|---|---|
searchProperties | ['consumer.v1', 'search', sanitizedQuery] | POST /api/v1/search/properties | 30 s | 5 min | Edge-cached 15 s; query-keyed |
searchMap | ['consumer.v1', 'search', sanitizedQuery, 'map'] | POST /api/v1/search/map | 30 s | 5 min | Bounding-box variant |
getCompare | ['consumer.v1', 'compare', ids] | GET /api/v1/search/compare?ids=... | 60 s | 5 min | Returns denormalised projection |
getProperty | ['consumer.v1', 'hotel', propertyId] | GET /api/v1/properties/:id | 60 s | 30 min | Aggregates property + pricing + ratings |
getFacets | ['consumer.v1', 'facets', country] | GET /api/v1/facets?country=... | 5 min | 30 min | Used by location autocomplete fallback |
6.2 URL state
/search?q=&checkIn=&checkOut=&adults=&rooms=&priceMin=&priceMax=&amenities=&sort=&view=list|map- Filters and sort are deep-linkable (SEO + share). View toggle persists in URL.
6.3 Local persistence
- IndexedDB (web) / MMKV (mobile): last 50 search responses; last 20 property details; locale + currency preference; recent searches.
- No PII stored client-side.
6.4 Idempotency
- All requests are GET / safe POST (search). No mutations on this journey.
7. AI Behavior
n/a in Phase 0/1 — privacy stance: no per-user personalisation on meta layer until guest signs in. Phase 2 introduces (with explicit consent and provenance):
ai-orchestrator-service /suggestLocationsfor autocomplete enhancementai-orchestrator-service /rerankfor personalised ranking- "Best fit for you" callout in compare drawer
When introduced, all AI elements MUST follow the canonical HITL pattern (no auto-action; provenance shown).
8. Offline Behavior
- PWA service worker serves last successful results page from IndexedDB (web).
- Mobile RN reads from MMKV for last 50 search responses.
MetaResultsListshows "Offline - cached results" pill.- New search: queued on web (deferred until reconnect); on mobile, search button disabled with banner.
PropertyDetailScreen: 20 cached property details readable; "Book on hotel site" CTA disabled with offline banner.- Map view: cached pins only; pan/zoom outside cache greys tiles with retry.
9. Error States
| Error | Trigger | UX shown | Recovery | Telemetry |
|---|---|---|---|---|
MELMASTOON.SEARCH.TIMEOUT | search-aggregation-service p99 breached | Banner: "Slow connection - showing recent results" + cached set if any | Auto-retry on next search submit; user can manually retry | error.surfaced { code, traceId } |
MELMASTOON.SEARCH.NO_RESULTS_IN_REGION | Phase 0 limited geo coverage | Empty state: "We're not in this city yet - tell us where to launch next" + waitlist email | User edits criteria or submits waitlist | frontend.meta.no_results { country } |
MELMASTOON.PRICING.FX_STALE | FX feed stale > 24 h for target currency | Display falls back to USD with tooltip | Tooltip explains snapshot freshness; user can pick another currency | error.surfaced { code } |
MELMASTOON.STORAGE.MEDIA_LOAD_FAILED | CDN image fetch failure | Skeleton placeholder remains; provider name + rate visible | Auto-retry on next viewport intersection | frontend.meta.image_failed { propertyId } |
MELMASTOON.SEARCH.RATE_LIMITED | client-side rate-limit (rare) | Toast "Too many searches - please wait | Honour Retry-After | error.surfaced { code } |
10. E2E Test Gates
| Gate | Scenario | Surface |
|---|---|---|
G-WEB-1 (composite, see ../../common/10-frontend-testing-strategy.md §3) | Search -> filter -> map toggle -> property detail -> handoff | Web meta + tenant booking |
G-MOB-1 (composite) | Discover (search) -> property detail -> "Book" -> bootstrap tenant -> ... | Mobile |
| Per-screen tests | LCP <= 2.5 s on results screen on Slow 4G; compare drawer opens <= 300 ms; RTL render verified for Pashto | Web + Mobile |
11. Performance Requirements
| Metric | Target | Source |
|---|---|---|
| LCP (results screen, p75) | < 2.5 s on Slow 4G / Moto G4 | NFRs §1.1 |
| INP (filter / sort interaction) | < 200 ms | NFRs §1.1 |
| Compare drawer open | <= 300 ms | this journey |
| Property detail mount | <= 1.5 s p95 (mobile cold) | NFRs §1.2 |
| First network call payload | <= 80 KiB gzipped | NFRs §7 capacity |
12. Accessibility Requirements
- WCAG 2.2 AA across all five screens.
- Each card on the results list is a single tab stop with comprehensive aria-label.
- Date picker is keyboard-operable per WAI ARIA combobox pattern.
- Map fallback: list link is always keyboard-reachable.
- Hero gallery is a keyboard-navigable carousel (left/right arrows; Home/End to first/last; Esc to close fullscreen).
- Touch targets >= 44 x 44 pt mobile / 32 x 32 px web.
- Reduced-motion: gallery transitions collapse to opacity fades; map pin animations disabled.
- Screen reader announces results count change ("12 results found").
13. Telemetry
Frontend events
| Event | When | Properties |
|---|---|---|
view.page / view.screen | Mount of any screen in this journey | path, tenantId?, propertyId? |
frontend.meta.search_submitted | Submit search | criteria (sanitised) |
frontend.meta.results_rendered | Results list paints | count, latency_ms |
frontend.meta.filter_applied | Toggle a filter | filter, value |
frontend.meta.viewport_changed | Map pan/zoom (debounced) | boundsHash |
frontend.meta.compare_opened | Compare drawer opens | ids |
frontend.meta.property_viewed | PropertyDetail mount | id |
frontend.meta.cta_book_clicked | Tap "Book on hotel site" | propertyId, tenantId |
error.surfaced | Visible error | errorCode, httpStatus, path, traceId |
Domain events emitted (server side)
melmastoon.search_aggregation.query.submitted.v1(sampled)melmastoon.search_aggregation.result_set.served.v1melmastoon.search_aggregation.filter_applied.v1melmastoon.search_aggregation.viewport_changed.v1(debounced)melmastoon.search_aggregation.property.viewed.v1
14. Success Criteria
- LCP <= 2.5 s on Slow 4G for results screen (Moto G4 emulation).
- All copy renders in user's locale (Pashto / Dari / Persian / EN); RTL layout correct end-to-end.
- Min/max prices reconcile with
pricing-servicesnapshot within +/- 0.5%. - Compare drawer opens <= 300 ms.
- Property detail mounts <= 1.5 s p95 on mobile cold.
- Zero broken-image icons on result cards (skeleton fallback always).
- E2E gate G-WEB-1 step "search to property detail to handoff" passes on every release branch.