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>
This commit is contained in:
107
backend/src/services/mitra-status.service.js
Normal file
107
backend/src/services/mitra-status.service.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const ensureStatusRow = async (mitraId) => {
|
||||
await sql`
|
||||
INSERT INTO mitra_online_status (mitra_id)
|
||||
VALUES (${mitraId})
|
||||
ON CONFLICT (mitra_id) DO NOTHING
|
||||
`
|
||||
}
|
||||
|
||||
export const setOnline = async (mitraId) => {
|
||||
await ensureStatusRow(mitraId)
|
||||
const now = new Date()
|
||||
await sql`
|
||||
UPDATE mitra_online_status
|
||||
SET is_online = true, last_online_at = ${now}, last_heartbeat_at = ${now}, updated_at = ${now}
|
||||
WHERE mitra_id = ${mitraId}
|
||||
`
|
||||
await sql`
|
||||
INSERT INTO mitra_online_logs (mitra_id, status) VALUES (${mitraId}, 'online')
|
||||
`
|
||||
}
|
||||
|
||||
export const setOffline = async (mitraId) => {
|
||||
await ensureStatusRow(mitraId)
|
||||
const now = new Date()
|
||||
const [status] = await sql`
|
||||
SELECT is_online FROM mitra_online_status WHERE mitra_id = ${mitraId}
|
||||
`
|
||||
if (!status?.is_online) return
|
||||
|
||||
await sql`
|
||||
UPDATE mitra_online_status
|
||||
SET is_online = false, last_offline_at = ${now}, updated_at = ${now}
|
||||
WHERE mitra_id = ${mitraId}
|
||||
`
|
||||
await sql`
|
||||
INSERT INTO mitra_online_logs (mitra_id, status) VALUES (${mitraId}, 'offline')
|
||||
`
|
||||
}
|
||||
|
||||
export const heartbeat = async (mitraId) => {
|
||||
const now = new Date()
|
||||
await sql`
|
||||
UPDATE mitra_online_status
|
||||
SET last_heartbeat_at = ${now}, updated_at = ${now}
|
||||
WHERE mitra_id = ${mitraId} AND is_online = true
|
||||
`
|
||||
}
|
||||
|
||||
export const getStatus = async (mitraId) => {
|
||||
await ensureStatusRow(mitraId)
|
||||
const [status] = await sql`
|
||||
SELECT is_online, last_online_at, last_offline_at, updated_at
|
||||
FROM mitra_online_status
|
||||
WHERE mitra_id = ${mitraId}
|
||||
`
|
||||
return status
|
||||
}
|
||||
|
||||
export const getOnlineMitras = async () => {
|
||||
const mitras = await sql`
|
||||
SELECT m.id, m.display_name, m.phone, s.last_online_at, s.updated_at,
|
||||
(SELECT COUNT(*) FROM chat_sessions cs
|
||||
WHERE cs.mitra_id = m.id AND cs.status IN ('active', 'pending_payment')) AS active_session_count
|
||||
FROM mitras m
|
||||
INNER JOIN mitra_online_status s ON s.mitra_id = m.id
|
||||
WHERE s.is_online = true AND m.is_active = true
|
||||
ORDER BY s.last_online_at DESC
|
||||
`
|
||||
return mitras
|
||||
}
|
||||
|
||||
export const getOnlineLogs = async (mitraId, { page = 1, limit = 50 } = {}) => {
|
||||
const offset = (page - 1) * limit
|
||||
const items = await sql`
|
||||
SELECT id, status, timestamp
|
||||
FROM mitra_online_logs
|
||||
WHERE mitra_id = ${mitraId}
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
const [{ count }] = await sql`
|
||||
SELECT COUNT(*) FROM mitra_online_logs WHERE mitra_id = ${mitraId}
|
||||
`
|
||||
return { items, total: Number(count), page, limit }
|
||||
}
|
||||
|
||||
export const autoOfflineStaleMitras = async (staleSeconds = 45) => {
|
||||
const stale = await sql`
|
||||
UPDATE mitra_online_status
|
||||
SET is_online = false, last_offline_at = NOW(), updated_at = NOW()
|
||||
WHERE is_online = true
|
||||
AND last_heartbeat_at < NOW() - ${staleSeconds + ' seconds'}::interval
|
||||
RETURNING mitra_id
|
||||
`
|
||||
|
||||
for (const row of stale) {
|
||||
await sql`
|
||||
INSERT INTO mitra_online_logs (mitra_id, status) VALUES (${row.mitra_id}, 'offline')
|
||||
`
|
||||
}
|
||||
|
||||
return stale.length
|
||||
}
|
||||
Reference in New Issue
Block a user