Skip to content

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_viewers threshold 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/:submissionId

Nested 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/deals with 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:

TermRequiredActualStatus
Min viewers200avg 317
Min duration120 min148 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_days are 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_viewers from 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:

  1. Extract the channel slug from stream_url (everything after kick.com/ stripped of query params and clip suffixes)
  2. Look up streamer_channels where channel_name = slug → get kick_user_id
  3. Query kick_stream_sessions where kick_user_id = ? and the session window overlaps the submission's submitted_at (within ±24 h, prefer latest ended session before submitted_at)

This matching is done server-side in a new API endpoint.

New API endpoint

GET /api/session-submissions/:id/stream-data

Response shape:

jsonc
{
  "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:

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:

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:

tsx
{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

FileChange
frontend/src/pages/operator/StreamDeliveryPage.tsxNew page
frontend/src/main.tsxAdd route operator/deals/:dealId/stream/:submissionId
frontend/src/pages/operator/DealsPage.tsxAdd "Stream data" link in submission row
frontend/src/api/applications.tsAdd getSubmissionStreamData()
frontend/src/api/types.tsAdd SubmissionStreamData interface
api/src/routes/submissions.tsNew handler: GET /api/session-submissions/:id/stream-data

Operator-specific constraints (differs from admin)

Admin StreamsPanelOperator StreamDeliveryPage
Browses all sessions cross-streamerSingle session scoped to one deal slot
No contract contextContract compliance panel: min_viewers / min_duration check
Confirm / Reject actions absentConfirm & Release / Reject actions present
Can trigger backfill, sync, etc.Read-only; no admin controls
Grouped by broadcasterNo 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

Verifluence Documentation