# Project Template: Native Betting Study App

This is the canonical working plan and progress log for the project. Use this file for progress notes, next steps, and status updates.

## Current Status

### Done So Far

- Android was renamed from the old study app into `com.hermes.app`, with the app entry, theme, repository, media, and feature screens rewritten around the backend-backed Hermes flow.
- Android no longer relies on the sample fixture; it boots from backend session and round data, uses `/health.server_time` for clock sync, buffers analytics, and flushes them to the backend.
- Android localization is in place for English and Swedish, including the remaining locale toggle labels and session language display.
- Backend now exposes `server_time` from `/health`, publishes the matching OpenAPI contract, and records audit events for session, bet, event, market, odds, and settlement actions.
- Backend smoke coverage was added for `server_time` and audit logging.
- iOS now follows the backend-backed flow, syncs clock from `/health`, flushes analytics, and uses localized language labels instead of hardcoded `EN` and `SV`.
- iOS source was updated for the backend-backed session and round flow, including the real preview cue points and localized session strings.
- iOS settings now uses localized copy instead of a scaffold placeholder, and the screen is wired into the root scroll flow.
- Android debug build passes with `./gradlew :app:assembleDebug`.
- Backend tests pass with `cargo test`.
- Backend localization bundles now have a contract test, and the localization catalog matches the shipped English and Swedish keys.
- The research export path is documented under `docs/research/export-path.md`.
- Backend bet coverage now includes a direct regression for late-lock rejection.
- Backend bet coverage now includes invalid-session and invalid-market rejection regressions, plus settlement override coverage.
- Backend analytics coverage now includes a representative key user flow batch.
- iOS project scaffolding now exists as `mobile/ios-app/HermesApp.xcodeproj` with a shared scheme and XCTest target.
- iOS sources and tests build and pass in Xcode on an iPhone simulator, and a standalone localization/error-mapping harness passes.
- Android local unit tests now cover localization, error mapping, and analytics batch conversion, and `./gradlew testDebugUnitTest` passes with the Android SDK installed.
- Android swipe-down gesture handling now uses cumulative drag distance, and edge-case tests cover upward drags, slow swipes, and per-gesture single-trigger behavior.
- iOS now defaults to a demo test mode with mock rounds, and the primary round flow is a fullscreen swipe-driven video experience with a 15-second lock timer and accelerated reveal transition.
- The fullscreen round interaction contract is now documented in `docs/mobile-fullscreen-round-flow.md`, including demo-mode rules, side-tint swipe feedback, and the requirement to finish the accelerated round clip before reveal starts.

### Still Open

- Continue through the remaining plan phases and finish any leftover localization and polish work.
- Keep the iOS project in sync with the plan as the app grows.
- Keep expanding tests around session, odds, settlement, and analytics behavior.
- Align Android with the clarified fullscreen swipe flow and demo-mode testing path.

## 1. Purpose

Build a native mobile prototype for iOS and Android for a research
study. Users watch a short sports clip before a critical moment, choose
an outcome before a lock time, and then watch the reveal segment.

The app must feel premium, fast, clear, mobile-first, and highly
polished. It must feel as easy and fun to use as possible as it's needed
for the research study. As mentioned, it is for research and
prototype use, not first-release real-money gambling.
However, the app will be tests on a small group and compared against traditional
betting sites. We thereful need to make this as premium and good as possible
to make sure that we can make meaningful observations and discussionpoints from the outcome.

The app must support:

- English
- Swedish

Every user-facing string must be localizable from day one.

---

## 2. Product Goals

### Primary goals

- Native feel on both iOS and Android
- Very high responsiveness
- Smooth video playback
- Strong gesture support
- Clear and fair lock timing
- Excellent readability and touch ergonomics
- Premium visual quality
- Stable architecture for long-term iteration

### Secondary goals

- Low initial app size
- Reusable backend
- Easy experimentation between:
  - `control`
  - `modern`

### Non-goals

- Real money betting in the first version
- Full regulatory launch in version one

---

## 3. Technology Stack

## Backend

- Rust
- Axum
- Tokio
- SQLx
- PostgreSQL
- Valkey (redis)
- tracing
- thiserror + anyhow
- validator

- OpenTelemetry
- Docker
- docker compose

## iOS

- Swift
- SwiftUI
- UIKit where needed
- AVFoundation / AVPlayer
- URLSession
- async/await
- native haptics
- native gesture handling

## Android

- Kotlin
- Jetpack Compose
- Media3 / ExoPlayer
- Kotlin coroutines
- Kotlin Flow
- Kotlinx Serialization
- kotlinx-datetime
- kotlinx-collections-immutable
- native haptics
- native gesture handling

## Shared principles

- No cross-platform UI
- Native UI on each platform
- Shared contracts through generated schemas and strict API specs
- Prefer Kotlinx libraries broadly inside Android and Kotlin tooling
- If a small shared Kotlin module is added later, prefer:
  - coroutines
  - Flow
  - Kotlinx Serialization
  - kotlinx-datetime
  - kotlinx-collections-immutable

---

## 4. Architecture Overview

The system has three layers:

### 4.1 Client layer

Two separate native apps:

- `ios-app`
- `android-app`

Client responsibilities:

- UI rendering
- video playback
- gestures
- odds overlay
- local state
- media prefetch
- clock sync
- sending user selections
- analytics and telemetry capture
- localization in English and Swedish

### 4.2 API layer

Rust backend responsible for:

- auth
- sessions
- event feed
- odds distribution
- bet acceptance
- settlement
- experiment assignment
- analytics ingestion
- audit trail

### 4.3 Data layer

- PostgreSQL for durable relational data
- Redis for short-lived state and caching
- object storage / CDN for HLS assets and media metadata

---

## 5. Repository Structure

```text
root/
  README.md
  docs/
    product/
    architecture/
    api/
    ux/
    research/
    localization/
    analytics/
  backend/
    Cargo.toml
    Cargo.lock
    migrations/
    src/
      main.rs
      config.rs
      app_state.rs
      error.rs
      telemetry.rs
      db/
      auth/
      users/
      sessions/
      events/
      media/
      markets/
      outcomes/
      odds/
      bets/
      settlement/
      analytics/
      experiments/
      localization/
      admin/
      jobs/
      utils/
    tests/
  mobile/
    ios-app/
    android-app/
  contracts/
    openapi/
    protobuf/
    localization/
  fixtures/
    videos/
    manifests/
    test-data/
  scripts/
  infra/
```

---

## 6. Core Principles

1. The server is authoritative
   - lock time is determined by the server
   - odds acceptance is determined by the server
   - settlement is determined by the server

2. The client must not decide fairness
   - the client sends intent
   - the server accepts or rejects

3. Video must never block on UI updates
   - player and overlay must be architecturally separate

4. Explicit state machines are required
   - client and server must follow the same state model

5. Optimize for smoothness
   - low input latency
   - consistent animation timing
   - clear hierarchy
   - readable odds and timers

6. Localization is first-class
   - all UI strings in English and Swedish
   - no hardcoded copy in views
   - use stable translation keys

7. Log aggressively
   - log as many meaningful user actions as possible
   - support later UX optimization and research analysis
   - keep logs structured and queryable

---

## 7. Main Product Flow

A user session contains multiple rounds.

Each round:

1. Prefetch the next event
2. Show the preview segment
3. Update odds during preview
4. User selects an outcome before `lock_at`
5. Show locked state
6. Play reveal segment
7. Show result
8. Prepare the next round

---

## 8. Domain Model

## Core entities

### User

- id
- external_ref
- created_at
- preferred_language
- device_platform

### Session

- id
- user_id
- started_at
- ended_at
- experiment_variant
- app_version
- device_model
- os_version
- locale_code

### Event

- id
- sport_type
- source_ref
- title_en
- title_sv
- status
- preview_start_ms
- preview_end_ms
- reveal_start_ms
- reveal_end_ms
- lock_at
- settle_at

### EventMedia

- id
- event_id
- media_type
- hls_master_url
- poster_url
- duration_ms
- preview_start_ms
- preview_end_ms
- reveal_start_ms
- reveal_end_ms

### Market

- id
- event_id
- question_key
- market_type
- status
- lock_at
- settlement_rule_key

### Outcome

- id
- market_id
- outcome_code
- label_key
- sort_order

### OddsVersion

- id
- market_id
- version_no
- created_at
- is_current

### OutcomeOdds

- id
- odds_version_id
- outcome_id
- decimal_odds
- fractional_num
- fractional_den

### BetIntent

- id
- user_id
- session_id
- event_id
- market_id
- outcome_id
- idempotency_key
- client_sent_at
- server_received_at
- accepted
- acceptance_code
- accepted_odds_version_id

### Settlement

- id
- market_id
- settled_at
- winning_outcome_id

### ExperimentAssignment

- id
- user_id
- session_id
- variant
- assigned_at

### LocalizationKey

- id
- key_name
- description

### LocalizationValue

- id
- localization_key_id
- locale_code
- text_value

### AnalyticsEventType

- id
- event_name
- description

### AnalyticsEvent

- id
- session_id
- user_id
- analytics_event_type_id
- occurred_at
- app_screen_id
- event_sequence_no

### AnalyticsEventAttribute

- id
- analytics_event_id
- attribute_name
- attribute_value

### AuditLog

- id
- actor_type
- actor_id
- action_name
- created_at
- target_type
- target_id

### AuditLogAttribute

- id
- audit_log_id
- attribute_name
- attribute_value

---

## 9. State Machines

### Event lifecycle

- `scheduled`
- `prefetch_ready`
- `preview_open`
- `locking`
- `locked`
- `reveal_open`
- `settled`
- `archived`

### Client round state

- `idle`
- `prefetching`
- `ready`
- `preview_playing`
- `selection_pending`
- `selection_submitting`
- `selection_accepted`
- `locked`
- `reveal_playing`
- `result_visible`
- `transitioning`
- `error`

### Bet acceptance state

- `received`
- `validated`
- `accepted`
- `rejected_too_late`
- `rejected_invalid_market`
- `rejected_invalid_session`
- `rejected_duplicate`
- `settled`

---

## 10. Backend Modules

## 10.1 auth

Responsibilities:

- anonymous study sign-in or simple participant sign-in
- token issuance
- session binding

## 10.2 users

Responsibilities:

- user creation
- locale preference
- participant metadata

## 10.3 sessions

Responsibilities:

- start and end sessions
- attach device metadata
- track experiment group

## 10.4 events

Responsibilities:

- list events
- return next round
- return event manifest

## 10.5 media

Responsibilities:

- expose media metadata
- expose HLS URLs
- expose cue points

## 10.6 markets

Responsibilities:

- create and query markets
- attach outcomes
- manage lock time

## 10.7 odds

Responsibilities:

- publish odds versions
- stream odds changes
- store full relational odds history

## 10.8 bets

Responsibilities:

- receive bet intents
- validate requests
- accept or reject against lock time
- enforce idempotency

## 10.9 settlement

Responsibilities:

- store winning outcome
- expose settlement result
- audit all settlement actions

## 10.10 experiments

Responsibilities:

- assign `control` or `modern`
- return feature flags
- keep assignment stable per session

## 10.11 analytics

Responsibilities:

- ingest all telemetry
- normalize event types
- write event attributes to child tables
- support analysis exports

## 10.12 localization

Responsibilities:

- serve language bundles
- support English and Swedish
- version translation payloads

## 10.13 admin

Responsibilities:

- create events
- create markets
- create odds versions
- publish fixtures
- set settlements

---

## 11. Database Design

Use SQLx migrations only.
Do not use JSONB.
Use normalized relational tables instead.

### Required tables

- users
- sessions
- events
- event_media
- markets
- outcomes
- odds_versions
- outcome_odds
- bet_intents
- settlements
- experiment_assignments
- localization_keys
- localization_values
- analytics_event_types
- analytics_events
- analytics_event_attributes
- audit_logs
- audit_log_attributes

### General rules

- UUID primary keys
- `timestamptz` for all timestamps
- no JSONB columns
- use child tables for attributes and event payloads
- store translation text in relational tables
- use foreign keys everywhere possible

### Key indexes

- `sessions.user_id`
- `sessions.started_at`
- `events.status`
- `markets.event_id`
- `markets.lock_at`
- `bet_intents.session_id`
- `bet_intents.market_id`
- `bet_intents.idempotency_key`
- `analytics_events.session_id`
- `analytics_events.user_id`
- `analytics_events.analytics_event_type_id`
- `audit_logs.created_at`

### Notes

For analytics and audit data, avoid schemaless storage.
Use parent-child tables such as:

- `analytics_events`
- `analytics_event_attributes`
- `audit_logs`
- `audit_log_attributes`

This allows queryable, indexable telemetry without JSONB.

---

## 12. API Design

## 12.1 Style

- REST for initialization and main flows
- WebSocket or SSE for live odds and state updates
- JSON transport is acceptable over the API
- database storage must remain relational
- version prefix: `/api/v1`

## 12.2 Core endpoints

### Session

- `POST /api/v1/session/start`
- `POST /api/v1/session/end`
- `GET /api/v1/session/me`

### Feed and events

- `GET /api/v1/feed/next`
- `GET /api/v1/events/{event_id}`
- `GET /api/v1/events/{event_id}/manifest`

### Markets and odds

- `GET /api/v1/events/{event_id}/markets`
- `GET /api/v1/markets/{market_id}/odds/current`
- `GET /api/v1/stream`

### Bets

- `POST /api/v1/bets/intent`
- `GET /api/v1/bets/{bet_intent_id}`

### Results

- `GET /api/v1/events/{event_id}/result`

### Analytics

- `POST /api/v1/analytics/batch`

### Experiments

- `GET /api/v1/experiments/config`

### Localization

- `GET /api/v1/localization/{locale_code}`

### Admin

- `POST /api/v1/admin/events`
- `POST /api/v1/admin/markets`
- `POST /api/v1/admin/odds`
- `POST /api/v1/admin/settlements`

---

## 13. Native Client Architecture

## 13.1 Common structure

Each app must have:

- Presentation
- Feature state
- Domain
- Networking
- Media
- Localization
- Analytics
- Persistence

## 13.2 iOS structure

```text
ios-app/
  App/
  Core/
    Networking/
    DesignSystem/
    Analytics/
    Media/
    Haptics/
    Gestures/
    Localization/
  Features/
    Onboarding/
    Feed/
    Round/
    Selection/
    Reveal/
    Result/
    Session/
    Settings/
  Resources/
  Tests/
```

### iOS rules

- SwiftUI by default
- UIKit only where precision control is needed
- AVPlayer for playback
- separate player state from overlay state
- all user strings loaded from localization layer
- English and Swedish supported at launch

## 13.3 Android structure

```text
android-app/
  app/
  core/
    network/
    designsystem/
    analytics/
    media/
    haptics/
    gestures/
    localization/
  data/
  domain/
  feature/
    onboarding/
    feed/
    round/
    selection/
    reveal/
    result/
    session/
    settings/
  benchmark/
  androidTest/
```

### Android rules

- Jetpack Compose by default
- Media3 / ExoPlayer for playback
- ViewModel + immutable state
- Kotlin coroutines and Flow everywhere practical
- Kotlinx Serialization for API models
- kotlinx-datetime for time handling
- kotlinx-collections-immutable for UI collections
- optimize recomposition carefully
- English and Swedish supported at launch

---

## 14. Gesture Specification

The app must feel mobile-native and gesture-friendly.

### Supported gestures

- tap to select outcome
- vertical swipe between rounds or cards where appropriate
- swipe down to dismiss modal
- edge gestures only where platform expectations exist
- no horizontal scrubbing during an active betting round

### Rules

- gestures must never make selection state ambiguous
- a critical choice must get clear visual confirmation
- avoid complex multi-touch gestures
- no gesture may fight system navigation
- touch targets must be large and comfortable

### Haptics

- subtle feedback when a selection is accepted
- subtle feedback when the market locks
- never overuse haptics

---

## 15. Screens

### Onboarding

- study intro
- consent
- language selection
- start session

### Feed

- next round preview card
- compact event information
- clear call to action

### Active round

- video
- countdown
- odds panel
- selection controls
- lock status
- pause or exit control

### Locked state

- selected outcome highlighted
- lock status clearly visible
- odds frozen for the user

### Reveal

- reveal segment
- simple visual emphasis
- no chaotic effects

### Result

- what happened
- user selection
- outcome display
- next round entry

### Pause and exit

- easy to reach
- easy to confirm
- no manipulative friction

---

## 16. Design System

### Visual direction

- premium dark base
- strong contrast
- limited accent colors
- clean typography
- large video surface
- restrained motion
- high readability

### Design tokens

Define centrally:

- colors
- spacing
- radii
- elevation
- typography scale
- icon sizes
- motion durations
- motion curves

### Motion rules

- quick feedback on touch
- calm transitions
- one consistent motion language
- no flashing or overly aggressive effects
- reveal should feel polished, not loud

---

## 17. Localization Requirements

The app must ship with:

- English
- Swedish

### Rules

- every visible string uses a localization key
- no hardcoded strings in views
- backend can serve localization bundles if needed
- use pluralization support where relevant
- event and market text must support both languages
- analytics must include active locale for each session

### Suggested locale codes

- `en`
- `sv`

---

## 18. Video and Media Pipeline

### Requirements

- HLS as the default streaming format
- short segments
- correct keyframes around:
  - preview start
  - preview end
  - lock cue
  - reveal start
  - reveal end

### Playback goals

- prefetch the next clip early
- keep player and overlay independent
- recover gracefully from playback errors
- support poster fallback

### Cue points

Each media item must define:

- preview start
- preview end
- lock cue
- reveal start
- reveal end

---

## 19. Fairness and Clock Sync

### Rule

The client may display a local countdown based on synced server time,
but the server always decides final bet acceptance.

### Requirements

- regular server clock sync
- client clock drift compensation
- request tracing for all bet intents
- strict idempotency keys
- duplicate submissions must be safe

### Logged timestamps

For all critical actions, capture:

- client timestamp
- server receive timestamp
- request id
- session id
- app version
- locale
- device platform

---

## 20. Analytics and Telemetry

Log as many meaningful actions as possible to support later
optimization, analysis, and UX refinement.

## 20.1 Core rule

All analytics events must be structured.
Do not store opaque analytics blobs in JSONB.
Use event tables and attribute tables.

## 20.2 Minimum tracked actions

- app_opened
- app_backgrounded
- app_closed
- language_selected
- consent_viewed
- consent_accepted
- session_started
- session_ended
- feed_viewed
- round_card_viewed
- round_loaded
- event_manifest_received
- preview_prefetch_started
- preview_prefetch_completed
- preview_started
- preview_paused
- preview_resumed
- preview_completed
- countdown_visible
- countdown_warning_threshold_hit
- odds_panel_viewed
- odds_version_received
- odds_changed
- outcome_focused
- outcome_selected
- selection_submitted
- selection_accepted
- selection_rejected
- duplicate_selection_attempt
- market_locked
- reveal_started
- reveal_completed
- result_viewed
- next_round_requested
- session_paused
- exit_prompt_viewed
- exit_confirmed
- playback_error
- network_error
- stream_reconnected
- localization_bundle_loaded
- locale_changed
- haptic_triggered
- gesture_swipe
- gesture_tap
- gesture_cancelled
- screen_viewed
- cta_pressed

## 20.3 Analytics event schema

Store:

- event row in `analytics_events`
- one row per attribute in `analytics_event_attributes`

Example attributes:

- screen_name
- event_id
- market_id
- outcome_id
- odds_version_id
- countdown_ms_remaining
- locale_code
- experiment_variant
- network_type
- device_orientation
- playback_position_ms
- latency_ms

## 20.4 Research metrics

- decision latency
- round completion rate
- missed rounds
- post-lock selection attempts
- replay desire
- exit rate
- difference between control and modern
- effect of locale
- effect of device platform

---

## 21. Audit Logging

Audit logging is separate from analytics.

### Audit goals

- fairness
- traceability
- admin accountability
- settlement trace

### Audit examples

- event created
- market opened
- odds version published
- market locked
- bet accepted
- bet rejected
- result settled
- admin override applied
- localization updated

Store:

- `audit_logs`
- `audit_log_attributes`

---

## 22. Security

- signed tokens
- strict input validation
- rate limiting
- request ids
- full tracing
- secrets outside the repo
- environment-based config
- server-side experiment assignment
- server-side odds authority

---

## 23. Testing Strategy

## Backend

- unit tests per module
- integration tests with PostgreSQL
- SQLx compile-time query checks
- state transition tests
- API contract tests
- migration tests

## iOS

- unit tests for view models and reducers
- snapshot tests for core screens
- UI tests for the full round flow
- playback error tests
- localization tests for English and Swedish

## Android

- unit tests for reducers and view models
- Compose UI tests
- macrobenchmark tests
- playback tests
- localization tests for English and Swedish
- coroutine and Flow tests

## End-to-end

- start session
- assign variant
- load event
- prefetch preview
- start preview
- submit selection
- lock market
- play reveal
- settle result
- ingest analytics

---

## 24. Performance Goals

## Backend

- p95 standard GET < 100 ms in staging-like conditions
- p95 `POST /bets/intent` < 150 ms
- low odds stream latency

## Mobile

- no visible jank in the round flow
- very fast touch feedback
- next round prefetched before the current one ends
- low memory churn on screen transitions

---

## 25. CI and CD

## Backend pipeline

- rustfmt
- clippy
- tests
- SQLx checks
- migration validation
- build container

## iOS pipeline

- lint
- unit tests
- UI tests
- localization checks

## Android pipeline

- ktlint or equivalent
- detekt if adopted
- unit tests
- Compose tests
- benchmark checks where practical
- localization checks

## General

- PR template
- changelog
- release tags
- API contract versioning

---

## 26. Configuration Profiles

Minimum environments:

- `local`
- `dev`
- `staging`
- `study`
- `prod`

Each environment must define:

- backend URL
- stream URL
- CDN base URL
- analytics mode
- experiment config
- localization source
- log verbosity

---

## 27. Feature Flags

Must support flags for:

- control vs modern
- animation intensity
- haptics enabled
- autoplay next enabled or disabled
- countdown style
- odds update visualization
- result card style
- localization source mode

---

## 28. AI Agent Execution Plan

The AI agent must work in this order.

After each major completed change set, create a git commit and push it before starting the next major change.

### Phase 1: Documents

1. Write root README
2. Write architecture document
3. Write API spec
4. Write relational schema
5. Write SQLx migrations
6. Write state machine spec
7. Write localization key catalog
8. Write analytics taxonomy

### Phase 2: Backend foundation

1. Initialize Rust workspace
2. Add Axum, Tokio, SQLx, tracing, serde, config
3. Add config system
4. Add app state
5. Add error handling
6. Add health endpoint
7. Connect PostgreSQL
8. Add migrations
9. Add Redis
10. Add session endpoints

### Phase 3: Domain and API

1. Implement users
2. Implement sessions
3. Implement events
4. Implement event media
5. Implement markets
6. Implement outcomes
7. Implement odds versions
8. Implement bet intents
9. Implement settlement
10. Implement experiments
11. Implement localization
12. Implement analytics

### Phase 4: Fixtures and admin

1. Add seed scripts
2. Add fixture loading
3. Add admin endpoints
4. Add test event generator

### Phase 5: iOS app

1. Initialize project structure
2. Build design system
3. Build localization layer
4. Build networking client
5. Build feed screen
6. Build round screen
7. Integrate AVPlayer
8. Build countdown and odds overlay
9. Build selection flow
10. Build reveal and result
11. Add analytics

### Phase 6: Android app

1. Initialize project structure
2. Build design system
3. Build localization layer
4. Build networking client
5. Use Kotlin coroutines and Flow broadly
6. Use Kotlinx Serialization for API models
7. Use kotlinx-datetime for time handling
8. Build feed screen
9. Build round screen
10. Integrate Media3
11. Build countdown and odds overlay
12. Build selection flow
13. Build reveal and result
14. Add analytics

### Phase 7: Quality

1. Add tests
2. Validate localization
3. benchmark critical paths
4. review gesture edge cases
5. validate analytics coverage
6. document research export path

---

## 29. Definition of Done

A version is done when:

- the user can start a session
- a variant is assigned
- the next event is loaded
- preview video plays
- odds are shown and updated
- the user can select an outcome
- the server accepts or rejects correctly
- lock state is clear
- reveal plays
- result is shown
- analytics are written relationally
- English and Swedish both work
- the flow passes end-to-end tests

---

## 30. Future Extensions

- small shared Kotlin module for non-UI state logic
- researcher admin web panel
- analytics export to CSV or Parquet
- accessibility pass
- tablet and foldable layouts
- richer localization management
- advanced telemetry dashboards

---

## 31. Important Constraints

- no real money flow in the first version
- localization in English and Swedish is mandatory
- normalized relational logging is mandatory
- avoid JSONB in database design
- log broadly enough to enable later UX optimization
