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
@@ -66,11 +66,11 @@ class AppInitApi extends BaseApi {
logContext: 'fetching account',
onSuccess: (data) => {
this.data = data;
licenseApi.setFeatures(data.licenseFeatures);
this.setupStores(params);
licenseApi.syncFromAccount(data.licenseFeatures, get(currentOrg));
setupAnalyticsBasedOnLicense(
data.profile?.id ? { id: data.profile.id, email: data.profile.email, name: data.profile.fullname } : undefined
);
this.setupStores(params);
this.setUserAnalytics();
this.routeUserToNextPage(params);
},
@@ -1,5 +1,6 @@
import { getFirstOrg, getOrgBySiteName, getOrgsByCustomDomain } from '$features/org/api/org.server';
import { dev } from '$app/environment';
import type { AccountOrg } from '$features/app/types';
import type { Cookies } from '@sveltejs/kit';
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
@@ -53,15 +54,10 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
// Custom domain
if (isURLCustomDomain(url)) {
console.log('it is custom domain');
const apiKeyHeaders = getApiKeyHeaders();
const orgs = await getOrgsByCustomDomain(url.host, true, apiKeyHeaders);
console.log('orgs', orgs);
const org = await lookupOrgFromWorker(url.origin, { customDomain: url.host });
if (!orgs || orgs.length === 0) {
return response;
}
if (!org) return response;
const org = orgs[0];
response.org = org as AccountOrg;
response.isOrgSite = true;
response.orgSiteName = response.org?.siteName || '';
@@ -82,9 +78,7 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
response.orgSiteName = debugMode ? _orgSiteName : subdomain;
if (response.orgSiteName) {
const apiKeyHeaders = getApiKeyHeaders();
const org = await getOrgBySiteName(response.orgSiteName, apiKeyHeaders);
response.org = org ?? null;
response.org = await lookupOrgFromWorker(url.origin, { siteName: response.orgSiteName });
}
const shouldDeleteCookie = !response.org && _orgSiteName;
@@ -96,6 +90,47 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
return response;
}
// Resolve the org via the tenant-router Worker. We call the *same origin* the
// browser hit (e.g. https://<tenant>.myclassroomio.com) because that host is
// fronted by the Worker, which serves /internal/org-lookup from KV (≈5ms) or
// Hyperdrive→Postgres on a miss (≈50ms). No API/Render hop, no extra env var.
//
// In dev / self-hosted there is no Worker in front, so fall back to the
// direct API call (same path the code used before the Worker cache existed).
async function lookupOrgFromWorker(
origin: string,
params: { siteName?: string; customDomain?: string }
): Promise<AccountOrg | null> {
if (dev || PUBLIC_IS_SELFHOSTED === 'true' || origin.includes('localhost')) {
const apiKeyHeaders = getApiKeyHeaders();
if (params.siteName) {
return (await getOrgBySiteName(params.siteName, apiKeyHeaders)) ?? null;
}
if (params.customDomain) {
const orgs = await getOrgsByCustomDomain(params.customDomain, true, apiKeyHeaders);
return (orgs?.[0] as AccountOrg) ?? null;
}
return null;
}
const qs = new URLSearchParams();
if (params.siteName) qs.set('siteName', params.siteName);
if (params.customDomain) qs.set('customDomain', params.customDomain);
try {
const res = await fetch(`${origin}/internal/org-lookup?${qs}`, {
headers: getApiKeyHeaders().headers
});
if (!res.ok) return null;
const json = (await res.json()) as { success: boolean; data: AccountOrg | null };
return json.success ? json.data : null;
} catch {
return null;
}
}
function isURLCustomDomain(url: URL) {
if (url.host.includes('localhost')) {
return false;