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
@@ -1,5 +1,6 @@
export * from './organization';
export * from './quiz';
export * from './invite';
export * from './link-invite';
export * from './automation-key';
export * from './automation-usage';
@@ -257,10 +257,12 @@ export async function getLatestOrgInvitesByEmails(
const latestByEmail = new Map<string, TLatestOrgInviteByEmail>();
for (const row of rows) {
if (!row.email) continue;
const key = row.email.toLowerCase();
const existing = latestByEmail.get(key);
if (!existing || new Date(row.createdAt) > new Date(existing.createdAt)) {
latestByEmail.set(key, row);
latestByEmail.set(key, { ...row, email: row.email });
}
}
@@ -0,0 +1,117 @@
import * as schema from '@db/schema';
import { and, eq, isNull } from 'drizzle-orm';
import { db, type DbOrTxClient } from '@db/drizzle';
import type { TOrganizationInvite } from '@db/types';
export type TLinkInviteRow = TOrganizationInvite & {
organization: { id: string; name: string; siteName: string };
};
export async function getOrgLinkInvite(organizationId: string): Promise<TOrganizationInvite | null> {
try {
const [row] = await db
.select()
.from(schema.organizationInvite)
.where(
and(
eq(schema.organizationInvite.organizationId, organizationId),
eq(schema.organizationInvite.type, 'LINK'),
eq(schema.organizationInvite.isRevoked, false)
)
)
.limit(1);
return row ?? null;
} catch (error) {
console.error('getOrgLinkInvite error:', error);
throw new Error(`Failed to get org link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function createLinkInvite(data: {
organizationId: string;
roleId: number;
tokenHash: string;
createdByProfileId: string;
expiresAt: string;
metadata: Record<string, unknown>;
}): Promise<TOrganizationInvite> {
try {
const [created] = await db
.insert(schema.organizationInvite)
.values({
...data,
type: 'LINK',
email: null,
isRevoked: false
})
.returning();
if (!created) {
throw new Error('Failed to create link invite');
}
return created;
} catch (error) {
console.error('createLinkInvite error:', error);
throw new Error(`Failed to create link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function setLinkInviteRevoked(
organizationId: string,
isRevoked: boolean,
profileId: string
): Promise<TOrganizationInvite | null> {
try {
const [updated] = await db
.update(schema.organizationInvite)
.set({
isRevoked,
revokedByProfileId: isRevoked ? profileId : null,
revokedAt: isRevoked ? new Date().toISOString() : null,
updatedAt: new Date().toISOString()
})
.where(
and(eq(schema.organizationInvite.organizationId, organizationId), eq(schema.organizationInvite.type, 'LINK'))
)
.returning();
return updated ?? null;
} catch (error) {
console.error('setLinkInviteRevoked error:', error);
throw new Error(`Failed to update link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function getOrgLinkInviteWithOrg(
dbClient: DbOrTxClient,
tokenHash: string
): Promise<{ invite: TOrganizationInvite; organization: { id: string; name: string; siteName: string } } | null> {
try {
const [row] = await dbClient
.select({
invite: schema.organizationInvite,
organization: {
id: schema.organization.id,
name: schema.organization.name,
siteName: schema.organization.siteName
}
})
.from(schema.organizationInvite)
.innerJoin(schema.organization, eq(schema.organizationInvite.organizationId, schema.organization.id))
.where(and(eq(schema.organizationInvite.tokenHash, tokenHash), eq(schema.organizationInvite.type, 'LINK')))
.limit(1);
if (!row) return null;
return {
...row,
organization: { ...row.organization, siteName: row.organization.siteName ?? '' }
};
} catch (error) {
console.error('getOrgLinkInviteWithOrg error:', error);
throw new Error(`Failed to load link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
@@ -913,9 +913,7 @@ export const getOrganizationPlanBySubscriptionId = async (
return plan ?? null;
} catch (error) {
console.error('getOrganizationPlanBySubscriptionId error:', error);
throw new Error(
`Failed to fetch organization plan: ${error instanceof Error ? error.message : 'Unknown error'}`
);
throw new Error(`Failed to fetch organization plan: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};