# Phase 2 Implementation Plan: Mitra Online Status & Pairing Logic ## Summary of Clarified Requirements | Topic | Decision | |---|---| | Mitra default status | Offline on app open; must explicitly toggle online | | Auto-offline | Yes — on app close / connection loss | | Online/offline logs | Backend + control center only; mitra don't see their own logs | | Multi-device | No — single device per mitra | | Customer active requests | One at a time; one active session at a time | | Search timeout | 60 seconds, then "no bestie available" message | | Cancel while searching | Yes | | Pairing blast | Valkey pub/sub (in-app real-time); push notifications deferred | | Race condition | First-come-first-served | | Mitra decline/ignore | Allowed; request stays open until 60s timeout or customer cancel | | Session end | Explicit end only (time-based deferred to next phase) | | Payment placeholder | `pending_payment` status, skip to `active` for now | | Max customer per mitra | Global config (not per-mitra) | | Reroute | Forced assignment (acceptance-based deferred) | | Reroute target | Online mitras only | | Dashboard refresh | Polling-based auto-refresh now; SSE/push later | --- ## 1. Database Changes ### 1.1 New table: `mitra_online_status` Tracks the current online/offline state per mitra. | Column | Type | Notes | |---|---|---| | `id` | `SERIAL PRIMARY KEY` | | | `mitra_id` | `INT REFERENCES mitras(id)` | Unique — one row per mitra | | `is_online` | `BOOLEAN DEFAULT false` | | | `last_online_at` | `TIMESTAMPTZ` | Last time they went online | | `last_offline_at` | `TIMESTAMPTZ` | Last time they went offline | | `updated_at` | `TIMESTAMPTZ` | | ### 1.2 New table: `mitra_online_logs` Append-only log for control center reporting. | Column | Type | Notes | |---|---|---| | `id` | `SERIAL PRIMARY KEY` | | | `mitra_id` | `INT REFERENCES mitras(id)` | | | `status` | `VARCHAR` | `'online'` or `'offline'` | | `timestamp` | `TIMESTAMPTZ DEFAULT now()` | | ### 1.3 New table: `chat_sessions` Tracks pairing and session lifecycle. | Column | Type | Notes | |---|---|---| | `id` | `SERIAL PRIMARY KEY` | | | `customer_id` | `INT REFERENCES customers(id)` | | | `mitra_id` | `INT REFERENCES mitras(id)` | Nullable — null while searching | | `status` | `VARCHAR` | See statuses below | | `created_at` | `TIMESTAMPTZ DEFAULT now()` | | | `paired_at` | `TIMESTAMPTZ` | When mitra accepted | | `ended_at` | `TIMESTAMPTZ` | When session explicitly ended | | `ended_by` | `VARCHAR` | `'customer'`, `'mitra'`, or `'system'` | **Session statuses:** - `searching` — customer requested, waiting for mitra - `pending_acceptance` — blast sent, waiting for a mitra to accept - `pending_payment` — mitra accepted, payment placeholder (auto-skip for now) - `active` — session in progress - `completed` — ended explicitly - `cancelled` — customer cancelled during search - `expired` — 60s timeout, no mitra accepted - `rerouted` — session reassigned by control center (old record status) ### 1.4 New table: `chat_request_notifications` Tracks which mitras were notified of a pairing request (for blast tracking). | Column | Type | Notes | |---|---|---| | `id` | `SERIAL PRIMARY KEY` | | | `session_id` | `INT REFERENCES chat_sessions(id)` | | | `mitra_id` | `INT REFERENCES mitras(id)` | | | `notified_at` | `TIMESTAMPTZ DEFAULT now()` | | | `response` | `VARCHAR` | `'accepted'`, `'declined'`, `'ignored'` | | `responded_at` | `TIMESTAMPTZ` | | ### 1.5 Extend `app_config` Add new config key: | Key | Value (JSONB) | Purpose | |---|---|---| | `max_customers_per_mitra` | `{ "value": 3 }` | Global max concurrent sessions per mitra | --- ## 2. Backend Changes ### 2.1 Valkey (Redis-compatible) Setup - Add Valkey client as Fastify plugin (`src/plugins/valkey.js`) - Used for pub/sub channels: - `mitra:{mitra_id}:requests` — notify specific mitra of incoming chat requests - `session:{session_id}:status` — notify customer of session status changes - This pub/sub infrastructure will be reused for real-time chat in the next phase ### 2.2 New Public Routes — Mitra | Method | Path | Purpose | |---|---|---| | `POST` | `/api/mitra/status/online` | Set mitra online | | `POST` | `/api/mitra/status/offline` | Set mitra offline | | `GET` | `/api/mitra/status` | Get own current status | | `GET` | `/api/mitra/chat-requests/incoming` | SSE/long-poll endpoint for incoming chat request notifications | | `POST` | `/api/mitra/chat-requests/:sessionId/accept` | Accept a chat request | | `POST` | `/api/mitra/chat-requests/:sessionId/decline` | Decline a chat request | | `GET` | `/api/mitra/sessions/active` | Get mitra's active sessions | | `POST` | `/api/mitra/sessions/:sessionId/end` | End a session | ### 2.3 New Public Routes — Client | Method | Path | Purpose | |---|---|---| | `POST` | `/api/client/chat/request` | Start a pairing request | | `GET` | `/api/client/chat/request/:sessionId/status` | SSE/long-poll for pairing status updates | | `POST` | `/api/client/chat/request/:sessionId/cancel` | Cancel a pairing request | | `GET` | `/api/client/sessions/active` | Get customer's active session | | `POST` | `/api/client/sessions/:sessionId/end` | End a session | ### 2.4 New Internal Routes — Control Center | Method | Path | Purpose | |---|---|---| | `GET` | `/internal/dashboard/stats` | Active chats, active mitras, active requests, customers per mitra | | `GET` | `/internal/config/max-customers-per-mitra` | Get current max config | | `PATCH` | `/internal/config/max-customers-per-mitra` | Update max config | | `GET` | `/internal/sessions` | List sessions (filterable by status) | | `GET` | `/internal/sessions/:sessionId` | Session detail | | `POST` | `/internal/sessions/:sessionId/reroute` | Reroute session to another mitra (forced) | | `GET` | `/internal/mitras/online` | List online mitras with active session count | | `GET` | `/internal/mitras/:mitraId/online-logs` | Online/offline log for a mitra | ### 2.5 New Services | Service | Responsibilities | |---|---| | `mitra-status.service.js` | Toggle online/offline, log entries, auto-offline logic | | `pairing.service.js` | Create pairing request, blast to mitras, handle accept/decline, timeout (60s), cancel | | `session.service.js` | Session lifecycle (active, end, reroute), active session queries | | `dashboard.service.js` | Aggregate stats for control center dashboard | ### 2.6 Pairing Flow (Backend Detail) ``` 1. Customer calls POST /api/client/chat/request 2. Backend creates chat_session (status: searching) 3. Backend queries available mitras: - is_online = true - active_session_count < max_customers_per_mitra config 4. If no mitra available → immediately return "no bestie available" 5. If mitras available: - Update session status → pending_acceptance - Create chat_request_notifications for each available mitra - Publish to each mitra's Valkey channel - Start 60s timeout timer (via setTimeout or scheduled job) 6. Mitra calls POST /api/mitra/chat-requests/:sessionId/accept - Check session still in pending_acceptance (first-come-first-served) - If yes → pair, set session status → pending_payment → active (skip payment) - Publish status update to customer's session channel - Mark other mitra notifications as "ignored" 7. On 60s timeout with no accept: - Set session status → expired - Publish timeout to customer's session channel 8. On customer cancel: - Set session status → cancelled - Notify mitras to dismiss the request ``` ### 2.7 Auto-Offline Mechanism - Mitra app sends periodic heartbeat (`POST /api/mitra/status/heartbeat`) every 15 seconds - Backend tracks last heartbeat timestamp in `mitra_online_status` - A background interval (every 30s) checks for mitras with `is_online = true` but no heartbeat in the last 45 seconds → auto-set offline - This handles force-close, network loss, and crashes --- ## 3. Mitra App Changes ### 3.1 New BLoC: `StatusBloc` Manages online/offline toggle and heartbeat. **Events:** `ToggleOnline`, `ToggleOffline`, `HeartbeatTick`, `AutoOfflineDetected` **States:** `StatusInitial`, `StatusOnline`, `StatusOffline`, `StatusLoading`, `StatusError` - On `ToggleOnline` → call API, start heartbeat timer (15s interval) - On `ToggleOffline` → call API, stop heartbeat timer - On app lifecycle `paused`/`detached` → call offline API, stop heartbeat ### 3.2 New BLoC: `ChatRequestBloc` Handles incoming chat request notifications. **Events:** `StartListening`, `StopListening`, `RequestReceived`, `AcceptRequest`, `DeclineRequest` **States:** `Idle`, `IncomingRequest(session)`, `Accepting`, `Accepted(session)`, `Declined`, `Error` - Listens to SSE/long-poll endpoint for incoming requests while mitra is online - Shows incoming request UI overlay/bottom sheet ### 3.3 Screen Changes | Screen | Changes | |---|---| | Home screen | Add online/offline toggle switch at the top; show active session count | | New: Incoming request sheet | Bottom sheet showing customer request with Accept/Decline buttons | | New: Active sessions screen | List of current active sessions (name, duration) with End button | ### 3.4 App Lifecycle Handling - Use `WidgetsBindingObserver` to detect app going to background/foreground - On `paused`/`detached` → call offline API - On `resumed` → do NOT auto-set online; mitra must explicitly toggle --- ## 4. Client App Changes ### 4.1 New BLoC: `PairingBloc` Manages the pairing request lifecycle. **Events:** `RequestPairing`, `CancelPairing`, `PairingStatusUpdate`, `PairingTimeout` **States:** `PairingInitial`, `Searching`, `BestieFound(mitraName)`, `Paired(session)`, `NoBestieAvailable`, `Cancelled`, `Error` - On `RequestPairing` → call API, start listening for status updates, start 60s local timer - On status update `active` → emit `BestieFound` briefly, then `Paired` - On timeout / expired → emit `NoBestieAvailable` ### 4.2 Screen Changes | Screen | Changes | |---|---| | Home screen | Add "Mulai Curhat" CTA button (disabled if already has active session) | | New: Searching screen | "Searching for Bestie..." with animation and Cancel button | | New: Bestie found screen | Brief "Bestie ditemukan, menghubungkan kamu ke Bestie" message | | New: Session active screen | Shows paired bestie name, End Session button | | New: No bestie screen | "No bestie available, try again later" with retry button | ### 4.3 Navigation Updates Add new routes in GoRouter: - `/chat/searching` — searching screen - `/chat/session/:sessionId` — active session screen --- ## 5. Control Center Changes ### 5.1 New Pages | Page | Purpose | |---|---| | Dashboard page | Real-time stats: active chats, online mitras, pending requests, customers per mitra breakdown | | Session management page | Table of all sessions (filterable by status), reroute action | | Session detail page | Session info, customer/mitra details, reroute button | ### 5.2 Updated Pages | Page | Changes | |---|---| | Settings page | Add "Max customers per mitra" config input | | Mitra management page | Add online status indicator column, online/offline log link | ### 5.3 Dashboard Auto-Refresh - Use React Query with `refetchInterval: 10000` (10s polling) - Later can be swapped to SSE push without changing component logic --- ## 6. Implementation Order Work should be done in this sequence to allow incremental testing: | Step | What | Apps affected | |---|---|---| | 1 | Database migration (new tables + config) | Backend | | 2 | Valkey plugin setup | Backend | | 3 | Mitra online/offline status API + service + heartbeat | Backend | | 4 | Mitra app: status toggle + heartbeat + lifecycle handling | Mitra app | | 5 | Control center: max config + mitra online status column + logs | Control center | | 6 | Pairing service + chat request APIs (mitra + client) | Backend | | 7 | Client app: pairing flow (request, searching, found, paired screens) | Client app | | 8 | Mitra app: incoming request notification + accept/decline | Mitra app | | 9 | Session management APIs (end, reroute, list) | Backend | | 10 | Client/mitra app: active session screen + end session | Both apps | | 11 | Control center: dashboard + session management + reroute | Control center | --- ## 7. New Dependencies | App | Package | Purpose | |---|---|---| | Backend | `ioredis` or `@valkey/valkey-glide` | Valkey/Redis client for pub/sub | | Mitra app | `dio` (existing) | SSE via streaming response | | Client app | `dio` (existing) | SSE via streaming response | No major new dependencies needed — leveraging existing stack. --- ## 8. Note for Next Phase **Valkey vs FCM — complementary, not competing:** - **Valkey pub/sub** is the real-time transport for in-app events (pairing blasts, status updates, and later chat messages). Works when the app is in foreground. - **FCM (Firebase Cloud Messaging)** should be added in the next phase for push notifications when the app is backgrounded/closed (e.g. "New message from customer"). Not needed this phase because mitras auto-go offline when the app is backgrounded — they only receive pairing requests while in foreground. - Architecture: Valkey for real-time transport, FCM for push notifications. They layer on top of each other.