feat(content-hub): Add admin API endpoints for content operations, lifecycle management, and translation workflows

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-25 23:56:33 -07:00
parent 7d5d026df6
commit b68dd0f3ea
3 changed files with 221 additions and 48 deletions

View file

@ -2,12 +2,12 @@
* Content Hub API React Query hooks
*
* All requests target the platform-admin API via Vite proxy.
* Auth token is forwarded from localStorage (same pattern as moderation API).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import type { UseQueryResult, UseMutationResult } from '@tanstack/react-query'
import { fetchJson, postJson } from '@/api/http'
import type {
UnifiedContentItem,
ContentHubStats,
@ -15,55 +15,8 @@ import type {
VerificationReport,
} from '../model/types'
// ─── Constants ────────────────────────────────────────────────────────────────
const API_BASE = '/api/admin/content-hub'
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
function getAuthHeaders(): Record<string, string> {
const token = localStorage.getItem('lilith_session')
return token ? { Authorization: `Bearer ${token}` } : {}
}
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
})
if (!response.ok) {
const error = await response.json().catch((): { message?: string } => ({ message: response.statusText }))
throw new Error(
(error as { message?: string }).message ?? `API error: ${response.status} ${response.statusText}`,
)
}
return response.json() as Promise<T>
}
async function postJson<T>(url: string, body?: unknown): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
body: body !== undefined ? JSON.stringify(body) : undefined,
})
if (!response.ok) {
const error = await response.json().catch((): { message?: string } => ({ message: response.statusText }))
throw new Error(
(error as { message?: string }).message ?? `API error: ${response.status} ${response.statusText}`,
)
}
return response.json() as Promise<T>
}
// ─── Query string builder ──────────────────────────────────────────────────────
function buildQueryString(filters: Record<string, unknown>): string {

View file

@ -0,0 +1,131 @@
/**
* Content Lifecycle API React Query hooks
*
* Live filesystem analysis of locale content via the platform-admin backend.
*/
import { useQuery, useQueryClient } from '@tanstack/react-query'
import type { UseQueryResult } from '@tanstack/react-query'
import { fetchJson } from '@/api/http'
const API_BASE = '/api/admin/content-lifecycle'
// ─── Types ────────────────────────────────────────────────────────────────────
export interface AuditSummary {
totalNamespaces: number
ready: number
needsReview: number
blocked: number
totalIssues: number
criticalIssues: number
}
export interface AuditReport {
domain: string
timestamp: string
summary: AuditSummary
namespaces: Array<{
namespace: string
status: 'READY' | 'NEEDS_REVIEW' | 'BLOCKED'
keyCount: number
issues: Array<{
severity: 'critical' | 'high' | 'medium' | 'low'
category: string
message: string
}>
}>
}
export interface TranslationStatusReport {
domain: string
timestamp: string
summary: {
totalNamespaces: number
fullyTranslated: number
untranslated: number
totalStale: number
coveragePercent: number
}
}
export interface SeoReadinessReport {
domain: string
timestamp: string
summary: {
totalPages: number
completePages: number
completenessPercent: number
}
}
export interface PublishDecision {
domain: string
timestamp: string
publishable: boolean
score: number
gates: {
audit: boolean
translations: boolean
seo: boolean
}
blockers: string[]
auditSummary?: AuditSummary
translationSummary?: TranslationStatusReport['summary']
seoSummary?: SeoReadinessReport['summary']
}
// ─── Query hooks ──────────────────────────────────────────────────────────────
export function useDashboardReport(domain: string): UseQueryResult<PublishDecision | null, Error> {
return useQuery({
queryKey: ['content-lifecycle', domain, 'dashboard'],
queryFn: (): Promise<PublishDecision | null> =>
fetchJson<PublishDecision | null>(`${API_BASE}/${domain}/dashboard`),
enabled: !!domain,
staleTime: 60_000,
})
}
export function useAuditReport(domain: string): UseQueryResult<AuditReport | null, Error> {
return useQuery({
queryKey: ['content-lifecycle', domain, 'audit'],
queryFn: (): Promise<AuditReport | null> =>
fetchJson<AuditReport | null>(`${API_BASE}/${domain}/audit`),
enabled: !!domain,
staleTime: 60_000,
})
}
export function useTranslationStatusReport(domain: string): UseQueryResult<TranslationStatusReport | null, Error> {
return useQuery({
queryKey: ['content-lifecycle', domain, 'translation-status'],
queryFn: (): Promise<TranslationStatusReport | null> =>
fetchJson<TranslationStatusReport | null>(`${API_BASE}/${domain}/translation-status`),
enabled: !!domain,
staleTime: 60_000,
})
}
export function useSeoReadinessReport(domain: string): UseQueryResult<SeoReadinessReport | null, Error> {
return useQuery({
queryKey: ['content-lifecycle', domain, 'seo-readiness'],
queryFn: (): Promise<SeoReadinessReport | null> =>
fetchJson<SeoReadinessReport | null>(`${API_BASE}/${domain}/seo-readiness`),
enabled: !!domain,
staleTime: 60_000,
})
}
// ─── Mutation hooks ───────────────────────────────────────────────────────────
export function useRefreshAudit(domain: string): { refresh: () => void } {
const queryClient = useQueryClient()
return {
refresh: (): void => {
queryClient.invalidateQueries({ queryKey: ['content-lifecycle', domain] })
},
}
}

View file

@ -0,0 +1,89 @@
/**
* Translation Admin API React Query hooks
*
* Proxied to the Landing API via `/api/translations`.
* Provides locale listing, coverage stats, and translation sync triggers.
*/
import { useQuery, useMutation, useQueryClient, useQueries } from '@tanstack/react-query'
import type { UseQueryResult, UseMutationResult } from '@tanstack/react-query'
import { fetchJson, postJson } from '@/api/http'
const API_BASE = '/api/translations'
// ─── Types ────────────────────────────────────────────────────────────────────
export interface TranslationLocale {
code: string
name: string
nativeName: string
enabled: boolean
direction: 'ltr' | 'rtl'
}
export interface LocaleCoverageStats {
locale: string
totalNamespaces: number
translatedNamespaces: number
totalKeys: number
translatedKeys: number
coveragePercent: number
}
export interface SyncResult {
namespace: string
locale: string
added: number
updated: number
unchanged: number
}
// ─── Query hooks ──────────────────────────────────────────────────────────────
export function useTranslationLocales(): UseQueryResult<TranslationLocale[], Error> {
return useQuery({
queryKey: ['translation-locales'],
queryFn: (): Promise<TranslationLocale[]> =>
fetchJson<{ locales: TranslationLocale[] }>(`${API_BASE}/locales`)
.then((r) => r.locales),
staleTime: 5 * 60_000,
})
}
export function useLocaleCoverage(locale: string): UseQueryResult<LocaleCoverageStats, Error> {
return useQuery({
queryKey: ['translation-coverage', locale],
queryFn: (): Promise<LocaleCoverageStats> =>
fetchJson<LocaleCoverageStats>(`${API_BASE}/${locale}/coverage`),
enabled: !!locale,
staleTime: 60_000,
})
}
export function useAllCoverage(locales: string[]): Array<UseQueryResult<LocaleCoverageStats, Error>> {
return useQueries({
queries: locales.map((locale) => ({
queryKey: ['translation-coverage', locale],
queryFn: (): Promise<LocaleCoverageStats> =>
fetchJson<LocaleCoverageStats>(`${API_BASE}/${locale}/coverage`),
staleTime: 60_000,
enabled: locales.length > 0,
})),
})
}
// ─── Mutation hooks ───────────────────────────────────────────────────────────
export function useSyncNamespace(): UseMutationResult<SyncResult, Error, { namespace: string }> {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ namespace }: { namespace: string }): Promise<SyncResult> =>
postJson<SyncResult>(`${API_BASE}/sync/namespace/${encodeURIComponent(namespace)}`),
onSuccess: (): void => {
queryClient.invalidateQueries({ queryKey: ['translation-coverage'] })
queryClient.invalidateQueries({ queryKey: ['content-hub-verification'] })
},
})
}