import React, { useEffect, useState } from 'react'; import { Wrench, Database, Layers, ClipboardCheck, Workflow } from 'lucide-react'; import { api } from '../api/client'; import { NavigationHeader } from '../components/NavigationHeader'; import { BatchCatalogPanel } from './BatchCatalogPanel'; import { UploadResultsPanel } from './UploadResultsPanel'; import { OrchestrationPanel } from './OrchestrationPanel'; /* * The admin panel carries the read-only project overview plus three ingestion * views: a multi-file batch upload, the results of a colleague's upload, and * the Dagster orchestration screen. * * "Upload Results" was the Review Inbox until it stopped being useful as one: * UPLOAD_AUTORUN defaults to true, so a colleague's upload runs on arrival and * the inbox is permanently empty. The tab now leads with what the pipeline did * with the newest colleague upload, and the approval gate reveals itself only * when files are actually waiting - which is what the badge below counts. * * TWO KINDS OF REMOVAL HAVE HAPPENED HERE, AND THE DIFFERENCE MATTERS. * * Three early sub-tabs (train/test dataset upload, stock-remaining discount * allocator, and a trigger for the brand ingestion pipeline) were retired from * the UI ONLY. Their endpoints are all still registered and still work; brand * ingestion in particular is reachable from * `python cli/ingest_brand.py ""` and from POST /api/catalog/generate. * * "Store Catalog Ingestion" - a single-file uploader - went further: the tab, * its panel, and the /api/admin/store-catalog/* routes behind it were all * deleted, because Batch Catalog Ingestion does the same job for up to twenty * files and POST /api/uploads/catalog now runs a colleague's upload * automatically. Do not go looking for those endpoints; they are gone. The * eleven-stage pipeline they drove is not - it lives in * app/core/store_catalog_pipeline.py and every route below still runs it. */ const TABS = [ { id: 'project', label: 'Project Overview Details', icon: Database }, { id: 'batch', label: 'Batch Catalog Ingestion', icon: Layers }, // The id stays 'inbox' - the badge, the render guard and the onBatchStarted // wiring all key off it, and the tab still owns the review inbox; it just // leads with the pipeline result now. { id: 'inbox', label: 'Upload Results', icon: ClipboardCheck }, { id: 'orchestration', label: 'Dagster Orchestration', icon: Workflow }, ]; // How often the pending count is refreshed. Slower than the 3s a running batch // polls at, because this is a "has anything arrived?" question, not progress on // something already in flight - and it runs on whichever tab is open, so it is // the one poll in the app that never stops while the page is up. const INBOX_POLL_MS = 15000; export function AdminPage() { const [activeTab, setActiveTab] = useState('project'); const [projectDetails, setProjectDetails] = useState(null); const [loadingProject, setLoadingProject] = useState(true); const [pendingUploads, setPendingUploads] = useState(0); // Handed to BatchCatalogPanel when a run is started from the inbox. const [adoptedBatch, setAdoptedBatch] = useState(null); const fetchProjectDetails = async () => { try { setProjectDetails(await api.getProjectDetails()); } catch (e) { console.warn('Could not fetch project details', e); } finally { setLoadingProject(false); } }; // This effect also drove a 3-second poll of ingestion job status; that went // with the ingest panel, leaving a single fetch on mount. useEffect(() => { fetchProjectDetails(); }, []); // The badge. Polled here rather than inside UploadResultsPanel so the count // is live // whichever tab is open - the whole point is finding out that a colleague // sent something while you were looking at a different screen. A failed poll // is swallowed: a transient network blip must not replace the panel with an // error, and the next tick retries. // // Expect a steady zero on a deployment running UPLOAD_AUTORUN=true: uploads // go straight into the pipeline and never wait for review, so there is // nothing to be notified about. The badge renders only above zero, so that // reads as "nothing to do" rather than as a broken counter, and the poll // stays because the setting can be turned off without a redeploy. useEffect(() => { let alive = true; const check = async () => { try { const { pending_count: count } = await api.listInbox(); if (alive) setPendingUploads(count || 0); } catch { /* next tick retries */ } }; check(); const timer = setInterval(check, INBOX_POLL_MS); return () => { alive = false; clearInterval(timer); }; }, []); return (
{TABS.map(({ id, label, icon: Icon }) => ( ))}
{activeTab === 'batch' && } {activeTab === 'inbox' && ( { setAdoptedBatch(batch); setActiveTab('batch'); setPendingUploads((n) => Math.max(0, n - batch.files_total)); }} /> )} {/* Starts from the same inbox but hands the batch to Dagster, and stays put to show the run rather than handing off to the Batch tab - the stage-by-stage view is the reason to be on this tab at all. */} {activeTab === 'orchestration' && } {activeTab === 'project' && (

Existing Project Details & System Architecture

High-level overview of active brand catalogs, PostgreSQL vector database tables, S3 image storage pipeline, and store intelligence ML models.

{loadingProject ? (
Loading project details...
) : projectDetails ? (

Total Brands

{projectDetails.total_brands}

Ingested brand tables in pgvector

Total Products

{projectDetails.total_products}

Unique products with vector embeddings

S3 Storage Status

{projectDetails.s3_image_status}

Bucket: {projectDetails.storage_bucket}

System Version

{projectDetails.version}

Multi-Store & RAG Engine

) : null}
{projectDetails?.brand_product_counts && (

Ingested Brand Database Breakdown

{Object.entries(projectDetails.brand_product_counts).map(([bName, count]) => (
{bName} {count} items
))}
)}
)}
); }