Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/cms/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1348,8 +1348,8 @@ enum ENUM_COMPONENTENRICHMENTJOBSTEP_NAME {
embeddings
metadata
mux_upload
theology_validation_bible_quotes
seo_improvements
theology_validation_bible_quotes
transcription
translation
}
Expand Down Expand Up @@ -4010,4 +4010,4 @@ input WatchSettingInput {

type WatchSettingRelationResponseCollection {
nodes: [WatchSetting!]!
}
}
25 changes: 25 additions & 0 deletions apps/manager/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
8 changes: 8 additions & 0 deletions apps/manager/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ const nextConfig: NextConfig = {
experimental: {
typedRoutes: true,
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
}

export default nextConfig
8 changes: 8 additions & 0 deletions apps/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,29 @@
"@forge/video-player": "workspace:*",
"@mux/mux-node": "^9.0.0",
"@t3-oss/env-nextjs": "^0.13.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.577.0",
"next": "^16.1.6",
"openai": "^4.0.0",
"p-limit": "^7.3.0",
"react": "19.2.4",
"react-blurhash": "^0.3.0",
"react-dom": "19.2.4",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0",
"video.js": "^8.22.0",
"workflow": "^4.2.0-beta.70",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.3",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"eslint": "^9.0.0",
"eslint-config-next": "^16.1.6",
"postcss": "^8.5.10",
"tailwindcss": "^4.2.3",
"typescript": "^5",
"vitest": "2.1.9"
}
Expand Down
7 changes: 7 additions & 0 deletions apps/manager/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
}

export default config
67 changes: 67 additions & 0 deletions apps/manager/src/app/api/coverage-snapshots/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { graphql } from "@forge/graphql"
import getClient from "@/cms/client"
import { createSwrCache } from "@/lib/swr-cache"

const GET_COVERAGE_SNAPSHOTS = graphql(`
query GetCoverageSnapshots(
$filters: CoverageSnapshotFiltersInput
$sort: [String]
$pagination: PaginationArg
) {
coverageSnapshots(filters: $filters, sort: $sort, pagination: $pagination) {
documentId
date
computedAt
totalVideos
videosWithAiMetadata
videosWithHumanMetadata
subtitlesHumanTotal
subtitlesAiTotal
audioHumanTotal
audioAiTotal
languageCoverage
}
}
`)

export const coverageSnapshotRangeDateRegex = /^\d{4}-\d{2}-\d{2}$/

type LatestCoverageSnapshotResult = {
snapshot: Awaited<ReturnType<typeof fetchLatestSnapshotFromCms>>
}

async function fetchLatestSnapshotFromCms() {
const client = getClient()
const result = await client.query({
query: GET_COVERAGE_SNAPSHOTS,
variables: {
sort: ["date:desc"],
pagination: { limit: 1 },
},
fetchPolicy: "no-cache",
})

return result.data?.coverageSnapshots?.[0] ?? null
}

async function fetchLatestSnapshot(): Promise<LatestCoverageSnapshotResult> {
try {
const snapshot = await fetchLatestSnapshotFromCms()
return { snapshot }
} catch (error) {
console.warn(
"[coverage-snapshot-cache] Falling back to empty latest snapshot:",
error instanceof Error ? error.message : "Unknown error",
)
return { snapshot: null }
}
}

export const latestCoverageSnapshotCache = createSwrCache({
fetcher: fetchLatestSnapshot,
ttlMs: 5 * 60_000,
maxStaleMs: 24 * 60 * 60_000,
label: "coverage-snapshot-cache",
})

export { GET_COVERAGE_SNAPSHOTS }
62 changes: 12 additions & 50 deletions apps/manager/src/app/api/coverage-snapshots/route.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,20 @@
import { NextResponse } from "next/server"
import { z } from "zod"
import { graphql } from "@forge/graphql"
import { authenticateRequest } from "@/lib/auth"
import getClient from "@/cms/client"
import { createSwrCache } from "@/lib/swr-cache"

const GET_COVERAGE_SNAPSHOTS = graphql(`
query GetCoverageSnapshots(
$filters: CoverageSnapshotFiltersInput
$sort: [String]
$pagination: PaginationArg
) {
coverageSnapshots(filters: $filters, sort: $sort, pagination: $pagination) {
documentId
date
computedAt
totalVideos
videosWithAiMetadata
videosWithHumanMetadata
subtitlesHumanTotal
subtitlesAiTotal
audioHumanTotal
audioAiTotal
languageCoverage
}
}
`)

const dateRegex = /^\d{4}-\d{2}-\d{2}$/
import {
coverageSnapshotRangeDateRegex,
GET_COVERAGE_SNAPSHOTS,
latestCoverageSnapshotCache,
} from "./cache"

const rangeSchema = z.object({
startDate: z.string().regex(dateRegex, "Must be YYYY-MM-DD format"),
endDate: z.string().regex(dateRegex, "Must be YYYY-MM-DD format"),
})

async function fetchLatestSnapshot() {
const client = getClient()
const result = await client.query({
query: GET_COVERAGE_SNAPSHOTS,
variables: {
sort: ["date:desc"],
pagination: { limit: 1 },
},
fetchPolicy: "no-cache",
})

return result.data?.coverageSnapshots?.[0] ?? null
}

export const latestCoverageSnapshotCache = createSwrCache({
fetcher: fetchLatestSnapshot,
ttlMs: 5 * 60_000,
maxStaleMs: 24 * 60 * 60_000,
label: "coverage-snapshot-cache",
startDate: z
.string()
.regex(coverageSnapshotRangeDateRegex, "Must be YYYY-MM-DD format"),
endDate: z
.string()
.regex(coverageSnapshotRangeDateRegex, "Must be YYYY-MM-DD format"),
})

export async function GET(request: Request) {
Expand All @@ -66,7 +28,7 @@ export async function GET(request: Request) {
const client = getClient()

if (isLatest) {
const snapshot = await latestCoverageSnapshotCache.get()
const { snapshot } = await latestCoverageSnapshotCache.get()
return NextResponse.json({ snapshot })
}

Expand Down
40 changes: 40 additions & 0 deletions apps/manager/src/app/api/languages/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { env } from "@/config/env"
import { createSwrCache } from "@/lib/swr-cache"

type CmsLanguageGeo = {
continents: Array<{ id: string; name: string }>
countries: Array<{ id: string; name: string; continentId: string }>
languages: Array<{
id: string
englishLabel: string
nativeLabel: string
countryIds: string[]
continentIds: string[]
countrySpeakers: Record<string, number>
}>
}

async function fetchLanguageGeo(): Promise<string> {
const url = `${env.STRAPI_URL}/api/language-geo`

const response = await fetch(url, {
headers: { Authorization: `Bearer ${env.STRAPI_API_TOKEN}` },
signal: AbortSignal.timeout(10_000),
})

if (!response.ok) {
throw new Error(
`CMS /api/language-geo returned ${response.status}: ${await response.text()}`,
)
}

const data = (await response.json()) as CmsLanguageGeo
return JSON.stringify(data)
}

export const languageCache = createSwrCache({
fetcher: fetchLanguageGeo,
ttlMs: 24 * 60 * 60_000,
maxStaleMs: 48 * 60 * 60_000,
label: "language-cache",
})
55 changes: 1 addition & 54 deletions apps/manager/src/app/api/languages/route.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,6 @@
import { NextResponse } from "next/server"
import { authenticateRequest } from "@/lib/auth"
import { env } from "@/config/env"
import { createSwrCache } from "@/lib/swr-cache"

// ---------------------------------------------------------------------------
// Types from CMS /api/language-geo endpoint
// ---------------------------------------------------------------------------

// Must match LanguageGeoResult in apps/cms/src/api/language-geo/services/language-geo.ts
type CmsLanguageGeo = {
continents: Array<{ id: string; name: string }>
countries: Array<{ id: string; name: string; continentId: string }>
languages: Array<{
id: string
englishLabel: string
nativeLabel: string
countryIds: string[]
continentIds: string[]
countrySpeakers: Record<string, number>
}>
}

// ---------------------------------------------------------------------------
// Fetch from CMS language-geo endpoint
// ---------------------------------------------------------------------------

async function fetchLanguageGeo(): Promise<string> {
const url = `${env.STRAPI_URL}/api/language-geo`

const response = await fetch(url, {
headers: { Authorization: `Bearer ${env.STRAPI_API_TOKEN}` },
signal: AbortSignal.timeout(10_000),
})

if (!response.ok) {
throw new Error(
`CMS /api/language-geo returned ${response.status}: ${await response.text()}`,
)
}

const data = (await response.json()) as CmsLanguageGeo
return JSON.stringify(data)
}

// ---------------------------------------------------------------------------
// SWR cache (geo data changes only on core sync)
// Caches pre-serialized JSON string for zero-cost response serving.
// ---------------------------------------------------------------------------

export const languageCache = createSwrCache({
fetcher: fetchLanguageGeo,
ttlMs: 24 * 60 * 60_000, // 24 hours — geo data changes only on core sync
maxStaleMs: 48 * 60 * 60_000, // 48 hours — hard limit
label: "language-cache",
})
import { languageCache } from "./cache"

// ---------------------------------------------------------------------------
// Route handler
Expand Down
Loading
Loading