# 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](#quick-action-buttons) 2. [Scheduled Publishing](#scheduled-publishing) 3. [Video Analytics](#video-analytics) 4. [Architecture](#architecture) 5. [API Reference](#api-reference) 6. [Security](#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 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:** ```typescript // 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`: ```typescript 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:** ```prisma 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** ```http 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** ```http GET /videos/:id/preview-link Authorization: Bearer {admin_token} Response: { "previewUrl": "https://media.cmlite.org/api/videos/preview/{jwt_token}", "expiryHours": 24 } ``` **Reset Analytics** ```http POST /videos/:id/reset-analytics Authorization: Bearer {admin_token} Response: { "success": true, "message": "Analytics reset successfully" } ``` **Get Video Analytics** ```http 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** ```http 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** ```http 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** ```http DELETE /videos/:id/schedule/:action Authorization: Bearer {admin_token} Params: - action: 'publish' or 'unpublish' Response: { "success": true, "message": "publish schedule cancelled" } ``` **Get Upcoming Schedules** ```http 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** ```http 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** ```http POST /track/view Content-Type: application/json Body: { "videoId": 123, "referer": "https://example.com" // optional } Response: { "viewId": 456 } ``` **Record Event** ```http 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)** ```http POST /track/heartbeat Content-Type: application/json Body: { "viewId": 456, "watchTimeSeconds": 120 } Response: { "success": true } ``` ### Analytics Query Endpoints (Admin) **Get Top Videos** ```http 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** ```http 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:** ```typescript { '/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) ### Preview Link Security **JWT Token Structure:** ```typescript { 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:** ```env VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24 JWT_ACCESS_SECRET=your_secret_here # Used for signing preview tokens ``` ### Privacy Protection **IP Address Hashing:** ```typescript import { createHash } from 'crypto'; function hashIpAddress(ipAddress: string): string { return createHash('sha256') .update(ipAddress) .digest('hex'); } ``` **User Agent Truncation:** ```typescript function truncateUserAgent(userAgent: string): string { // Remove version numbers: "Chrome/91.0.4472.124" → "Chrome" return userAgent .replace(/\/[\d.]+/g, '') .substring(0, 200); } ``` **Data Retention:** ```typescript // 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`: ```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:** ```bash # 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:** ```bash docker compose logs redis docker compose exec redis redis-cli ping ``` **Check Schedule History:** ```sql SELECT * FROM video_schedule_history WHERE status = 'failed' ORDER BY scheduled_for DESC LIMIT 10; ``` ### Analytics Not Tracking **Check Rate Limits:** ```bash # 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:** ```bash curl -X POST http://localhost:4100/api/track/view \ -H "Content-Type: application/json" \ -d '{"videoId": 1}' ``` ### Preview Link Expired **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:** ```env # 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.