import { join } from 'path' const PORT = 3456 const PIPELINE_ROOT = process.env.PIPELINE_ROOT || join(import.meta.dir, '..', 'data', 'pipeline') const INTEL_ROOT = join(import.meta.dir, '..', 'data', 'intelligence') const DIST_ROOT = join(import.meta.dir, '..', 'dist') const IS_PROD = process.env.NODE_ENV === 'production' const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Content-Type': 'application/json' } async function readJson(path: string) { const f = Bun.file(path) if (!(await f.exists())) return null return f.json() } async function getProducts() { const catalog = await readJson(join(PIPELINE_ROOT, 'catalog.json')) as any[] const images = await readJson(join(PIPELINE_ROOT, 'product-images.json')) as Record return catalog.map((p: any) => { const img = images?.[p.sku] || {} const intelPath = join(INTEL_ROOT, p.sku, 'review-aspects.json') return { sku: p.sku, name: img.name || p.sku, totalReviews: p.total_reviews || 0, variants: p.variants || [], imageUrl: img.image_url || null, productUrl: img.url ? `https://www.justvitamins.co.uk${img.url}` : null, hasIntelligence: Bun.file(intelPath).exists() } }).sort((a: any, b: any) => b.totalReviews - a.totalReviews) } async function serveStatic(pathname: string): Promise { if (!IS_PROD) return null // Strip query string, decode const clean = decodeURIComponent(pathname.split('?')[0]) const filePath = join(DIST_ROOT, clean === '/' ? 'index.html' : clean) const f = Bun.file(filePath) if (await f.exists()) return new Response(f) // SPA fallback — serve index.html for all non-file routes const index = Bun.file(join(DIST_ROOT, 'index.html')) if (await index.exists()) return new Response(index, { headers: { 'Content-Type': 'text/html' } }) return null } const server = Bun.serve({ port: PORT, async fetch(req) { const url = new URL(req.url) const path = url.pathname if (req.method === 'OPTIONS') return new Response(null, { headers: cors }) // API routes if (path.startsWith('/api/')) { // GET /api/products if (path === '/api/products') { const products = await getProducts() const resolved = await Promise.all( products.map(async (p: any) => ({ ...p, hasIntelligence: await p.hasIntelligence })) ) return new Response(JSON.stringify(resolved), { headers: cors }) } // GET /api/intelligence/:sku/:view const intelMatch = path.match(/^\/api\/intelligence\/([^/]+)\/([^/]+)$/) if (intelMatch) { const [, sku, view] = intelMatch if (view === 'all') { const views = ['review-aspects', 'strengths-weaknesses', 'conversion-blockers', 'improvements', 'image-audit', 'photo-brief'] const result: Record = {} for (const v of views) { const data = await readJson(join(INTEL_ROOT, sku, `${v}.json`)) if (data) result[v.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase())] = data } return Object.keys(result).length ? new Response(JSON.stringify(result), { headers: cors }) : new Response(JSON.stringify({ error: 'No intelligence data' }), { status: 404, headers: cors }) } const data = await readJson(join(INTEL_ROOT, sku, `${view}.json`)) return data ? new Response(JSON.stringify(data), { headers: cors }) : new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors }) } return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors }) } // Static file serving in production const static_ = await serveStatic(path) if (static_) return static_ return new Response('Not found', { status: 404 }) } }) console.log(`JV Dashboard running on http://localhost:${PORT} (${IS_PROD ? 'production' : 'dev'})`)