changemaker.lite/docs/MEDIA_ADMIN_FEATURES.md

22 KiB

Media Admin Features - Complete Guide

Overview

The Video Admin Features system provides comprehensive content management capabilities for the Changemaker Lite video library. Implemented in February 2026, this system transforms the basic CRUD interface into a professional-grade video management platform with quick actions, scheduled publishing, and detailed analytics.

Table of Contents

  1. Quick Action Buttons
  2. Scheduled Publishing
  3. Video Analytics
  4. Architecture
  5. API Reference
  6. Security

Quick Action Buttons

Overview

Quick action buttons appear on video cards when hovering, providing instant access to common operations without navigating away from the library view.

Features

Primary Actions:

  • Edit (E) - Modify video metadata, title, producer, creator
  • Preview (P) - Watch video with full analytics tracking
  • Analytics (A) - View quick statistics modal
  • Schedule (S) - Set publish/unpublish times

Secondary Actions (Overflow Menu):

  • Duplicate - Clone video with new title
  • Generate Preview Link - Create expiring share link (24h)
  • Download - Download original video file (coming soon)
  • Generate Thumbnail - Auto-generate thumbnail from video (coming soon)
  • Reset Analytics - Clear all view data and statistics
  • Delete - Permanently remove video

Keyboard Shortcuts

Quick actions support keyboard shortcuts when the library page is focused:

Key Action
E Edit video
P Preview video
A Show analytics
S Schedule publishing

Note: Shortcuts are disabled when typing in input fields or text areas.

Preview links allow sharing videos with external parties without requiring authentication.

Characteristics:

  • JWT-based token authentication
  • 24-hour expiration (configurable via VIDEO_PREVIEW_LINK_EXPIRY_HOURS)
  • Automatically copied to clipboard
  • Single-use tracking (each open creates new view)

Generating a Preview Link:

  1. Hover over video card
  2. Click "More Actions" (three dots)
  3. Select "Generate Preview Link"
  4. Link is automatically copied to clipboard
  5. Modal displays link and expiry time

Preview Link Format:

https://media.cmlite.org/api/videos/preview/{token}

Scheduled Publishing

Overview

The scheduled publishing system uses BullMQ job queue with Redis backend to automatically publish/unpublish videos at specific times with timezone support.

Features

Publishing Options:

  • Publish Now - Immediately set video to published state
  • Schedule Publish - Set future publish date/time
  • Schedule Unpublish - Auto-unpublish after period (optional)
  • Timezone Support - 11 common timezones with auto-detection
  • Calendar View - Visual overview of all upcoming schedules

Supported Timezones:

  • UTC (Coordinated Universal Time)
  • EST (Eastern Standard Time)
  • CST (Central Standard Time)
  • MST (Mountain Standard Time)
  • PST (Pacific Standard Time)
  • Toronto, Vancouver (Canada)
  • GMT (London), CET (Paris)
  • JST (Tokyo), AEDT (Sydney)

Using Scheduled Publishing

Schedule a Video:

  1. Click Schedule button on video card (clock icon)
  2. Toggle "Publish immediately" off
  3. Select timezone (defaults to your system timezone)
  4. Choose publish date/time
  5. Optionally enable "Auto-unpublish after period"
  6. Click "Schedule" button

View Scheduled Videos:

  1. Click "View Calendar" button in LibraryPage header
  2. Calendar shows badge counts for days with scheduled events
  3. Click a date to view scheduled publish/unpublish events
  4. Cancel individual schedules from the list

Schedule Badge:

  • Appears on video cards when publish/unpublish is scheduled
  • Shows time until scheduled action
  • Clock icon with color coding:
    • Green: Scheduled to publish
    • Orange: Scheduled to unpublish
    • Red: Overdue (failed to execute)

BullMQ Job Queue

Queue Configuration:

  • Queue name: video-schedules
  • Redis connection: Shared with email queue
  • Job retry: 3 attempts with exponential backoff
  • Job timeout: 30 seconds

Job Processing:

  1. Schedule created → Job added to queue with delayed timestamp
  2. Redis stores job until scheduled time
  3. Worker picks up job at scheduled time
  4. Updates video isPublished field
  5. Records execution in VideoScheduleHistory
  6. Removes job from queue

Monitoring: See MediaJobsPage at /app/media/jobs for queue monitoring (coming soon).


Video Analytics

Overview

Comprehensive analytics tracking system that records views, watch time, user engagement, and traffic sources with privacy-focused design.

Metrics Tracked

Overview Statistics:

  • Total Views - Number of times video was played
  • Unique Viewers - Deduplicated viewers (by IP hash or user ID)
  • Average Watch Time - Mean watch duration across all views
  • Completion Rate - Percentage of viewers who watched ≥95%
  • Total Watch Time - Cumulative watch time across all views

Per-View Data:

  • IP address (SHA-256 hashed for privacy)
  • User agent (truncated, version numbers removed)
  • Referrer URL (traffic source)
  • User ID (for logged-in users)
  • Watch time in seconds
  • Completion status (boolean)

Event Tracking:

  • Play - Video started/resumed
  • Pause - Video paused
  • Seek - User skipped forward/backward (with timestamp)
  • Complete - Video watched to 95%+

Privacy & GDPR Compliance

Data Protection Measures:

  1. IP Address Hashing - All IP addresses hashed with SHA-256 before storage
  2. User Agent Truncation - Version numbers and detailed info removed
  3. Anonymous Aggregation - Anonymous views separated from registered users
  4. 90-Day Retention - Configurable data retention policy (default: 90 days)
  5. Do Not Track - Respects DNT header (optional)
  6. User Opt-Out - Users can disable analytics tracking in settings

Compliance Features:

  • No personally identifiable information (PII) stored for anonymous users
  • Registered user tracking requires explicit consent
  • GDPR Article 17 "Right to be forgotten" via reset analytics
  • Transparent data collection disclosure

Analytics Dashboard

Quick Analytics Modal (accessible from video card):

  • Overview stats (4 cards)
  • Top referrers (up to 5 sources)
  • Recent registered viewers (up to 10)

Detailed Analytics Modal (click Analytics button):

  • Overview Tab:

    • 4 overview stat cards
    • Total watch time card
    • Top referrers table (sortable)
  • Charts Tab:

    • Views over time (area chart, last 30 days)
    • Traffic sources distribution (pie chart)
  • Viewers Tab:

    • Full registered viewers table
    • Sortable by watch time, completion status
    • Filter by completed/partial views

Global Analytics Dashboard (/app/media/analytics):

  • Platform-wide statistics (total videos, views, watch time)
  • Average completion rate across all videos
  • Top 10 videos by views or watch time (switchable)
  • Ranking system with medal icons (🥇🥈🥉)

Tracking Implementation

Client-Side Tracking:

// Record view when modal opens
await mediaApi.post('/track/view', {
  videoId: video.id,
  referer: document.referrer || undefined,
});

// Record events
await mediaApi.post('/track/event', {
  videoId: video.id,
  viewId: viewId,
  eventType: 'play', // or 'pause', 'seek', 'complete'
  timestamp: videoElement.currentTime,
});

// Heartbeat every 10 seconds
setInterval(() => {
  navigator.sendBeacon(
    '/api/track/heartbeat',
    JSON.stringify({ viewId, watchTimeSeconds: currentTime })
  );
}, 10000);

Tracking Endpoints:

  • POST /track/view - Record video view start
  • POST /track/event - Record video event (play, pause, etc.)
  • POST /track/heartbeat - Update watch time (high frequency)
  • POST /track/batch - Batch event submission (coming soon)

Rate Limiting:

  • /track/view: 100 requests/minute per IP
  • /track/event: 100 requests/minute per IP
  • /track/heartbeat: 200 requests/minute per IP (higher for frequent updates)

Analytics Aggregation

Analytics are aggregated in real-time via the VideoAnalyticsService:

class VideoAnalyticsService {
  async aggregateVideoAnalytics(videoId: number) {
    // Aggregate from VideoView and VideoEvent tables
    // Update Video model fields:
    // - uniqueViewers
    // - totalWatchTimeSeconds
    // - averageWatchTimeSeconds
    // - completionRate
  }
}

Aggregation Triggers:

  • After each video view completes
  • On-demand via API endpoint
  • Scheduled batch job (nightly, coming soon)

Architecture

System Design

Technology Stack:

  • Backend: Fastify Media API (port 4100) with Prisma ORM
  • Frontend: React + Ant Design + Zustand
  • Job Queue: BullMQ with Redis
  • Database: PostgreSQL 16 with Prisma migrations
  • Charts: Recharts library

Database Schema

New Models:

model Video {
  // ... existing fields ...

  // Publishing
  scheduledPublishAt   DateTime?
  scheduledUnpublishAt DateTime?

  // Analytics
  uniqueViewers           Int     @default(0)
  totalWatchTimeSeconds   Int     @default(0)
  averageWatchTimeSeconds Decimal @default(0) @db.Decimal(10, 2)
  completionRate          Decimal @default(0) @db.Decimal(5, 2)

  // Relations
  videoViews   VideoView[]
  videoEvents  VideoEvent[]
}

model VideoView {
  id               Int      @id @default(autoincrement())
  videoId          Int
  userId           Int?
  ipAddress        String?  @db.VarChar(45)  // SHA-256 hash
  userAgent        String?  @db.Text
  referer          String?  @db.Text
  watchTimeSeconds Int      @default(0)
  completed        Boolean  @default(false)
  createdAt        DateTime @default(now())

  video            Video    @relation(fields: [videoId], references: [id], onDelete: Cascade)
  user             User?    @relation(fields: [userId], references: [id], onDelete: SetNull)

  @@index([videoId])
  @@index([userId])
  @@index([createdAt])
}

model VideoEvent {
  id         Int      @id @default(autoincrement())
  videoId    Int
  viewId     Int?
  eventType  String   @db.VarChar(50)  // play, pause, seek, complete
  timestamp  Decimal  @db.Decimal(10, 2)  // Video timestamp in seconds
  createdAt  DateTime @default(now())

  video      Video    @relation(fields: [videoId], references: [id], onDelete: Cascade)

  @@index([videoId])
  @@index([viewId])
}

model VideoScheduleHistory {
  id                Int       @id @default(autoincrement())
  videoId           Int
  action            String    @db.VarChar(20)  // 'publish' or 'unpublish'
  scheduledFor      DateTime
  executedAt        DateTime?
  status            String    @db.VarChar(20)  // 'pending', 'completed', 'failed', 'cancelled'
  error             String?   @db.Text
  scheduledByUserId Int

  video             Video     @relation(fields: [videoId], references: [id], onDelete: Cascade)
  scheduledBy       User      @relation(fields: [scheduledByUserId], references: [id])

  @@index([videoId])
  @@index([scheduledFor])
  @@index([status])
}

File Structure

Backend Services:

api/src/modules/media/
├── services/
│   ├── video-analytics.service.ts      # Analytics aggregation
│   └── ffprobe.service.ts              # Video metadata (existing)
├── routes/
│   ├── videos.routes.ts                # Video CRUD (existing)
│   ├── video-actions.routes.ts         # Quick actions (duplicate, preview link, etc.)
│   ├── video-schedule.routes.ts        # Schedule management
│   ├── video-analytics.routes.ts       # Analytics queries (admin)
│   └── video-tracking.routes.ts        # Public tracking endpoints
└── db/
    └── schema.ts                        # Drizzle schema (existing)

api/src/services/
└── video-schedule-queue.service.ts     # BullMQ queue + worker

Frontend Components:

admin/src/components/media/
├── VideoCard.tsx                        # Enhanced with actions overlay
├── VideoActions.tsx                     # Action buttons component
├── QuickAnalyticsModal.tsx             # Quick stats modal
├── SchedulePublishModal.tsx            # Schedule picker with timezone
├── ScheduleCalendarModal.tsx           # Calendar view
├── ScheduleBadge.tsx                   # Schedule status badge
├── VideoAnalyticsModal.tsx             # Detailed analytics (3 tabs)
├── AnalyticsChart.tsx                  # Recharts wrapper
├── ViewersTable.tsx                    # Registered viewers table
└── VideoViewerModal.tsx                # Enhanced with tracking

admin/src/pages/media/
├── LibraryPage.tsx                      # Enhanced with calendar button
└── AnalyticsDashboardPage.tsx          # Global analytics dashboard

API Reference

Quick Actions Endpoints

Duplicate Video

POST /videos/:id/duplicate
Authorization: Bearer {admin_token}

Response:
{
  "id": 123,
  "title": "Original Title (Copy)",
  "filename": "uuid-copy.mp4",
  // ... other video fields
}

Generate Preview Link

GET /videos/:id/preview-link
Authorization: Bearer {admin_token}

Response:
{
  "previewUrl": "https://media.cmlite.org/api/videos/preview/{jwt_token}",
  "expiryHours": 24
}

Reset Analytics

POST /videos/:id/reset-analytics
Authorization: Bearer {admin_token}

Response:
{
  "success": true,
  "message": "Analytics reset successfully"
}

Get Video Analytics

GET /videos/:id/analytics
Authorization: Bearer {admin_token}

Query Params:
  - startDate (optional): ISO date string
  - endDate (optional): ISO date string

Response:
{
  "overview": {
    "totalViews": 1234,
    "uniqueViewers": 567,
    "averageWatchTime": 180.5,
    "completionRate": 67.3,
    "totalWatchTime": 123456
  },
  "topReferrers": [
    { "referer": "google.com", "count": 45 },
    { "referer": "facebook.com", "count": 23 }
  ],
  "registeredViewers": [
    {
      "userId": 1,
      "userName": "John Doe",
      "userEmail": "john@example.com",
      "watchTime": 300,
      "completed": true
    }
  ],
  "viewsOverTime": [
    { "date": "2026-02-01", "count": 12 },
    { "date": "2026-02-02", "count": 18 }
  ]
}

Schedule Management Endpoints

Schedule Publish

POST /videos/:id/schedule-publish
Authorization: Bearer {admin_token}

Body:
{
  "publishAt": "2026-02-20T14:00:00Z",
  "timezone": "America/New_York"
}

Response:
{
  "success": true,
  "jobId": "schedule:publish:123:abc-def",
  "scheduledFor": "2026-02-20T14:00:00Z"
}

Schedule Unpublish

POST /videos/:id/schedule-unpublish
Authorization: Bearer {admin_token}

Body:
{
  "unpublishAt": "2026-03-01T00:00:00Z",
  "timezone": "UTC"
}

Response:
{
  "success": true,
  "jobId": "schedule:unpublish:123:xyz-123",
  "scheduledFor": "2026-03-01T00:00:00Z"
}

Cancel Schedule

DELETE /videos/:id/schedule/:action
Authorization: Bearer {admin_token}

Params:
  - action: 'publish' or 'unpublish'

Response:
{
  "success": true,
  "message": "publish schedule cancelled"
}

Get Upcoming Schedules

GET /videos/schedules/upcoming
Authorization: Bearer {admin_token}

Query Params:
  - limit (optional): default 100

Response:
{
  "schedules": [
    {
      "jobId": "schedule:publish:123:abc",
      "videoId": 123,
      "videoTitle": "My Video",
      "action": "publish",
      "scheduledFor": "2026-02-20T14:00:00Z",
      "status": "pending"
    }
  ]
}

Get Schedule History

GET /videos/:id/schedule-history
Authorization: Bearer {admin_token}

Response:
{
  "history": [
    {
      "id": 1,
      "action": "publish",
      "scheduledFor": "2026-02-15T10:00:00Z",
      "executedAt": "2026-02-15T10:00:03Z",
      "status": "completed",
      "scheduledBy": "admin@example.com"
    }
  ]
}

Analytics Tracking Endpoints (Public)

Record View

POST /track/view
Content-Type: application/json

Body:
{
  "videoId": 123,
  "referer": "https://example.com"  // optional
}

Response:
{
  "viewId": 456
}

Record Event

POST /track/event
Content-Type: application/json

Body:
{
  "videoId": 123,
  "viewId": 456,  // optional
  "eventType": "play",  // 'play', 'pause', 'seek', 'complete'
  "timestamp": 45.5
}

Response:
{
  "success": true
}

Update Watch Time (Heartbeat)

POST /track/heartbeat
Content-Type: application/json

Body:
{
  "viewId": 456,
  "watchTimeSeconds": 120
}

Response:
{
  "success": true
}

Analytics Query Endpoints (Admin)

Get Top Videos

GET /videos/analytics/top
Authorization: Bearer {admin_token}

Query Params:
  - metric: 'views' or 'watchTime'
  - limit: default 10

Response:
{
  "videos": [
    {
      "id": 123,
      "title": "Popular Video",
      "value": 1234  // views or watch time based on metric
    }
  ]
}

Get Analytics Overview

GET /videos/analytics/overview
Authorization: Bearer {admin_token}

Response:
{
  "totalVideos": 50,
  "totalViews": 12345,
  "totalWatchTimeSeconds": 567890,
  "averageCompletionRate": 65.4
}

Security

Authorization

Admin-Only Endpoints: All quick action, schedule management, and analytics query endpoints require admin role:

  • requireAdminRole middleware (SUPER_ADMIN, INFLUENCE_ADMIN, MAP_ADMIN)

Public Tracking Endpoints:

  • No authentication required
  • Rate limited (100-200 req/min per IP)
  • Optional authentication via optionalAuth middleware (tracks user ID if logged in)

Rate Limiting

Tracking Endpoints:

{
  '/track/view': { max: 100, windowMs: 60000 },      // 100/min
  '/track/event': { max: 100, windowMs: 60000 },     // 100/min
  '/track/heartbeat': { max: 200, windowMs: 60000 }, // 200/min (higher for frequent updates)
}

Admin Endpoints:

  • Covered by global admin rate limits (500 req/min)

JWT Token Structure:

{
  videoId: number,
  exp: number,  // 24 hours from generation
  iat: number
}

Validation:

  • JWT signature verification
  • Expiration check (401 if expired)
  • Video existence check (404 if deleted)
  • Single-use recommended (no enforcement yet)

Environment Configuration:

VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24
JWT_ACCESS_SECRET=your_secret_here  # Used for signing preview tokens

Privacy Protection

IP Address Hashing:

import { createHash } from 'crypto';

function hashIpAddress(ipAddress: string): string {
  return createHash('sha256')
    .update(ipAddress)
    .digest('hex');
}

User Agent Truncation:

function truncateUserAgent(userAgent: string): string {
  // Remove version numbers: "Chrome/91.0.4472.124" → "Chrome"
  return userAgent
    .replace(/\/[\d.]+/g, '')
    .substring(0, 200);
}

Data Retention:

// Scheduled cleanup (nightly)
async function cleanupOldAnalytics() {
  const retentionDays = parseInt(process.env.VIDEO_ANALYTICS_RETENTION_DAYS || '90');
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - retentionDays);

  await prisma.videoView.deleteMany({
    where: { createdAt: { lt: cutoffDate } }
  });

  await prisma.videoEvent.deleteMany({
    where: { createdAt: { lt: cutoffDate } }
  });
}

Environment Variables

Add to .env:

# Video Analytics
VIDEO_ANALYTICS_RETENTION_DAYS=90
VIDEO_ANALYTICS_IP_HASHING_ENABLED=true

# Video Scheduling
VIDEO_SCHEDULE_DEFAULT_TIMEZONE=UTC
VIDEO_SCHEDULE_NOTIFICATION_ENABLED=true

# Preview Links
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24

Troubleshooting

Scheduled Publish Not Executing

Check BullMQ Queue:

# View queue status
docker compose exec media-api npm run queue:status

# Retry failed jobs
docker compose exec media-api npm run queue:retry

Check Redis Connection:

docker compose logs redis
docker compose exec redis redis-cli ping

Check Schedule History:

SELECT * FROM video_schedule_history
WHERE status = 'failed'
ORDER BY scheduled_for DESC
LIMIT 10;

Analytics Not Tracking

Check Rate Limits:

# View Redis rate limit keys
docker compose exec redis redis-cli --scan --pattern "rl:*"

# Check remaining requests
docker compose exec redis redis-cli GET "rl:track-view:192.168.1.100"

Check Network Tab:

  • Open browser DevTools → Network
  • Filter by /track/
  • Verify 200 OK responses
  • Check for CORS errors

Verify Tracking Endpoints:

curl -X POST http://localhost:4100/api/track/view \
  -H "Content-Type: application/json" \
  -d '{"videoId": 1}'

Regenerate Link:

  1. Navigate to LibraryPage
  2. Hover over video card
  3. Click "More Actions" → "Generate Preview Link"
  4. New 24-hour link generated

Adjust Expiry Time:

# In .env
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=48  # 2 days

Future Enhancements

Coming Soon:

  1. Download Functionality - Direct video file downloads
  2. Thumbnail Generation - Auto-generate thumbnails from video frames
  3. Batch Event Submission - /track/batch endpoint for bulk events
  4. Scheduled Reports - Email weekly analytics summaries
  5. A/B Testing - Compare multiple video versions
  6. Heatmaps - Visual representation of drop-off points
  7. Export Analytics - CSV/PDF export for reports
  8. Custom Dashboards - User-configurable analytics views

Support

Documentation:

  • Main docs: CLAUDE.md
  • Analytics guide: VIDEO_ANALYTICS_GUIDE.md
  • API architecture: api/src/modules/media/README.md

Issues: Report bugs or request features at: https://github.com/anthropics/changemaker-lite/issues

Questions: Contact the development team or check the wiki for FAQs.