Appearance
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:
- Budget pre-check:
getCampaignSpend(campaignId)→ ifpercentageUsed >= 100→ 409BUDGET_FULL
Side Effects (in order):
prisma.submission.update:status=ACCEPTED,acceptedAt(or existing),nextPollAt=now- Budget clamp on accept (
admin.ts:1537-1572):getCampaignSpend→ remaining budget- If
currentViewswould exceed remaining →frozenViewCount = maxViews campaign.update:acceptingSubmissions=false,viewsFrozen=true,viewsFrozenAt=now
- Auto-close campaign (
checkAndCloseCampaign): if ≥95% budget →acceptingSubmissions=false - User notification:
UserNotification - Audit log:
AdminAuditLog
Tracking: Starts immediately (nextPollAt=now → picked up by next 30-min tick)
DENIED
Side Effects:
prisma.submission.update:status=DENIED- Audit log:
SUBMISSION_DENIEDwith reason
No tracking started. No budget impact. No user notification (in-app only).
FLAGGED
Side Effects:
prisma.submission.update:status=FLAGGED- Audit log:
SUBMISSION_FLAGGEDwith reason
Tracking: Not started (or stopped if previously accepted)
PENDING (Re-open)
Side Effects:
prisma.submission.update:status=PENDING- 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:
consecutiveScrapeFailures++- If
>= 3:status=FLAGGEDtrackingStoppedAt=nownextPollAt=nullstats.flagged++- Console warn log
No audit log entry (system action, not admin)
Reason Popover (Frontend)
Component: SubmissionReviewRow.tsx → ReasonPopover (lines 199-300)
Flow:
- Admin clicks Flag/Deny icon →
setReasonPopover({submissionId, status}) - Popover shows textarea + Submit button
- On submit →
submitStatus(submissionId, status, reason) - 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:
- Find target user (by discordId or webUserId)
- Create
Banrecord (active, reason, optional expiresAt) - Deactivate existing bans for user
- Add identifiers to
IdentityBlacklist:- WEB_USER_ID, DISCORD_ID, EMAIL, PHONE, IPs, provider IDs
- Update WebUser (implicit via blacklist)
Triggered from: AdminSubmissionsBanModal → submitAdminSubmissionsBan
Notifications on Review
| Event | Notification Type | Recipient | Channel |
|---|---|---|---|
| Submission accepted | SUBMISSION_ACCEPTED | Clipper | In-app (UserNotification) |
| Submission denied | — | — | None |
| Submission flagged | — | — | None |
| Payout sent | PAYOUT_SENT | Clipper | In-app + email (if opted in) |
| Payout rejected | PAYOUT_REJECTED | Clipper | In-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:
- User requests payout →
Payout(REQUESTED) → async rescrape →READY_FOR_REVIEW - Admin reviews at
/dashboard/admin/payouts→AdminPayoutReviewscreen - Per-item decisions:
POST /api/admin/payouts/review/:id/items/:itemId - Bulk approve unflagged:
POST /api/admin/payouts/review/:id/approve-unflagged - Approve payout:
POST /api/admin/payouts/review/:id/approve(bookkeeping, tax snapshot, affiliate sweep) →AWAITING_SEND - Send payout:
POST /api/admin/payouts/review/:id/send(TOTP) → dispatch to rail →COMPLETED
Side effects on payout approve (per item):
REJECTEDitem → submissionDENIED,trackingStoppedAtFLAGGEDitem → submissionFLAGGED,trackingStoppedAtAPPROVEDitem →paidViewsTotal++,paidAmountTotal++,lastPaidAt
Missing / Incomplete Review Features
| Feature | Status | Notes |
|---|---|---|
| Bulk accept/deny | Not implemented | Only per-row |
| Re-review workflow | Manual | Admin must change status back to PENDING |
| Rejection reason on submission | Not stored | Only in audit log |
| Notify clipper on deny/flag | Not implemented | Only accept notifies |
| Appeal process | Not implemented | No UI or endpoint |
| SLA/timer for review | Not implemented | No aging metrics |
State Transition Table
| From | Trigger | Actor | To | Code | Side Effects |
|---|---|---|---|---|---|
| PENDING | Accept | Admin | ACCEPTED | admin.ts:1518 | tracking start, budget clamp, notify, audit |
| PENDING | Deny | Admin | DENIED | admin.ts:1528 | audit only |
| PENDING | Flag | Admin | FLAGGED | admin.ts:1528 | audit only |
| ACCEPTED | Deny | Admin | DENIED | admin.ts:1528 | audit only |
| ACCEPTED | Flag | Admin | FLAGGED | admin.ts:1528 | audit only |
| ACCEPTED | 3 failures | System | FLAGGED | runTrackingTick.ts:131 | tracking stopped, no audit |
| FLAGGED | Re-open | Admin | PENDING | admin.ts:1528 | audit only |
| DENIED | Re-open | Admin | PENDING | admin.ts:1528 | audit only |
| ANY | Manual view update | Admin | (same) | admin.ts:1308 | audit only |
| ANY | Custom rate update | Admin | (same) | admin.ts:1347 | audit only |
| ANY | Custom cap update | Admin | (same) | admin.ts:1406 | audit 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