feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)

New features:
- HLS video encoding/streaming (hls routes, hls-encoder) and a media
  transcripts pipeline (transcripts route + media-transcript queries)
- AI assistant chat vs. run model: agent-course-generation worker/queue,
  run-monitor pane, chat-changes card, content ask-ai bar
- Per-theme course-page previews across org landing-page themes (bold,
  classic, corporate, editorial, etc.) with a shared course token/component set
- View-as-student mode and login-link / link-invite invite flows

Refactor:
- Extract packages/core consolidating config (env, storage, dashboard-url),
  services and utils previously duplicated under apps/api/src/config
- Remove legacy agent service modules (chat-tools, chat-context,
  context-window, document, fetch-url, lesson-content)
- Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding
- Add format script to tenant-router so turbo run format covers it
This commit is contained in:
Rotimi Best
2026-06-07 18:43:05 +01:00
committed by GitHub
parent a2e70779a0
commit d5b07d356e
496 changed files with 19050 additions and 8312 deletions
View File
+2 -1
View File
@@ -28,8 +28,9 @@ const envSchema = z.object({
MEDIA_WORKER_CONCURRENCY: z.string().optional(),
TRANSCRIBE_WORKER_CONCURRENCY: z.string().optional(),
EMAIL_WORKER_CONCURRENCY: z.string().optional(),
AGENT_COURSE_GENERATION_WORKER_CONCURRENCY: z.string().optional(),
/** Optional OpenAI Whisper key — when unset, transcribe-audio jobs no-op. */
/** Optional OpenAI key — when unset, transcribe-audio jobs no-op and OpenAI-backed agent runs fail config checks. */
OPENAI_API_KEY: z.string().optional()
});
-78
View File
@@ -1,78 +0,0 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.getStorageConfig = getStorageConfig;
exports.getS3Client = getS3Client;
var client_s3_1 = require('@aws-sdk/client-s3');
var env_1 = require('./env');
/**
* Worker-side S3 client. Mirrors apps/api/src/config/storage.ts so workers
* can stream the same buckets the API uploads to. Kept self-contained so
* apps/jobs has no runtime dependency on apps/api.
*/
var BUCKET_DEFAULTS = {
VIDEOS: 'videos',
DOCUMENTS: 'documents',
MEDIA: 'media'
};
function useS3CompatibleConfig() {
return Boolean(
env_1.env.OBJECT_STORAGE_ENDPOINT &&
env_1.env.OBJECT_STORAGE_ACCESS_KEY_ID &&
env_1.env.OBJECT_STORAGE_SECRET_ACCESS_KEY
);
}
function isCloudflareConfigured() {
return Boolean(
env_1.env.CLOUDFLARE_ACCOUNT_ID && env_1.env.CLOUDFLARE_ACCESS_KEY && env_1.env.CLOUDFLARE_SECRET_ACCESS_KEY
);
}
var cachedConfig = null;
function getStorageConfig() {
if (cachedConfig) return cachedConfig;
if (useS3CompatibleConfig()) {
cachedConfig = {
endpoint: env_1.env.OBJECT_STORAGE_ENDPOINT,
region: env_1.env.OBJECT_STORAGE_REGION || 'us-east-1',
accessKeyId: env_1.env.OBJECT_STORAGE_ACCESS_KEY_ID,
secretAccessKey: env_1.env.OBJECT_STORAGE_SECRET_ACCESS_KEY,
forcePathStyle:
env_1.env.OBJECT_STORAGE_FORCE_PATH_STYLE === 'true' || env_1.env.OBJECT_STORAGE_FORCE_PATH_STYLE === '1',
bucketVideos: env_1.env.OBJECT_STORAGE_BUCKET_VIDEOS || BUCKET_DEFAULTS.VIDEOS,
bucketDocuments: env_1.env.OBJECT_STORAGE_BUCKET_DOCUMENTS || BUCKET_DEFAULTS.DOCUMENTS,
bucketMedia: env_1.env.OBJECT_STORAGE_BUCKET_MEDIA || BUCKET_DEFAULTS.MEDIA,
mediaPublicBaseUrl: env_1.env.OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL || null
};
} else if (isCloudflareConfigured()) {
cachedConfig = {
endpoint: 'https://'.concat(env_1.env.CLOUDFLARE_ACCOUNT_ID, '.r2.cloudflarestorage.com'),
region: 'auto',
accessKeyId: env_1.env.CLOUDFLARE_ACCESS_KEY,
secretAccessKey: env_1.env.CLOUDFLARE_SECRET_ACCESS_KEY,
forcePathStyle: false,
bucketVideos: BUCKET_DEFAULTS.VIDEOS,
bucketDocuments: BUCKET_DEFAULTS.DOCUMENTS,
bucketMedia: BUCKET_DEFAULTS.MEDIA,
mediaPublicBaseUrl: env_1.env.CLOUDFLARE_IMAGE_BUCKET_DOMAIN || null
};
} else {
throw new Error(
'Object storage not configured. Set OBJECT_STORAGE_* (MinIO/S3) or CLOUDFLARE_* (R2) env vars on apps/jobs.'
);
}
return cachedConfig;
}
var s3Client = null;
function getS3Client() {
if (s3Client) return s3Client;
var config = getStorageConfig();
s3Client = new client_s3_1.S3Client({
region: config.region,
endpoint: config.endpoint,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey
},
forcePathStyle: config.forcePathStyle
});
return s3Client;
}