Files
halobestie-clone/requirement/phase2-plan.md
ramadhan sjamsani d668112edd Phase 2 scaffold: mitra online status & pairing logic
Add mitra online/offline status with heartbeat-based auto-offline,
customer-mitra pairing via Valkey pub/sub blast, session management,
and control center dashboard with real-time stats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:17:49 +08:00

13 KiB

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.