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>
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 mitrapending_acceptance— blast sent, waiting for a mitra to acceptpending_payment— mitra accepted, payment placeholder (auto-skip for now)active— session in progresscompleted— ended explicitlycancelled— customer cancelled during searchexpired— 60s timeout, no mitra acceptedrerouted— 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 requestssession:{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 = truebut 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
WidgetsBindingObserverto 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→ emitBestieFoundbriefly, thenPaired - 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.