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
@@ -15,19 +15,20 @@ export const getRequestBaseUrl = () => {
if (typeof window === 'undefined') {
// When on the server, we want to hit the private url which is the docker container of `api` or the private network url of `api`.
// if that isn't set then it will fallback to the public url of the `api`
// this doesn't need proxy because on the server we are using our api key to make queries.
return process.env.PRIVATE_SERVER_URL || env.PUBLIC_SERVER_URL;
}
// Self-hosted: dashboard and API are on different subdomains of the
// operator's apex. Browser calls go straight to PUBLIC_SERVER_URL;
// cookies cross subdomains via AUTH_COOKIE_DOMAIN. No Worker proxy.
if (env.PUBLIC_IS_SELFHOSTED === 'true' || dev) {
// Local dev keeps the direct API path so existing Vite/API workflows do not
// depend on the SvelteKit proxy.
if (dev) {
return env.PUBLIC_SERVER_URL ?? '';
}
// Cloud (multi-tenant): same-origin via the Cloudflare Worker `/proxy`
// prefix so auth cookies stay host-only on whichever tenant or BYOD
// domain the user is currently visiting. The Worker strips `/proxy` before forwarding to the API
// Browser calls are same-origin in production. For self-hosted Node builds,
// hooks.server.ts proxies `/proxy/*` to PRIVATE_SERVER_URL.
// For cloud multi-tenant builds, the Cloudflare Worker handles the same
// prefix. In both cases auth cookies stay first-party on the dashboard host.
return `${window.location.origin}/proxy`;
};
@@ -5,9 +5,9 @@ import { dev } from '$app/environment';
// Browser baseURL depends on deployment shape:
//
// - Self-hosted: dashboard and API are on different subdomains under
// one apex. Browser calls go straight to PUBLIC_SERVER_URL; cookies
// cross subdomains via AUTH_COOKIE_DOMAIN. No Worker proxy.
// - Self-hosted: browser auth calls stay on the dashboard origin at
// `/api/auth/*`; hooks.server.ts forwards them to the API over
// PRIVATE_SERVER_URL.
//
// - Cloud (multi-tenant): same-origin via the Cloudflare Worker so
// auth cookies stay host-only on whichever tenant/BYOD domain the
@@ -23,9 +23,12 @@ function resolveBaseURL() {
if (typeof window === 'undefined') {
return env.PUBLIC_SERVER_URL || 'http://localhost:3002';
}
if (env.PUBLIC_IS_SELFHOSTED === 'true' || dev) {
if (dev) {
return env.PUBLIC_SERVER_URL || `${window.location.origin}/api/auth`;
}
if (env.PUBLIC_IS_SELFHOSTED === 'true') {
return `${window.location.origin}/api/auth`;
}
return `${window.location.origin}/proxy/api/auth`;
}
@@ -7,6 +7,7 @@ import { getRequestBaseUrl } from '$lib/utils/services/api';
export const getSessionData = async (cookies: Cookies): Promise<App.Locals | null> => {
try {
const cioCookies = getCioCookieString(cookies);
console.log('cioCookies', cioCookies);
if (!cioCookies) return null;
@@ -39,6 +40,7 @@ export async function getThroughTrpc(allCookies: string) {
export async function getThroughAuthClient(allCookies: string) {
const baseURL = getRequestBaseUrl();
console.log('baseURL', baseURL);
console.log('allCookies', allCookies);
const session = await authServerClient.getSession({
fetchOptions: {
headers: {
@@ -38,7 +38,10 @@ export const initPosthog = (user?: PosthogBootstrapUser): void => {
if (dev || hasNoTracking()) return;
posthog.init('phc_JfdHOZ6v0cVlGELBYx1Tmoen2nxNOrAzvgvrPA6Ksov', {
api_host: 'https://eu.posthog.com',
// Route PostHog through our own domain via the tenant-router Worker so the
// cookie is first-party and doesn't trigger the Lighthouse "third-party cookie" deduction.
api_host: `${window.location.origin}/ingest`,
ui_host: 'https://eu.posthog.com',
...(user && {
bootstrap: { distinctID: user.id, isIdentifiedID: true }
})
@@ -1,7 +1,6 @@
import { dev } from '$app/environment';
import { get } from 'svelte/store';
import { globalStore } from '$lib/utils/store/app';
import { licenseApi } from '$features/license/api/license.svelte';
const USERJOT_APP_ID = 'cm4a6vcmp00jpmdb5n66rmkzz';
@@ -9,7 +8,6 @@ let isInitialized = false;
function isWidgetAllowed(): boolean {
if (dev) return false;
if (licenseApi.hasAccess('no-tracking')) return false;
if (get(globalStore).isOrgSite) return false;
return true;
@@ -44,7 +42,7 @@ export function initUserJot(): void {
ensureSdkLoaded();
window.uj.init(USERJOT_APP_ID, {
widget: true,
trigger: 'custom',
position: 'right',
theme: 'auto'
});
@@ -75,3 +73,21 @@ export function identifyUserJotUser({ id, email, fullname, avatarUrl }: UserJotI
avatar: avatarUrl ?? undefined
});
}
export function clearUserJotUser(): void {
if (!isWidgetAllowed()) return;
ensureSdkLoaded();
window.uj.identify(null);
}
export type UserJotWidgetSection = 'feedback' | 'roadmap' | 'updates';
export function showUserJotWidget(section: UserJotWidgetSection): void {
if (!isWidgetAllowed()) return;
ensureSdkLoaded();
window.uj.showWidget({ section });
}
+4 -1
View File
@@ -10,7 +10,10 @@ declare global {
theme?: 'auto' | 'light' | 'dark';
}
) => void;
identify: (user: { id: string; email?: string; firstName?: string; lastName?: string; avatar?: string }) => void;
identify: (
user: { id: string; email?: string; firstName?: string; lastName?: string; avatar?: string } | null
) => void;
showWidget: (options: { section: 'feedback' | 'roadmap' | 'updates' }) => void;
[method: string]: (...args: unknown[]) => void;
};
}