Proposal: Operator Stream Delivery Detail Page
Problem
When a streamer submits a delivery proof for an allocated deal slot, the operator receives a stream_url and a status badge (pending_review / confirmed / rejected). That is all they see. There is no way for the operator to verify inside the app:
- Did the stream actually meet the
min_viewersthreshold agreed in the deal? - Did it run long enough to satisfy
min_duration? - What did the viewer curve look like — sustained peak or a brief spike?
- How engaged was the chat audience?
The admin panel already captures and displays exactly this data (per-minute probe points, stat strip, ApexChart time-series) but it is not scoped to a deal or accessible to operators.
Goal
Add a read-only Stream Delivery Detail page for operators that shows the platform-tracked metrics for a specific submission, contextualised against the deal's agreed terms.
New route
/operator/deals/:dealId/stream/:submissionIdNested under the existing /operator layout (shares the OperatorLayout sidebar). Entry point: a "View stream data ↗" link added to each confirmed/pending submission row in DealsPage.tsx.
Page anatomy
1. Header strip
← Deal #42 · @opslotos · Slot 2 / 4
[ Confirm & release ] [ Reject ]- Back link to
/operator/dealswith the deal ID pre-selected - Streamer username + slot ordinal from the parent deal
- Confirm/Reject actions (same logic already in
DealsPage) surfaced here so the operator can decide after reviewing the data without going back
2. Contract compliance panel
Side-by-side pill check for each measurable term in the agreed deal:
| Term | Required | Actual | Status |
|---|---|---|---|
| Min viewers | 200 | avg 317 | ✅ |
| Min duration | 120 min | 148 min | ✅ |
- Green
✅when the actual value meets or exceeds the requirement - Red
✗when it falls short - Amber
~when the stream session could not be matched (no tracking data) payment_per_stream,deliveries,timeframe_daysare not shown here — they are contract-level terms, not per-stream verifiable metrics
3. Stat strip
Four metric cards in a horizontal row (mirrors admin StreamsPanel):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Avg CCU │ │ Peak │ │ Duration │ │ Chatter │
│ 317 │ │ 512 │ │ 2h 28m │ │ ratio │
│ viewers │ │ viewers │ │ │ │ 14 % │
└──────────┘ └──────────┘ └──────────┘ └──────────┘Values sourced from the matched kick_stream_sessions row.
4. Viewer time-series chart
ReactApexChart area chart (same library already used in admin StreamsPanel):
- Primary series — viewer_count per minute (solid teal area)
- Secondary series — chatter_count per minute (dashed purple line, secondary Y-axis)
- X-axis: wall-clock time (HH:MM)
- Horizontal dashed reference line at
min_viewersfrom the agreed terms - Tooltip shows exact viewer + chatter counts at cursor position
- If no probe data available: empty-state banner "No real-time probe data recorded for this session — only VOD summary stats are available"
5. No-match empty state
If the submission's stream_url cannot be resolved to a kick_stream_sessions row (e.g. streamer was not tracked at the time, or submitted a VOD link from before platform tracking was enabled):
⚠ No platform tracking data found for this stream.
This can happen when:
• The streamer was not yet subscribed to Kick webhooks at stream time
• The submitted URL points to a VOD from before the tracking window
• The stream started before platform tracking was enabled
You can still review the submitted URL and decide manually.
[ Open stream URL ↗ ]Data flow
Matching submission → stream session
session_submissions has a stream_url field (e.g. https://kick.com/opslotos?clip=… or the live channel URL). To match it to a kick_stream_sessions row:
- Extract the channel slug from
stream_url(everything afterkick.com/stripped of query params and clip suffixes) - Look up
streamer_channelswherechannel_name = slug→ getkick_user_id - Query
kick_stream_sessionswherekick_user_id = ?and the session window overlaps the submission'ssubmitted_at(within ±24 h, prefer latest ended session beforesubmitted_at)
This matching is done server-side in a new API endpoint.
New API endpoint
GET /api/session-submissions/:id/stream-dataResponse shape:
{
"submission": {
"id": 42,
"offer_id": 101,
"slot_index": 2,
"attempt": 1,
"status": "pending_review",
"stream_url": "https://kick.com/opslotos",
"submitted_at": "2026-04-05T18:30:00Z",
"reviewed_at": null
},
"session": { // null when no match found
"id": 1234,
"stream_started_at": "2026-04-05T15:00:00Z",
"ended_at": "2026-04-05T17:28:00Z",
"avg_viewers": 317,
"peak_viewers": 512,
"avg_chatters": 44,
"measurement_count": 148
},
"probes": [ // [] when no match or data purged
{ "recorded_at": "2026-04-05T15:00:00Z", "viewer_count": 210, "chatter_count": 18, "msg_count": 54 },
…
]
}The existing GET /api/streams/:sessionId/probes endpoint is reused server-side to populate probes.
Frontend API addition
Add to frontend/src/api/applications.ts:
async getSubmissionStreamData(submissionId: number): Promise<SubmissionStreamData> {
const res = await fetch(`${API_BASE}/api/session-submissions/${submissionId}/stream-data`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}New type in frontend/src/api/types.ts:
export interface SubmissionStreamData {
submission: SessionSubmission;
session: StreamSession | null;
probes: StreamProbePoint[];
}(StreamSession and StreamProbePoint are already defined in webhooks.ts.)
DealsPage integration
In the existing submission row in DealsPage.tsx, add a small icon link after the status badge:
{sub.stream_url && (
<Link
to={`/operator/deals/${deal.id}/stream/${sub.id}`}
style={{ display: "inline-flex", alignItems: "center", gap: 4,
fontSize: 11, color: T.accent, textDecoration: "none" }}
>
<VideoCamera className="w-3.5 h-3.5" />
Stream data
</Link>
)}(VideoCamera is already imported in DealsPage.tsx.)
Files to create / modify
| File | Change |
|---|---|
frontend/src/pages/operator/StreamDeliveryPage.tsx | New page |
frontend/src/main.tsx | Add route operator/deals/:dealId/stream/:submissionId |
frontend/src/pages/operator/DealsPage.tsx | Add "Stream data" link in submission row |
frontend/src/api/applications.ts | Add getSubmissionStreamData() |
frontend/src/api/types.ts | Add SubmissionStreamData interface |
api/src/routes/submissions.ts | New handler: GET /api/session-submissions/:id/stream-data |
Operator-specific constraints (differs from admin)
| Admin StreamsPanel | Operator StreamDeliveryPage |
|---|---|
| Browses all sessions cross-streamer | Single session scoped to one deal slot |
| No contract context | Contract compliance panel: min_viewers / min_duration check |
| Confirm / Reject actions absent | Confirm & Release / Reject actions present |
| Can trigger backfill, sync, etc. | Read-only; no admin controls |
| Grouped by broadcaster | No grouping — single submission focus |
Scope estimate
- API: ~60 lines — one new handler, slug extraction util, session match query
- Frontend: ~280 lines — new page component, compliance panel, chart, stat strip
- Plumbing: ~25 lines across
main.tsx,DealsPage.tsx,applications.ts,types.ts - No new dependencies — ApexCharts already installed in
frontend/ - No DB migrations required