Skip to content

Historical snapshot archived 2026-09-25. This records an earlier review or plan, not current implementation or live ticket state. For current work, follow root AGENTS.md, the relevant BloxClips skill, and owning repository source/tests. Preserve approved decisions as evidence; verify their present authority before acting.

Review Flow ​

How staff review submissions in the current implementation.


Review Surfaces ​

1. Global Admin Submissions Queue ​

Route: /dashboard/admin/submissions
Hook: useAdminSubmissions.ts
API: GET /api/admin/submissions/review

2. Campaign-Scoped Review ​

Route: /dashboard/admin/campaigns/[id]
Hook: useAdminCampaignDetail.ts
API: GET /api/admin/campaigns/[id]

Both use the same status update endpoint: PUT /api/admin/submissions/:id/status


Who Can Review ​

Backend: requireAdmin middleware (src/api/middleware/adminAuth.ts)
Frontend: Admin navigation link only shown to admins (adminNavigation.ts)

No separate "moderator" role — only admin/non-admin distinction.


Pending Review Queue ​

Default filter: status=PENDING (admin queue defaults to pending)
Also shows: FLAGGED submissions (separate filter)
Excludes: ACCEPTED, DENIED (unless filter changed to "all")

Columns shown: Video preview, title, campaign, platform, type, views, earnings, status, actions

Actions per row:

  • Accept → submitStatus(id, 'ACCEPTED')
  • Deny → reason popover → submitStatus(id, 'DENIED', reason)
  • Flag → reason popover → submitStatus(id, 'FLAGGED', reason)
  • Ban user → modal → submitBan(userId, reason, duration)

Status Transitions (Verified from Code) ​

ACCEPTED ​

Endpoint: PUT /api/admin/submissions/:id/status (admin.ts:1459)

Pre-checks:

  1. Budget pre-check: getCampaignSpend(campaignId) → if percentageUsed >= 100 → 409 BUDGET_FULL

Side Effects (in order):

  1. prisma.submission.update: status=ACCEPTED, acceptedAt (or existing), nextPollAt=now
  2. Budget clamp on accept (admin.ts:1537-1572):
    • getCampaignSpend → remaining budget
    • If currentViews would exceed remaining → frozenViewCount = maxViews
    • campaign.update: acceptingSubmissions=false, viewsFrozen=true, viewsFrozenAt=now
  3. Auto-close campaign (checkAndCloseCampaign): if ≥95% budget → acceptingSubmissions=false
  4. User notification: UserNotification
  5. Audit log: AdminAuditLog

Tracking: Starts immediately (nextPollAt=now → picked up by next 30-min tick)


DENIED ​

Side Effects:

  1. prisma.submission.update: status=DENIED
  2. Audit log: SUBMISSION_DENIED with reason

No tracking started. No budget impact. No user notification (in-app only).


FLAGGED ​

Side Effects:

  1. prisma.submission.update: status=FLAGGED
  2. Audit log: SUBMISSION_FLAGGED with reason

Tracking: Not started (or stopped if previously accepted)


PENDING (Re-open) ​

Side Effects:

  1. prisma.submission.update: status=PENDING
  2. Audit log: SUBMISSION_PENDING

Use Case: Re-review after deny/flag


Auto-Flag (System-Initiated) ​

Trigger: 3 consecutive scrape failures in tracking tick
Code: runTrackingTick.ts:121-137 → applyFailure

Side Effects:

  1. consecutiveScrapeFailures++
  2. If >= 3:
    • status=FLAGGED
    • trackingStoppedAt=now
    • nextPollAt=null
    • stats.flagged++
    • Console warn log

No audit log entry (system action, not admin)


Reason Popover (Frontend) ​

Component: SubmissionReviewRow.tsx → ReasonPopover (lines 199-300)

Flow:

  1. Admin clicks Flag/Deny icon → setReasonPopover({submissionId, status})
  2. Popover shows textarea + Submit button
  3. On submit → submitStatus(submissionId, status, reason)
  4. Reason passed to backend → stored in audit log only (not on submission)

Backend: Reason only in AdminAuditLog.details.reason — not persisted on Submission model


Ban User (From Review Queue) ​

Endpoint: POST /api/admin/users/:userId/ban (admin.ts:1179)

Flow:

  1. Find target user (by discordId or webUserId)
  2. Create Ban record (active, reason, optional expiresAt)
  3. Deactivate existing bans for user
  4. Add identifiers to IdentityBlacklist:
    • WEB_USER_ID, DISCORD_ID, EMAIL, PHONE, IPs, provider IDs
  5. Update WebUser (implicit via blacklist)

Triggered from: AdminSubmissionsBanModal → submitAdminSubmissionsBan


Notifications on Review ​

EventNotification TypeRecipientChannel
Submission acceptedSUBMISSION_ACCEPTEDClipperIn-app (UserNotification)
Submission denied——None
Submission flagged——None
Payout sentPAYOUT_SENTClipperIn-app + email (if opted in)
Payout rejectedPAYOUT_REJECTEDClipperIn-app + email

Discord: Legacy approval queue removed. No Discord messages on review actions.


Audit Logging ​

Every status change creates AdminAuditLog:

typescript
{
  adminId: req.user.discordId,
  action: `SUBMISSION_${newStatus}`, // ACCEPTED/DENIED/FLAGGED/PENDING
  resourceType: 'SUBMISSION',
  resourceId: String(submissionId),
  details: {
    previousStatus,
    newStatus,
    reason: reason || null,
    campaignId,
    videoLink
  },
  ipAddress,
  userAgent
}

Also logged: View/rate/cap updates (UPDATE_SUBMISSION_VIEWS, UPDATE_SUBMISSION_RATE, UPDATE_SUBMISSION_CAP)


Payout Review (Separate Flow) ​

Not part of submission review — separate admin tab: /dashboard/admin/payouts

Flow:

  1. User requests payout → Payout (REQUESTED) → async rescrape → READY_FOR_REVIEW
  2. Admin reviews at /dashboard/admin/payouts → AdminPayoutReview screen
  3. Per-item decisions: POST /api/admin/payouts/review/:id/items/:itemId
  4. Bulk approve unflagged: POST /api/admin/payouts/review/:id/approve-unflagged
  5. Approve payout: POST /api/admin/payouts/review/:id/approve (bookkeeping, tax snapshot, affiliate sweep) → AWAITING_SEND
  6. Send payout: POST /api/admin/payouts/review/:id/send (TOTP) → dispatch to rail → COMPLETED

Side effects on payout approve (per item):

  • REJECTED item → submission DENIED, trackingStoppedAt
  • FLAGGED item → submission FLAGGED, trackingStoppedAt
  • APPROVED item → paidViewsTotal++, paidAmountTotal++, lastPaidAt

Missing / Incomplete Review Features ​

FeatureStatusNotes
Bulk accept/denyNot implementedOnly per-row
Re-review workflowManualAdmin must change status back to PENDING
Rejection reason on submissionNot storedOnly in audit log
Notify clipper on deny/flagNot implementedOnly accept notifies
Appeal processNot implementedNo UI or endpoint
SLA/timer for reviewNot implementedNo aging metrics

State Transition Table ​

FromTriggerActorToCodeSide Effects
PENDINGAcceptAdminACCEPTEDadmin.ts:1518tracking start, budget clamp, notify, audit
PENDINGDenyAdminDENIEDadmin.ts:1528audit only
PENDINGFlagAdminFLAGGEDadmin.ts:1528audit only
ACCEPTEDDenyAdminDENIEDadmin.ts:1528audit only
ACCEPTEDFlagAdminFLAGGEDadmin.ts:1528audit only
ACCEPTED3 failuresSystemFLAGGEDrunTrackingTick.ts:131tracking stopped, no audit
FLAGGEDRe-openAdminPENDINGadmin.ts:1528audit only
DENIEDRe-openAdminPENDINGadmin.ts:1528audit only
ANYManual view updateAdmin(same)admin.ts:1308audit only
ANYCustom rate updateAdmin(same)admin.ts:1347audit only
ANYCustom cap updateAdmin(same)admin.ts:1406audit only

Mermaid State Diagram ​

mermaid
stateDiagram-v2
    [*] --> PENDING: POST /api/submissions
    PENDING --> ACCEPTED: Admin accept
    PENDING --> DENIED: Admin deny
    PENDING --> FLAGGED: Admin flag
    ACCEPTED --> FLAGGED: Admin flag OR 3 scrape failures
    ACCEPTED --> DENIED: Admin deny (re-review)
    FLAGGED --> PENDING: Admin re-open
    DENIED --> PENDING: Admin re-open
    ACCEPTED --> [*]: trackingStoppedAt (30d / freeze / flag)
    ACCEPTED --> PAID: Delta payout COMPLETED