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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user