diff --git a/components/manuals-dashboard-client.tsx b/components/manuals-dashboard-client.tsx index e5e3864b..946ca185 100644 --- a/components/manuals-dashboard-client.tsx +++ b/components/manuals-dashboard-client.tsx @@ -1,9 +1,8 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { fetchPublishedManualsDashboard } from '@/lib/manuals-live-catalog' import { BarChart3, FileText, @@ -33,51 +32,29 @@ interface ManualsDashboardClientProps { export function ManualsDashboardClient({ data }: ManualsDashboardClientProps) { const [searchTerm, setSearchTerm] = useState('') - const [liveData, setLiveData] = useState(data) - - useEffect(() => { - let cancelled = false - - void (async () => { - const refreshed = await fetchPublishedManualsDashboard() - if (cancelled || !refreshed) { - return - } - - setLiveData((current) => { - const currentCount = Array.isArray(current?.metadata) ? current.metadata.length : 0 - const refreshedCount = Array.isArray(refreshed?.metadata) ? refreshed.metadata.length : 0 - return refreshedCount > currentCount ? refreshed : current - }) - })() - - return () => { - cancelled = true - } - }, []) // Calculate statistics const stats = useMemo(() => { - const missing = liveData.missingManuals?.summary || {} - const qaCategories = liveData.qaData.reduce((acc: any, item: any) => { + const missing = data.missingManuals?.summary || {} + const qaCategories = data.qaData.reduce((acc: any, item: any) => { const cat = item.category || 'unknown' acc[cat] = (acc[cat] || 0) + 1 return acc }, {}) - const metadataWithSpecs = liveData.metadata.filter((m: any) => + const metadataWithSpecs = data.metadata.filter((m: any) => m.specifications?.dimensions && Object.keys(m.specifications.dimensions).length > 0 ).length - const metadataWithParts = liveData.metadata.filter((m: any) => + const metadataWithParts = data.metadata.filter((m: any) => m.parts_list && m.parts_list.length > 0 ).length - const metadataWithTroubleshooting = liveData.metadata.filter((m: any) => + const metadataWithTroubleshooting = data.metadata.filter((m: any) => m.troubleshooting && m.troubleshooting.length > 0 ).length - const schemaTypes = liveData.structuredData.reduce((acc: any, item: any) => { + const schemaTypes = data.structuredData.reduce((acc: any, item: any) => { item.schemas?.forEach((schema: any) => { const type = schema['@type'] || 'unknown' acc[type] = (acc[type] || 0) + 1 @@ -89,36 +66,36 @@ export function ManualsDashboardClient({ data }: ManualsDashboardClientProps) { totalModels: missing.total_expected_models || 0, missingAll: missing.models_missing_all || 0, partial: missing.models_partial || 0, - totalQAPairs: liveData.qaData.length, + totalQAPairs: data.qaData.length, qaCategories, - totalManuals: liveData.metadata.length, + totalManuals: data.metadata.length, metadataWithSpecs, metadataWithParts, metadataWithTroubleshooting, - totalChunks: liveData.semanticIndex?.total_chunks || 0, + totalChunks: data.semanticIndex?.total_chunks || 0, schemaTypes, - highPriority: liveData.acquisitionList?.high_priority || 0, - mediumPriority: liveData.acquisitionList?.medium_priority || 0, - lowPriority: liveData.acquisitionList?.low_priority || 0, + highPriority: data.acquisitionList?.high_priority || 0, + mediumPriority: data.acquisitionList?.medium_priority || 0, + lowPriority: data.acquisitionList?.low_priority || 0, } - }, [liveData]) + }, [data]) // Filter Q&A data const filteredQA = useMemo(() => { - if (!searchTerm) return liveData.qaData.slice(0, 50) + if (!searchTerm) return data.qaData.slice(0, 50) const term = searchTerm.toLowerCase() - return liveData.qaData.filter((item: any) => + return data.qaData.filter((item: any) => item.question?.toLowerCase().includes(term) || item.answer?.toLowerCase().includes(term) ).slice(0, 50) - }, [liveData.qaData, searchTerm]) + }, [data.qaData, searchTerm]) // Get high priority missing manuals const highPriorityMissing = useMemo(() => { - return (liveData.acquisitionList?.acquisition_list || []) + return (data.acquisitionList?.acquisition_list || []) .filter((item: any) => item.priority === 'high') .slice(0, 20) - }, [liveData.acquisitionList]) + }, [data.acquisitionList]) return (
@@ -414,13 +391,13 @@ export function ManualsDashboardClient({ data }: ManualsDashboardClientProps) { Sample Manual Metadata - {liveData.metadata.length > 0 && ( + {data.metadata.length > 0 && (
-

Manufacturer: {liveData.metadata[0].manufacturer}

-

Model: {liveData.metadata[0].model_number || 'N/A'}

-

Type: {liveData.metadata[0].manual_type}

- {liveData.metadata[0].specifications?.interfaces && ( -

Interfaces: {liveData.metadata[0].specifications.interfaces.join(', ')}

+

Manufacturer: {data.metadata[0].manufacturer}

+

Model: {data.metadata[0].model_number || 'N/A'}

+

Type: {data.metadata[0].manual_type}

+ {data.metadata[0].specifications?.interfaces && ( +

Interfaces: {data.metadata[0].specifications.interfaces.join(', ')}

)}
)} @@ -439,7 +416,7 @@ export function ManualsDashboardClient({ data }: ManualsDashboardClientProps) {

- Total Schemas: {liveData.structuredData.length} + Total Schemas: {data.structuredData.length}

Schema Types:

@@ -466,7 +443,7 @@ export function ManualsDashboardClient({ data }: ManualsDashboardClientProps) {
Manuals Indexed - {liveData.semanticIndex?.total_manuals || 0} + {data.semanticIndex?.total_manuals || 0}

Note: Using placeholder embeddings. Ready for production integration. diff --git a/components/manuals-page-experience.tsx b/components/manuals-page-experience.tsx index 4c6fbf39..1d341383 100644 --- a/components/manuals-page-experience.tsx +++ b/components/manuals-page-experience.tsx @@ -1,10 +1,9 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' +import { useMemo } from 'react' import { PublicInset, PublicPageHeader } from '@/components/public-surface' import { ManualsPageShell } from '@/components/manuals-page-shell' import { groupManuals, getCategories, getManufacturers } from '@/lib/manuals-catalog' -import { fetchPublishedManualsCatalog } from '@/lib/manuals-live-catalog' import type { Manual } from '@/lib/manuals-types' interface ManualsPageExperienceProps { @@ -12,34 +11,9 @@ interface ManualsPageExperienceProps { } export function ManualsPageExperience({ initialManuals }: ManualsPageExperienceProps) { - const [manuals, setManuals] = useState(initialManuals) - - useEffect(() => { - let cancelled = false - - void (async () => { - const liveManuals = await fetchPublishedManualsCatalog() - if (cancelled || liveManuals.length === 0) { - return - } - - setManuals((current) => { - if (liveManuals.length === current.length) { - return current - } - - return liveManuals - }) - })() - - return () => { - cancelled = true - } - }, []) - - const groupedManuals = useMemo(() => groupManuals(manuals), [manuals]) - const manufacturers = useMemo(() => getManufacturers(manuals), [manuals]) - const categories = useMemo(() => getCategories(manuals), [manuals]) + const groupedManuals = useMemo(() => groupManuals(initialManuals), [initialManuals]) + const manufacturers = useMemo(() => getManufacturers(initialManuals), [initialManuals]) + const categories = useMemo(() => getCategories(initialManuals), [initialManuals]) return ( <> @@ -50,14 +24,14 @@ export function ManualsPageExperience({ initialManuals }: ManualsPageExperienceP > - {manuals.length} manuals available from {manufacturers.length} manufacturers + {initialManuals.length} manuals available from {manufacturers.length} manufacturers

("manuals:list") const MANUALS_DASHBOARD = makeFunctionReference<"query">("manuals:dashboard") -let cachedServerConvexClient: ConvexHttpClient | null | undefined +let cachedServerConvexAdminClient: ConvexHttpClient | null | undefined +let cachedServerConvexPublicClient: ConvexHttpClient | null | undefined -function getServerConvexClient() { - if (cachedServerConvexClient !== undefined) { - return cachedServerConvexClient +function getServerConvexClient(useAdminAuth: boolean) { + const cacheKey = useAdminAuth ? 'admin' : 'public' + const cachedClient = useAdminAuth ? cachedServerConvexAdminClient : cachedServerConvexPublicClient + if (cachedClient !== undefined) { + return cachedClient } const convexUrl = process.env.CONVEX_URL || process.env.NEXT_PUBLIC_CONVEX_URL if (!convexUrl) { - cachedServerConvexClient = null - return cachedServerConvexClient + if (useAdminAuth) { + cachedServerConvexAdminClient = null + return cachedServerConvexAdminClient + } + + cachedServerConvexPublicClient = null + return cachedServerConvexPublicClient } const client = new ConvexHttpClient(convexUrl) const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY - if (adminKey) { + if (useAdminAuth && adminKey) { client.setAdminAuth(adminKey) } - cachedServerConvexClient = client - return cachedServerConvexClient + if (cacheKey === 'admin') { + cachedServerConvexAdminClient = client + return cachedServerConvexAdminClient + } + + cachedServerConvexPublicClient = client + return cachedServerConvexPublicClient +} + +async function queryManualsWithAuthFallback( + label: string, + queryRef: ReturnType>, + fallback: TData, +): Promise { + const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY + const adminClient = getServerConvexClient(true) + + if (adminClient) { + try { + return await adminClient.query(queryRef, {}) as TData + } catch (error) { + console.error(`[convex-service] ${label} admin query failed`, error) + if (!adminKey) { + return fallback + } + } + } + + const publicClient = getServerConvexClient(false) + if (!publicClient) { + return fallback + } + + return await safeFetchQuery(`${label}.public`, publicClient.query(queryRef, {}), fallback) } type ConvexOrderItem = { @@ -167,12 +207,7 @@ export async function listConvexManuals(): Promise { return [] } - const client = getServerConvexClient() - if (!client) { - return [] - } - - const manuals = await safeFetchQuery("manuals.list", client.query(LIST_MANUALS, {}), [] as ConvexManualDoc[]) + const manuals = await queryManualsWithAuthFallback("manuals.list", LIST_MANUALS, [] as ConvexManualDoc[]) return (manuals as ConvexManualDoc[]).map(mapConvexManual) } @@ -181,12 +216,7 @@ export async function getConvexManualDashboard() { return null } - const client = getServerConvexClient() - if (!client) { - return null - } - - return await safeFetchQuery("manuals.dashboard", client.query(MANUALS_DASHBOARD, {}), null) + return await queryManualsWithAuthFallback("manuals.dashboard", MANUALS_DASHBOARD, null) } export async function getConvexOrderMetrics() { diff --git a/lib/manuals-live-catalog.ts b/lib/manuals-live-catalog.ts deleted file mode 100644 index e4953ce9..00000000 --- a/lib/manuals-live-catalog.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ConvexHttpClient } from 'convex/browser' -import { api } from '@/convex/_generated/api' -import type { Manual } from '@/lib/manuals-types' - -type ConvexManualDoc = { - filename: string - path: string - manufacturer: string - category: string - size?: number - lastModified?: number - searchTerms?: string[] - commonNames?: string[] - thumbnailUrl?: string -} - -function getClient() { - const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL - if (!convexUrl) { - return null - } - - return new ConvexHttpClient(convexUrl) -} - -function mapManual(manual: ConvexManualDoc): Manual { - return { - filename: manual.filename, - path: manual.path, - manufacturer: manual.manufacturer, - category: manual.category, - size: manual.size, - lastModified: manual.lastModified ? new Date(manual.lastModified) : undefined, - searchTerms: manual.searchTerms, - commonNames: manual.commonNames, - thumbnailUrl: manual.thumbnailUrl, - } -} - -export async function fetchPublishedManualsCatalog(): Promise { - const client = getClient() - if (!client) { - return [] - } - - try { - const manuals = await client.query(api.manuals.list, {}) - return (manuals as ConvexManualDoc[]).map(mapManual) - } catch (error) { - console.error('[manuals-live-catalog] catalog refresh failed', error) - return [] - } -} - -export async function fetchPublishedManualsDashboard() { - const client = getClient() - if (!client) { - return null - } - - try { - return await client.query(api.manuals.dashboard, {}) - } catch (error) { - console.error('[manuals-live-catalog] dashboard refresh failed', error) - return null - } -}