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:
2026-04-05 23:17:49 +08:00
parent a7a2a32d27
commit d668112edd
44 changed files with 2800 additions and 80 deletions

View 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
}