Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
abe7f55
fix(cms): add video-coverage REST endpoint and revert maxLimit regres…
tataihono Apr 2, 2026
7e61b04
fix(manager): replace GraphQL video fetch with CMS REST endpoint
tataihono Apr 2, 2026
ce6ab1b
fix(manager): update frontend to consume coverage counts from new API
tataihono Apr 2, 2026
fe05abf
feat(manager): render collection as first tile in coverage grid
tataihono Apr 2, 2026
10dfd9f
fix(manager): sort collections by title, skip collection tile for sta…
tataihono Apr 2, 2026
14ef034
feat(manager): show coverage counts in tile hover tooltip
tataihono Apr 2, 2026
0a703af
feat(manager): show coverage counts in hover detail bar
tataihono Apr 2, 2026
ef6cb40
feat(manager): colored coverage count pills in hover detail bar
tataihono Apr 2, 2026
4d43190
fix(manager): lowercase pill labels
tataihono Apr 2, 2026
9b504f6
feat(manager): dashed border on tiles with partial coverage
tataihono Apr 2, 2026
48375df
fix(manager): apply URL update when removing language via pill X button
tataihono Apr 2, 2026
e49ca7d
feat(manager): show explore/translate toggle on all report types
tataihono Apr 2, 2026
560fa6d
fix(manager): use CSS tooltip for disabled translate button
tataihono Apr 2, 2026
49e0023
fix(manager): add arrow to coming soon tooltip
tataihono Apr 2, 2026
62815fa
fix(manager): increase tooltip arrow size
tataihono Apr 2, 2026
d030b55
fix(manager): fix tooltip arrow positioning
tataihono Apr 2, 2026
7eb446b
fix(manager): dashed checkbox border for partial coverage in detail view
tataihono Apr 2, 2026
c2a83c9
fix(manager): pin collection to top of its status group in detail view
tataihono Apr 2, 2026
cc63d45
fix(manager): show language names instead of IDs in selection bar
tataihono Apr 2, 2026
eedaee8
fix(manager): fetch language names for selection bar display
tataihono Apr 2, 2026
dd5f03b
feat(manager): add client-side video search with yellow highlight
tataihono Apr 2, 2026
b9b8689
fix(manager): hide collections with no search matches
tataihono Apr 2, 2026
1895e75
fix(manager): move searchMatchIds above visibleCollections to fix ini…
tataihono Apr 2, 2026
08f9b1a
feat(manager): move search below mode toggle, full-width pill style
tataihono Apr 2, 2026
4a88195
feat(manager): add filter dropdown next to search, remove yellow filt…
tataihono Apr 2, 2026
31602f6
feat(manager): show filtered count and clear filters button
tataihono Apr 2, 2026
52f8eb3
fix(manager): move collection count below search bar
tataihono Apr 2, 2026
e55af9e
fix(manager): reduce spacing on collection count row
tataihono Apr 2, 2026
abe7a45
feat(manager): wrap search, filter, and count in a card
tataihono Apr 2, 2026
08107a8
fix(manager): restyle search card status row and clear button
tataihono Apr 2, 2026
97de821
fix(manager): show collection count and clear button even when 0 matches
tataihono Apr 2, 2026
a38142f
fix(manager): revert clear filters to underline style
tataihono Apr 2, 2026
448228a
fix(manager): match filter dropdown styling to search input
tataihono Apr 2, 2026
7848251
feat(manager): search by video name or ID
tataihono Apr 2, 2026
4e246b2
fix(manager): replace native select with custom filter dropdown
tataihono Apr 2, 2026
115f00c
fix(manager): add 2px gap between filter dropdown items
tataihono Apr 2, 2026
5fe3709
fix(manager): remove thick border from collection tile
tataihono Apr 2, 2026
23c3285
fix(manager): match collection tile border-radius to video tiles
tataihono Apr 2, 2026
f1a5514
fix(manager): prevent tile hover shadow from clipping at edges
tataihono Apr 2, 2026
380b669
feat(manager): add collection type filter dropdown
tataihono Apr 2, 2026
38b0571
feat(manager): improve empty filter state with icon and helpful message
tataihono Apr 2, 2026
b575b08
fix(manager): fixed-width filter dropdown based on widest option
tataihono Apr 2, 2026
4735339
fix(manager): left-align dropdown label text
tataihono Apr 2, 2026
95ec0b8
docs: add brainstorm and plan for coverage query performance fix
tataihono Apr 2, 2026
a8822aa
fix: prettier formatting
tataihono Apr 2, 2026
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
3 changes: 0 additions & 3 deletions apps/cms/config/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ const config = ({
// dynamic-zone unions (reads .kind on undefined nodes). Set high
// to avoid the crash path in the library's recursive traversal.
depthLimit: 100,
// Cap the maximum number of items returned in a single collection
// field to limit N+1 association queries that exhaust the DB pool.
maxLimit: 100,
landingPage: env("NODE_ENV") !== "production",
generateArtifacts: true,
artifacts: {
Expand Down
24 changes: 24 additions & 0 deletions apps/cms/src/api/video-coverage/controllers/video-coverage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Core } from "@strapi/strapi"
import { queryVideoCoverage } from "../services/video-coverage"

type StrapiContext = {
status: number
body: unknown
query: Record<string, string | undefined>
}

export default ({ strapi }: { strapi: Core.Strapi }) => ({
async index(ctx: StrapiContext) {
const languageIds = ctx.query.languageIds
? ctx.query.languageIds.split(",").filter(Boolean)
: undefined

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const knex = (strapi.db as any).connection

const videos = await queryVideoCoverage(knex, languageIds)

ctx.status = 200
ctx.body = { videos }
},
})
14 changes: 14 additions & 0 deletions apps/cms/src/api/video-coverage/routes/video-coverage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
routes: [
{
method: "GET",
path: "/video-coverage",
handler: "video-coverage.index",
config: {
auth: false,
policies: [],
middlewares: [],
},
},
],
}
177 changes: 177 additions & 0 deletions apps/cms/src/api/video-coverage/services/video-coverage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* Video Coverage Service
*
* Computes per-video coverage counts via SQL aggregation. Returns all
* published videos with metadata, parent-child links, image URLs, and
* subtitle/audio coverage counts broken down by human vs AI.
*
* Follows the same SQL pattern as coverage-snapshot service but returns
* per-video detail instead of library-wide aggregates.
*
* Critical: All queries filter `published_at IS NOT NULL` to avoid
* counting Strapi v5 draft rows.
*/

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type KnexInstance = any

type CoverageCounts = {
human: number
ai: number
}

type VideoCoverageRow = {
document_id: string
core_id: string | null
title: string | null
label: string | null
slug: string | null
ai_metadata: boolean | null
thumbnail: string | null
video_still: string | null
parent_document_ids: string[] | null
sub_human: number
sub_ai: number
aud_human: number
aud_ai: number
}

type VideoCoverageResult = {
documentId: string
coreId: string | null
title: string | null
label: string | null
slug: string | null
aiMetadata: boolean | null
thumbnailUrl: string | null
videoStillUrl: string | null
parentDocumentIds: string[]
coverage: {
subtitles: CoverageCounts
audio: CoverageCounts
}
}

export async function queryVideoCoverage(
knex: KnexInstance,
languageIds?: string[],
): Promise<VideoCoverageResult[]> {
const hasLangFilter = languageIds && languageIds.length > 0

// Two-step approach: first compute per-video, per-language coverage in CTEs,
// then aggregate to per-video counts and join with video metadata.

const bindings: unknown[] = []

// Subtitle coverage CTE
const subLangClause = hasLangFilter ? `AND l.core_id = ANY(?)` : ""
if (hasLangFilter) bindings.push(languageIds)

// Variant coverage CTE
const audLangClause = hasLangFilter ? `AND l.core_id = ANY(?)` : ""
if (hasLangFilter) bindings.push(languageIds)

const sql = `
WITH subtitle_per_lang AS (
SELECT
v.document_id AS vid,
l.core_id AS lang_core_id,
BOOL_OR(NOT COALESCE(s.ai_generated, true)) AS has_human
FROM video_subtitles s
JOIN video_subtitles_video_lnk svl ON svl.video_subtitle_id = s.id
JOIN video_subtitles_language_lnk sll ON sll.video_subtitle_id = s.id
JOIN videos v ON v.id = svl.video_id AND v.published_at IS NOT NULL
JOIN languages l ON l.id = sll.language_id ${subLangClause}
WHERE s.published_at IS NOT NULL
GROUP BY v.document_id, l.core_id
),
subtitle_cov AS (
SELECT
vid,
COUNT(*) FILTER (WHERE has_human) AS sub_human,
COUNT(*) FILTER (WHERE NOT has_human) AS sub_ai
FROM subtitle_per_lang
GROUP BY vid
),
variant_per_lang AS (
SELECT
v.document_id AS vid,
l.core_id AS lang_core_id,
BOOL_OR(NOT COALESCE(vr.ai_generated, true)) AS has_human
FROM video_variants vr
JOIN video_variants_video_lnk vvl ON vvl.video_variant_id = vr.id
JOIN video_variants_language_lnk vll ON vll.video_variant_id = vr.id
JOIN videos v ON v.id = vvl.video_id AND v.published_at IS NOT NULL
JOIN languages l ON l.id = vll.language_id ${audLangClause}
WHERE vr.published_at IS NOT NULL
GROUP BY v.document_id, l.core_id
),
variant_cov AS (
SELECT
vid,
COUNT(*) FILTER (WHERE has_human) AS aud_human,
COUNT(*) FILTER (WHERE NOT has_human) AS aud_ai
FROM variant_per_lang
GROUP BY vid
),
parent_links AS (
SELECT
child.document_id AS child_doc_id,
ARRAY_AGG(DISTINCT parent.document_id) AS parent_document_ids
FROM videos_children_lnk cl
JOIN videos parent ON parent.id = cl.video_id AND parent.published_at IS NOT NULL
JOIN videos child ON child.id = cl.inv_video_id AND child.published_at IS NOT NULL
GROUP BY child.document_id
),
video_image AS (
SELECT DISTINCT ON (v.document_id)
v.document_id AS vid,
vi.thumbnail,
vi.video_still
FROM videos v
JOIN video_images_video_lnk vil ON vil.video_id = v.id
JOIN video_images vi ON vi.id = vil.video_image_id AND vi.published_at IS NOT NULL
WHERE v.published_at IS NOT NULL
ORDER BY v.document_id, vi.id
)
SELECT
v.document_id,
v.core_id,
v.title,
v.label,
v.slug,
v.ai_metadata,
img.thumbnail,
img.video_still,
pl.parent_document_ids,
COALESCE(sc.sub_human, 0)::int AS sub_human,
COALESCE(sc.sub_ai, 0)::int AS sub_ai,
COALESCE(vc.aud_human, 0)::int AS aud_human,
COALESCE(vc.aud_ai, 0)::int AS aud_ai
FROM videos v
LEFT JOIN subtitle_cov sc ON sc.vid = v.document_id
LEFT JOIN variant_cov vc ON vc.vid = v.document_id
LEFT JOIN parent_links pl ON pl.child_doc_id = v.document_id
LEFT JOIN video_image img ON img.vid = v.document_id
WHERE v.published_at IS NOT NULL
ORDER BY v.title NULLS LAST
`

const result: { rows: VideoCoverageRow[] } = await knex.raw(sql, bindings)

return result.rows.map((row) => ({
documentId: row.document_id,
coreId: row.core_id,
title: row.title,
label: row.label,
slug: row.slug,
aiMetadata: row.ai_metadata,
thumbnailUrl: row.thumbnail,
videoStillUrl: row.video_still,
parentDocumentIds: row.parent_document_ids ?? [],
coverage: {
subtitles: { human: row.sub_human, ai: row.sub_ai },
audio: { human: row.aud_human, ai: row.aud_ai },
},
}))
}
4 changes: 2 additions & 2 deletions apps/manager/src/app/api/languages/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ async function fetchLanguagePayload(): Promise<string> {

export const languageCache = createSwrCache({
fetcher: fetchLanguagePayload,
ttlMs: 5 * 60_000, // 5 minutes — geo data changes only on core sync
maxStaleMs: 60 * 60_000, // 1 hour — hard limit
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",
})

Expand Down
Loading
Loading