From dd5c70fd5471a4c7eb1e7c9d9b90461fe20b13a9 Mon Sep 17 00:00:00 2001 From: sriram Date: Mon, 31 Aug 2026 16:33:32 +0530 Subject: [PATCH] Frontend Feature Removal of single file manual --- src/api/client.js | 28 ++-- src/pages/AdminPage.jsx | 30 ++-- src/pages/BatchCatalogPanel.jsx | 17 +- src/pages/StoreCatalogPanel.jsx | 265 -------------------------------- 4 files changed, 37 insertions(+), 303 deletions(-) delete mode 100644 src/pages/StoreCatalogPanel.jsx diff --git a/src/api/client.js b/src/api/client.js index 49add64..02c7f56 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -337,27 +337,17 @@ export const api = { body: JSON.stringify({ store_id, rules }), }), - // --- Admin: store-catalog ingestion (Excel -> 11-stage pipeline -> brand tables) --- - // See app/api/routers/store_catalog.py. `preview` parses only, so the - // operator can check the column mapping before paying for a full scrape. - previewStoreCatalog: (file) => upload('/api/admin/store-catalog/preview', file), - - // The two flags are query params, not form fields: the endpoint takes them - // as plain query arguments alongside the multipart body. - ingestStoreCatalog: (file, { use_llm = true, fetch_images = true } = {}) => - upload(`/api/admin/store-catalog/ingest${qs({ use_llm, fetch_images })}`, file), - - getStoreCatalogJob: (jobId) => - request(`/api/admin/store-catalog/jobs/${encodeURIComponent(jobId)}`), - // --- Admin: batch catalog ingestion (many spreadsheets -> one batch) --- - // The multi-file sibling of the block above. See - // app/api/routers/batch_catalog.py. + // Excel -> the 11-stage pipeline -> brand tables. See + // app/api/routers/batch_catalog.py. `preview` parses only, so the operator + // can check the column mapping before paying for a full scrape. // - // NOTE the flag defaults are the OPPOSITE of ingestStoreCatalog's. One file - // with image search on is a considered trade; twenty files is thousands of - // outbound requests and a Playwright subprocess per row, on a single-vCPU - // container that is also serving the API. A batch opts in. + // NOTE the flag defaults here are OFF. A single file with image search on is + // a considered trade; twenty files is thousands of outbound requests and a + // Playwright subprocess per row, on a single-vCPU container that is also + // serving the API. A batch opts in. (There used to be a single-file sibling, + // `ingestStoreCatalog`, which defaulted both ON for exactly that reason; it + // was removed with the Store Catalog Ingestion admin tab.) previewCatalogBatch: (files) => uploadMany('/api/admin/catalog-batch/preview', files), diff --git a/src/pages/AdminPage.jsx b/src/pages/AdminPage.jsx index babe249..c207b1e 100644 --- a/src/pages/AdminPage.jsx +++ b/src/pages/AdminPage.jsx @@ -1,26 +1,34 @@ import React, { useEffect, useState } from 'react'; -import { Wrench, Database, UploadCloud, Layers, Inbox, Workflow } from 'lucide-react'; +import { Wrench, Database, Layers, Inbox, Workflow } from 'lucide-react'; import { api } from '../api/client'; import { NavigationHeader } from '../components/NavigationHeader'; -import { StoreCatalogPanel } from './StoreCatalogPanel'; import { BatchCatalogPanel } from './BatchCatalogPanel'; import { InboxPanel } from './InboxPanel'; import { OrchestrationPanel } from './OrchestrationPanel'; /* - * The admin panel carries two views: the read-only project overview, and the - * store-catalog ingestion workspace. + * The admin panel carries the read-only project overview plus three ingestion + * views: a multi-file batch upload, the review inbox, and the Dagster + * orchestration screen. * - * It briefly carried neither tab bar nor second view - three earlier sub-tabs - * (train/test dataset upload, stock-remaining discount allocator, and a - * trigger for the brand ingestion pipeline) were retired from the UI, leaving - * only the overview. Their endpoints are all still registered and still work; - * brand ingestion in particular is reachable from + * 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: 'ingest', label: 'Store Catalog Ingestion', icon: UploadCloud }, { id: 'batch', label: 'Batch Catalog Ingestion', icon: Layers }, { id: 'inbox', label: 'Review Inbox', icon: Inbox }, { id: 'orchestration', label: 'Dagster Orchestration', icon: Workflow }, @@ -121,8 +129,6 @@ export function AdminPage() { ))} - {activeTab === 'ingest' && } - {activeTab === 'batch' && } {activeTab === 'inbox' && ( diff --git a/src/pages/BatchCatalogPanel.jsx b/src/pages/BatchCatalogPanel.jsx index e2fc64f..ac631de 100644 --- a/src/pages/BatchCatalogPanel.jsx +++ b/src/pages/BatchCatalogPanel.jsx @@ -9,12 +9,14 @@ import { api } from '../api/client'; /* * Several store spreadsheets -> one batch -> the same 11-stage pipeline. * - * The multi-file sibling of StoreCatalogPanel, which is left exactly as it - * was. What is different here is the unit of work: five files are ONE batch - * with one id, so the question a colleague actually asks - "did the drop - * land?" - has one answer instead of five. + * This began as the multi-file sibling of a single-file StoreCatalogPanel; + * that panel and its /api/admin/store-catalog/* routes have since been deleted, + * and this is now the only admin upload screen. What made it the better of the + * two is the unit of work: five files are ONE batch with one id, so the + * question a colleague actually asks - "did the drop land?" - has one answer + * instead of five. * - * Two things are deliberately not like the single-file panel: + * Two things are deliberately not like that single-file panel was: * * - the enrichment toggles default OFF. At one file, image search is a * considered trade; at twenty it is thousands of outbound requests on a @@ -80,8 +82,9 @@ export function BatchCatalogPanel({ adoptBatch = null }) { }, [adoptBatch]); // One interval for the component's lifetime, reading the id from a ref - // rather than closing over it. Same reasoning as StoreCatalogPanel: starting - // a second batch must not leave an older interval polling the previous one. + // rather than closing over it. The reason is that starting a second batch + // must not leave an older interval polling the previous one. OrchestrationPanel + // does the same thing for the same reason. useEffect(() => { pollRef.current = setInterval(async () => { const id = batchIdRef.current; diff --git a/src/pages/StoreCatalogPanel.jsx b/src/pages/StoreCatalogPanel.jsx deleted file mode 100644 index 2ada849..0000000 --- a/src/pages/StoreCatalogPanel.jsx +++ /dev/null @@ -1,265 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { - UploadCloud, FileSpreadsheet, PlayCircle, Loader2, CheckCircle2, - XCircle, AlertTriangle, ArrowRight, Table2, -} from 'lucide-react'; -import { api } from '../api/client'; - -/* - * Store spreadsheet -> 11-stage scraping pipeline -> brand tables. - * - * Two deliberate steps. "Preview" only parses the file and shows how each - * column was understood; the mapping from a store's own headers onto catalog - * fields is a guess, and discovering that "Item" was read as the description - * after a 2000-row scrape is expensive. "Start Ingestion" is the committing - * action, and from then on this polls the job for stage + row progress. - */ - -const POLL_MS = 3000; -const TERMINAL = new Set(['done', 'failed']); - -export function StoreCatalogPanel() { - const [file, setFile] = useState(null); - const [preview, setPreview] = useState(null); - const [previewing, setPreviewing] = useState(false); - const [starting, setStarting] = useState(false); - const [error, setError] = useState(''); - const [job, setJob] = useState(null); - - const pollRef = useRef(null); - const jobIdRef = useRef(null); - - // One interval for the component's lifetime. It reads the job id from a ref - // rather than closing over it, so starting a second ingestion does not leave - // an older interval polling the previous job. - useEffect(() => { - pollRef.current = setInterval(async () => { - const id = jobIdRef.current; - if (!id) return; - try { - const next = await api.getStoreCatalogJob(id); - setJob(next); - if (TERMINAL.has(next.status)) jobIdRef.current = null; - } catch { - /* a dropped poll is not worth surfacing; the next tick retries */ - } - }, POLL_MS); - return () => clearInterval(pollRef.current); - }, []); - - const onPick = (e) => { - setFile(e.target.files?.[0] || null); - setPreview(null); - setJob(null); - setError(''); - }; - - const handlePreview = async () => { - if (!file) return; - setPreviewing(true); - setError(''); - try { - setPreview(await api.previewStoreCatalog(file)); - } catch (err) { - setError(err?.message || 'Could not read that file.'); - } finally { - setPreviewing(false); - } - }; - - const handleIngest = async () => { - if (!file) return; - setStarting(true); - setError(''); - try { - const started = await api.ingestStoreCatalog(file); - jobIdRef.current = started.job_id; - setJob(started); - } catch (err) { - setError(err?.message || 'Could not start the ingestion job.'); - } finally { - setStarting(false); - } - }; - - const running = job && !TERMINAL.has(job.status); - const result = job?.result; - - return ( -
- {/* ---- 1. File selection --------------------------------------- */} -
-

- Store Catalog Ingestion -

-

- Upload a store's product list (.xlsx, .xls, .csv or .tsv). Every row is run through - the 11-stage scraping pipeline to fill in whatever the sheet does not supply — - images, price bands, pack sizes, SKU, barcode, HSN/GST and FSSAI — then validated - and written into the matching brand table. Columns the sheet already fills are never - overwritten. -

- -
- - - - - -
- - {error && ( -
- {error} -
- )} -
- - {/* ---- 2. Column mapping preview ------------------------------- */} - {preview && ( -
-

- Column mapping — {preview.rows_total} row(s) -

-

- {preview.brand_column_present - ? 'Brand column found.' - : 'No brand column; the brand will be inferred from each product name.'} -

- -
- {Object.entries(preview.recognised_columns).map(([field, column]) => ( -
- {column} - - {field} -
- ))} -
- - {preview.unrecognised_columns?.length > 0 && ( -

- - Ignored (no matching catalog field): {preview.unrecognised_columns.join(', ')} -

- )} -
- )} - - {/* ---- 3. Live stage tracker ----------------------------------- */} - {job && ( -
-
-

- {job.status === 'done' && } - {job.status === 'failed' && } - {running && } - Pipeline — {job.filename} -

- {job.status} -
- - {job.stage_index > 0 && ( - <> -
- - Stage {job.stage_index}/{job.total_stages} — {job.stage_name} - - - {job.rows_done}/{job.rows_total} rows - -
-
-
-
- - )} - - {job.detail &&

{job.detail}

} - - {result && ( - <> -
- - - - - -
- - {result.brands?.length > 0 && ( -

- Brands touched: {result.brands.join(', ')} -

- )} - - `${r.product_name} ${r.size || ''} — ${r.reason}`) - } /> - `row ${e.row}: ${e.product_name || '(unnamed)'} — ${e.error}`) - } /> - - - )} -
- )} -
- ); -} - -const TONES = { - emerald: 'border-emerald-500/20 bg-emerald-500/5 text-emerald-700', - blue: 'border-blue-500/20 bg-blue-500/5 text-blue-700', - slate: 'border-ink-900/10 bg-white text-slate-600', - maroon: 'border-maroon-500/20 bg-maroon-100/40 text-maroon-600', - amber: 'border-amber-500/20 bg-amber-500/5 text-amber-700', -}; - -function Stat({ label, value, tone }) { - return ( -
-

{label}

-

{value ?? 0}

-
- ); -} - -function IssueList({ title, items }) { - if (!items || items.length === 0) return null; - return ( -
-

{title} ({items.length})

-
    - {items.map((text, idx) => ( -
  • - {text} -
  • - ))} -
-
- ); -}