Lewati ke konten utama

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

  1. Ends the stream (same as End Stream).
  2. Sets user_profiles.live_banned = true and live_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:

MethodRoute
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

FileRole
src/app/components/admin/LiveMonitor.tsxReact ops panel component
src/app/lib/api-admin.tsliveStreamAdminApi client
supabase/functions/server/interface/http/routes-admin-extended.tsxAdmin list + action endpoints
supabase/functions/server/interface/http/routes-misc.tsxCreation guard + viewer/chat routes
supabase/functions/server/infrastructure/repositories/SupabaseLiveStreamRepository.tsAll DB access
supabase/migrations/20260512000050_live_ban_column.sqllive_banned schema

Sprint PS-354→PS-358 · 2026-05-12