From b68dd0f3ea62e941690eeef57dfa1f1dd71879d1 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 25 Mar 2026 23:56:33 -0700 Subject: [PATCH] =?UTF-8?q?feat(content-hub):=20=E2=9C=A8=20Add=20admin=20?= =?UTF-8?q?API=20endpoints=20for=20content=20operations,=20lifecycle=20man?= =?UTF-8?q?agement,=20and=20translation=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../pages/content-hub/api/content-hub.api.ts | 49 +------ .../content-hub/api/content-lifecycle.api.ts | 131 ++++++++++++++++++ .../content-hub/api/translation-admin.api.ts | 89 ++++++++++++ 3 files changed, 221 insertions(+), 48 deletions(-) create mode 100644 features/platform-admin/frontend-admin/src/pages/content-hub/api/content-lifecycle.api.ts create mode 100644 features/platform-admin/frontend-admin/src/pages/content-hub/api/translation-admin.api.ts diff --git a/features/platform-admin/frontend-admin/src/pages/content-hub/api/content-hub.api.ts b/features/platform-admin/frontend-admin/src/pages/content-hub/api/content-hub.api.ts index d8531ac15..382238603 100644 --- a/features/platform-admin/frontend-admin/src/pages/content-hub/api/content-hub.api.ts +++ b/features/platform-admin/frontend-admin/src/pages/content-hub/api/content-hub.api.ts @@ -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 { - const token = localStorage.getItem('lilith_session') - return token ? { Authorization: `Bearer ${token}` } : {} -} - -async function fetchJson(url: string): Promise { - 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 -} - -async function postJson(url: string, body?: unknown): Promise { - 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 -} - // ─── Query string builder ────────────────────────────────────────────────────── function buildQueryString(filters: Record): string { diff --git a/features/platform-admin/frontend-admin/src/pages/content-hub/api/content-lifecycle.api.ts b/features/platform-admin/frontend-admin/src/pages/content-hub/api/content-lifecycle.api.ts new file mode 100644 index 000000000..c83365e76 --- /dev/null +++ b/features/platform-admin/frontend-admin/src/pages/content-hub/api/content-lifecycle.api.ts @@ -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 { + return useQuery({ + queryKey: ['content-lifecycle', domain, 'dashboard'], + queryFn: (): Promise => + fetchJson(`${API_BASE}/${domain}/dashboard`), + enabled: !!domain, + staleTime: 60_000, + }) +} + +export function useAuditReport(domain: string): UseQueryResult { + return useQuery({ + queryKey: ['content-lifecycle', domain, 'audit'], + queryFn: (): Promise => + fetchJson(`${API_BASE}/${domain}/audit`), + enabled: !!domain, + staleTime: 60_000, + }) +} + +export function useTranslationStatusReport(domain: string): UseQueryResult { + return useQuery({ + queryKey: ['content-lifecycle', domain, 'translation-status'], + queryFn: (): Promise => + fetchJson(`${API_BASE}/${domain}/translation-status`), + enabled: !!domain, + staleTime: 60_000, + }) +} + +export function useSeoReadinessReport(domain: string): UseQueryResult { + return useQuery({ + queryKey: ['content-lifecycle', domain, 'seo-readiness'], + queryFn: (): Promise => + fetchJson(`${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] }) + }, + } +} diff --git a/features/platform-admin/frontend-admin/src/pages/content-hub/api/translation-admin.api.ts b/features/platform-admin/frontend-admin/src/pages/content-hub/api/translation-admin.api.ts new file mode 100644 index 000000000..8d9fda843 --- /dev/null +++ b/features/platform-admin/frontend-admin/src/pages/content-hub/api/translation-admin.api.ts @@ -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 { + return useQuery({ + queryKey: ['translation-locales'], + queryFn: (): Promise => + fetchJson<{ locales: TranslationLocale[] }>(`${API_BASE}/locales`) + .then((r) => r.locales), + staleTime: 5 * 60_000, + }) +} + +export function useLocaleCoverage(locale: string): UseQueryResult { + return useQuery({ + queryKey: ['translation-coverage', locale], + queryFn: (): Promise => + fetchJson(`${API_BASE}/${locale}/coverage`), + enabled: !!locale, + staleTime: 60_000, + }) +} + +export function useAllCoverage(locales: string[]): Array> { + return useQueries({ + queries: locales.map((locale) => ({ + queryKey: ['translation-coverage', locale], + queryFn: (): Promise => + fetchJson(`${API_BASE}/${locale}/coverage`), + staleTime: 60_000, + enabled: locales.length > 0, + })), + }) +} + +// ─── Mutation hooks ─────────────────────────────────────────────────────────── + +export function useSyncNamespace(): UseMutationResult { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ namespace }: { namespace: string }): Promise => + postJson(`${API_BASE}/sync/namespace/${encodeURIComponent(namespace)}`), + onSuccess: (): void => { + queryClient.invalidateQueries({ queryKey: ['translation-coverage'] }) + queryClient.invalidateQueries({ queryKey: ['content-hub-verification'] }) + }, + }) +}