16 · Live Monitor, Admin Workflow
The Live Monitor is the section of the ops panel (ops.pasarseken.id) where admins watch active live streams, track engagement in real-time, and take moderation actions.
Stream List Flow
LiveMonitor.tsx (React)
└─ liveStreamAdminApi.getAll(token)
└─ GET /admin/live-streams
├─ SELECT * FROM live_streams (status != "ended")
├─ batch: SELECT stream_id FROM live_chat_messages IN (streamIds)
└─ map DB shape → LiveStream interface
The DB stores seller_username / seller_id / seller_avatar / duration_minutes. The React interface uses host_name / host_id / host_avatar / duration_min. A mapping layer in routes-admin-extended.tsx translates between the two before the JSON response is emitted. Message count is batch-joined from live_chat_messages, no N+1 queries.
Viewer Tracking
POST /live-streams/:id/view → liveStreamRepo.incrementViewers(id)
POST /live-streams/:id/leave → liveStreamRepo.decrementViewers(id)
Both use atomic DB-level arithmetic (UPDATE live_streams SET viewers = viewers + 1 ...). This eliminates the race condition that existed when two viewers joined at the same moment and both read the same stale count.
Live Chat
POST /live-streams/:id/chat
→ liveStreamRepo.insertChatMessage(id, { user_id, username, text })
GET /live-streams/:id/chat?after=<timestamp>
→ liveStreamRepo.findChatByStreamId(id, after)
→ SELECT * FROM live_chat_messages WHERE stream_id = ? AND created_at > after
ORDER BY created_at DESC LIMIT 100
Chat messages are stored in live_chat_messages (relational). The KV-based chat path was retired in Phase 14.
Admin Actions
End Stream
PUT /admin/live-streams/:streamId/end
Patches status = "ended", ended_at = now(), duration_minutes = elapsed. React applies an optimistic update and rolls back on error.
Ban from Live
PUT /admin/live-streams/:streamId/ban
- Ends the stream (same as End Stream).
- Sets
user_profiles.live_banned = trueandlive_banned_at = now()for the seller.
Enforcement
When a seller attempts POST /live-streams, the creation handler checks profile.live_banned. If true, it returns HTTP 403 with the Indonesian message:
"Akunmu tidak bisa melakukan live stream. Hubungi support SEKEN."
The live_banned column is defined in migration 20260512000050_live_ban_column.sql:
ALTER TABLE user_profiles
ADD COLUMN IF NOT EXISTS live_banned BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS live_banned_at TIMESTAMPTZ;
Status Values
Valid live_streams.status values: "live", "upcoming", "ended". No other values are written to the DB.
Repository Methods
All in SupabaseLiveStreamRepository.ts:
| Method | Route |
|---|---|
findAll() | GET /admin/live-streams |
incrementViewers(id) | POST /live-streams/:id/view |
decrementViewers(id) | POST /live-streams/:id/leave |
insertChatMessage(id, msg) | POST /live-streams/:id/chat |
findChatByStreamId(id, after) | GET /live-streams/:id/chat |
Admin write operations (end, ban) call getAdminClient() directly in routes-admin-extended.tsx for service-role access.
Key Files
| File | Role |
|---|---|
src/app/components/admin/LiveMonitor.tsx | React ops panel component |
src/app/lib/api-admin.ts | liveStreamAdminApi client |
supabase/functions/server/interface/http/routes-admin-extended.tsx | Admin list + action endpoints |
supabase/functions/server/interface/http/routes-misc.tsx | Creation guard + viewer/chat routes |
supabase/functions/server/infrastructure/repositories/SupabaseLiveStreamRepository.ts | All DB access |
supabase/migrations/20260512000050_live_ban_column.sql | live_banned schema |
Sprint PS-354→PS-358 · 2026-05-12