887 lines
22 KiB
Markdown
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.
|