changemaker.lite/docs/MEDIA_ADMIN_FEATURES.md

887 lines
22 KiB
Markdown

# 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.