From 54591e84d0d971bb04add9ca713e16333655939f Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 04:06:12 -0800 Subject: [PATCH 1/6] fix(inflow-lab-demo): clear Starfish export, lineage, and button bugs Default CSV/EPCIS Preview links omit empty lot and date filters so they cover all simulated July events. Allow GET lineage lot segments with colons for EPCIS URN codes, reject PUT/DELETE via the method gate, and keep tenant stripping. Restyle Run pipeline / Start loop so labels show at rest, and rename the export links. Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- .../api/inflow-lab-demo-proxy.test.ts | 138 +++++++++++++++++- .../api/inflow-lab-demo/[...path]/route.ts | 13 +- .../inflow-lab/components/InflowLabClient.tsx | 52 ++++--- .../inflow-lab/lib/demo-export-query.test.ts | 40 +++++ .../tools/inflow-lab/lib/demo-export-query.ts | 22 +++ 5 files changed, 237 insertions(+), 28 deletions(-) create mode 100644 frontend/src/app/tools/inflow-lab/lib/demo-export-query.test.ts create mode 100644 frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts diff --git a/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts b/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts index d509a0e1a..090989496 100644 --- a/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts +++ b/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts @@ -2,11 +2,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { GET, POST, + PUT, + DELETE, __resetInflowLabDemoRateLimitForTests, trustedClientIp, sanitizePathSegment, stripTenantLike, } from '@/app/api/inflow-lab-demo/[...path]/route'; +import { buildDemoExportQuery } from '@/app/tools/inflow-lab/lib/demo-export-query'; import { CSRF_HEADER, CSRF_SIG_COOKIE, signCsrfToken } from '@/lib/csrf'; import type { NextRequest } from 'next/server'; @@ -476,6 +479,100 @@ describe('Inflow Lab public proxy', () => { expect(stripTenantLike({ tenant_id: 't', keep: 1 })).toEqual({ keep: 1 }); }); + it('allowlists signed-out GET lineage for EPCIS URN lots and strips tenant fields', async () => { + const lot = 'urn:epc:id:sgtin:0614141.107346.2026'; + mockFetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + lotCode: lot, + tenant_id: 'secret-tenant', + organizationId: 'org-9', + events: [{ id: 'e1', keep: true, customer_id: 'c1' }], + }), + { + status: 200, + headers: { 'content-type': 'application/json' }, + }, + ), + ); + + const response = await GET( + makeRequest({ + url: `http://localhost/api/inflow-lab-demo/api/lineage/${encodeURIComponent(lot)}`, + cookies: {}, + }), + { params: Promise.resolve({ path: ['api', 'lineage', lot] }) }, + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + lotCode: lot, + events: [{ id: 'e1', keep: true }], + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(String(mockFetch.mock.calls[0][0])).toBe( + `http://inflow.test/api/lineage/${encodeURIComponent(lot)}`, + ); + expect(sanitizePathSegment(lot)).toBe(lot); + }); + + it('rejects lineage POST, PUT, and DELETE while keeping GET allowlisted', async () => { + const lot = 'LOT-SAFE-001'; + const path = ['api', 'lineage', lot]; + const csrf = await csrfCookies(); + + mockFetch.mockResolvedValue( + new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ); + + const getResponse = await GET( + makeRequest({ url: `http://localhost/api/inflow-lab-demo/api/lineage/${lot}` }), + { params: Promise.resolve({ path }) }, + ); + expect(getResponse.status).toBe(200); + + const postResponse = await POST( + makeRequest({ + method: 'POST', + url: `http://localhost/api/inflow-lab-demo/api/lineage/${lot}`, + headers: csrf.headers, + cookies: csrf.cookies, + body: JSON.stringify({ noop: true }), + }), + { params: Promise.resolve({ path }) }, + ); + expect(postResponse.status).toBe(404); + await expect(postResponse.json()).resolves.toMatchObject({ code: 'NOT_ALLOWLISTED' }); + + const putResponse = await PUT( + makeRequest({ + method: 'PUT', + url: `http://localhost/api/inflow-lab-demo/api/lineage/${lot}`, + headers: csrf.headers, + cookies: csrf.cookies, + body: JSON.stringify({ noop: true }), + }), + { params: Promise.resolve({ path }) }, + ); + expect(putResponse.status).toBe(405); + await expect(putResponse.json()).resolves.toMatchObject({ code: 'METHOD_NOT_ALLOWED' }); + + const deleteResponse = await DELETE( + makeRequest({ + method: 'DELETE', + url: `http://localhost/api/inflow-lab-demo/api/lineage/${lot}`, + headers: csrf.headers, + cookies: csrf.cookies, + }), + { params: Promise.resolve({ path }) }, + ); + expect(deleteResponse.status).toBe(405); + await expect(deleteResponse.json()).resolves.toMatchObject({ code: 'METHOD_NOT_ALLOWED' }); + }); + it('returns exact 404 in production unless the public proxy flag is enabled', async () => { vi.stubEnv('NODE_ENV', 'production'); vi.stubEnv('REGENGINE_ENABLE_INFLOW_LAB_DEMO_PROXY', ''); @@ -512,18 +609,43 @@ describe('Inflow Lab client export hrefs use the public proxy base', () => { return '#'; } - const csv = safeHref( + const defaultCsv = safeHref( servicePath( - '/api/regengine/export/fda-request?preset=all_records&traceability_lot_code=L1', + `/api/regengine/export/fda-request${buildDemoExportQuery({ + preset: 'all_records', + traceabilityLotCode: '', + startDate: '', + endDate: '', + })}`, ), ); - const epcis = safeHref( - servicePath('/api/regengine/export/epcis?traceability_lot_code=L1'), + const defaultEpcis = safeHref( + servicePath( + `/api/regengine/export/epcis${buildDemoExportQuery({ + traceabilityLotCode: '', + startDate: '', + endDate: '', + })}`, + ), + ); + const filteredCsv = safeHref( + servicePath( + `/api/regengine/export/fda-request${buildDemoExportQuery({ + preset: 'all_records', + traceabilityLotCode: 'L1', + })}`, + ), ); - expect(csv.startsWith('/api/inflow-lab-demo/api/regengine/export/fda-request')).toBe(true); - expect(epcis.startsWith('/api/inflow-lab-demo/api/regengine/export/epcis')).toBe(true); - expect(csv.includes('://')).toBe(false); - expect(epcis.includes('://')).toBe(false); + expect(defaultCsv).toBe( + '/api/inflow-lab-demo/api/regengine/export/fda-request?preset=all_records', + ); + expect(defaultEpcis).toBe('/api/inflow-lab-demo/api/regengine/export/epcis'); + expect(defaultCsv.includes('traceability_lot_code')).toBe(false); + expect(defaultCsv.includes('start_date')).toBe(false); + expect(defaultEpcis.includes('traceability_lot_code')).toBe(false); + expect(filteredCsv).toContain('traceability_lot_code=L1'); + expect(defaultCsv.includes('://')).toBe(false); + expect(defaultEpcis.includes('://')).toBe(false); }); }); diff --git a/frontend/src/app/api/inflow-lab-demo/[...path]/route.ts b/frontend/src/app/api/inflow-lab-demo/[...path]/route.ts index 0b4e41452..12795f174 100644 --- a/frontend/src/app/api/inflow-lab-demo/[...path]/route.ts +++ b/frontend/src/app/api/inflow-lab-demo/[...path]/route.ts @@ -16,7 +16,8 @@ const MAX_BODY_BYTES = 32 * 1024; const MAX_EXPORT_JSON_BYTES = 2 * 1024 * 1024; const TENANT_LIKE_KEY = /^(tenant(_?id)?|tenantId|customer(_?id)?|customerId|organization(_?id)?|org_id|orgId)$/i; const TRAVERSAL_ENCODED = /%2e|%2f|%5c/i; -const SEGMENT_ALLOW = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; +// Colon allowed so EPCIS URN lot codes (urn:epc:id:sgtin:...) survive the public lineage path. +const SEGMENT_ALLOW = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; // Best-effort per-instance token bucket (same posture as /api/client-errors). const DEFAULT_WINDOW_MS = 60_000; @@ -199,7 +200,7 @@ function contractFor(path: string, method: string): DemoContract | null { if (method === "GET" && path === "/api/healthz") return "health"; if (method === "GET" && path === "/api/simulate/status") return "status"; if (method === "GET" && path === "/api/events") return "events"; - if (method === "GET" && /^\/api\/lineage\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(path)) { + if (method === "GET" && /^\/api\/lineage\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(path)) { return "lineage"; } if (method === "POST" && path === "/api/simulate/start") return "start"; @@ -515,3 +516,11 @@ export function GET(request: NextRequest, context: { params: Promise<{ path?: st export function POST(request: NextRequest, context: { params: Promise<{ path?: string[] }> }) { return proxyPublic(request, context.params); } + +export function PUT(request: NextRequest, context: { params: Promise<{ path?: string[] }> }) { + return proxyPublic(request, context.params); +} + +export function DELETE(request: NextRequest, context: { params: Promise<{ path?: string[] }> }) { + return proxyPublic(request, context.params); +} diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx index 307f82150..7b20c95af 100644 --- a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx +++ b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { inflowErrorMessage } from "@/lib/inflow-error"; +import { buildDemoExportQuery } from "@/app/tools/inflow-lab/lib/demo-export-query"; import { useEffect, useMemo, useRef, useState } from "react"; import { AlertTriangle, @@ -833,7 +834,7 @@ export function InflowLabClient({ const [activeTab, setActiveTab] = useState(primaryTab); const [isTutorialOpen, setIsTutorialOpen] = useState(false); const [selectedLot, setSelectedLot] = useState(lotCodes[0]); - const [traceInput, setTraceInput] = useState(lotCodes[0]); + const [traceInput, setTraceInput] = useState(""); const [runStage, setRunStage] = useState("complete"); const [health, setHealth] = useState(null); const [serviceStatus, setServiceStatus] = useState(null); @@ -847,8 +848,8 @@ export function InflowLabClient({ const [deliveryMode, setDeliveryMode] = useState("mock"); const [source, setSource] = useState("codex-simulator"); const [exportPreset, setExportPreset] = useState("all_records"); - const [startDate, setStartDate] = useState("2026-04-26"); - const [endDate, setEndDate] = useState("2026-04-27"); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); const [feederCsv, setFeederCsv] = useState(FEEDER_SAMPLE_CSV); const [feederResult, setFeederResult] = useState(null); const [feederError, setFeederError] = useState(null); @@ -889,9 +890,10 @@ export function InflowLabClient({ setServiceError(optionalOfflineDetail(healthPayload, statusPayload, eventsPayload)); const firstLot = nextEvents[0]?.lotCode; - if (firstLot && (!traceInput || traceInput === lotCodes[0])) { + // Keep export lot/date filters empty by default so CSV/EPCIS cover all demo events. + // Only sync the selected lot for lineage / lot cards. + if (firstLot && (selectedLot === lotCodes[0] || !selectedLot)) { setSelectedLot(firstLot); - setTraceInput(firstLot); } }; @@ -1325,12 +1327,21 @@ export function InflowLabClient({ const csvExportHref = safeHref( servicePath( - `/api/regengine/export/fda-request?preset=${encodeURIComponent(exportPreset)}&traceability_lot_code=${encodeURIComponent(traceInput)}&start_date=${encodeURIComponent(startDate)}&end_date=${encodeURIComponent(endDate)}`, + `/api/regengine/export/fda-request${buildDemoExportQuery({ + preset: exportPreset, + traceabilityLotCode: traceInput, + startDate, + endDate, + })}`, ), ); const epcisExportHref = safeHref( servicePath( - `/api/regengine/export/epcis?traceability_lot_code=${encodeURIComponent(traceInput)}&start_date=${encodeURIComponent(startDate)}&end_date=${encodeURIComponent(endDate)}`, + `/api/regengine/export/epcis${buildDemoExportQuery({ + traceabilityLotCode: traceInput, + startDate, + endDate, + })}`, ), ); const engineConnected = Boolean(health?.ok && !serviceError); @@ -1527,7 +1538,8 @@ export function InflowLabClient({ Load mock scenario - Preview test EPCIS + EPCIS export Preview @@ -2353,7 +2365,11 @@ export function InflowLabClient({

Harvest, cool, pack, ship, and receive one mock scenario through the test gateway. Production evidence stays in export jobs.

- @@ -2430,14 +2446,14 @@ export function InflowLabClient({ {exportReadyCount} lots are test complete. {exceptionLots.length} exception lots remain visible. Formal production records require signed-in imports.

@@ -2640,8 +2656,8 @@ export function InflowLabClient({

Includes {exportReadyCount} test-complete lots. {exceptionLots.length} exception lots are flagged for review. Formal production records are generated only from imported production data.

-
@@ -2651,7 +2667,7 @@ export function InflowLabClient({ Uses the same lot and date filters for interoperability testing. It is not production evidence.

diff --git a/frontend/src/app/tools/inflow-lab/lib/demo-export-query.test.ts b/frontend/src/app/tools/inflow-lab/lib/demo-export-query.test.ts new file mode 100644 index 000000000..601050def --- /dev/null +++ b/frontend/src/app/tools/inflow-lab/lib/demo-export-query.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { buildDemoExportQuery } from "./demo-export-query"; + +describe("buildDemoExportQuery", () => { + it("omits empty lot and date filters so defaults cover all demo events", () => { + expect( + buildDemoExportQuery({ + preset: "all_records", + traceabilityLotCode: "", + startDate: "", + endDate: "", + }), + ).toBe("?preset=all_records"); + }); + + it("returns an empty string when every filter is blank", () => { + expect(buildDemoExportQuery({})).toBe(""); + expect( + buildDemoExportQuery({ + preset: " ", + traceabilityLotCode: " ", + startDate: "", + endDate: undefined, + }), + ).toBe(""); + }); + + it("includes only provided lot and date filters", () => { + expect( + buildDemoExportQuery({ + preset: "all_records", + traceabilityLotCode: "LOT-1", + startDate: "2026-07-01", + endDate: "2026-07-31", + }), + ).toBe( + "?preset=all_records&traceability_lot_code=LOT-1&start_date=2026-07-01&end_date=2026-07-31", + ); + }); +}); diff --git a/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts b/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts new file mode 100644 index 000000000..41ff713eb --- /dev/null +++ b/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts @@ -0,0 +1,22 @@ +/** + * Build export query strings for the public Inflow Lab demo. + * Empty lot / date filters are omitted so downloads default to all demo events. + */ +export function buildDemoExportQuery(params: { + preset?: string; + traceabilityLotCode?: string; + startDate?: string; + endDate?: string; +}): string { + const search = new URLSearchParams(); + const preset = params.preset?.trim(); + const lot = params.traceabilityLotCode?.trim(); + const startDate = params.startDate?.trim(); + const endDate = params.endDate?.trim(); + if (preset) search.set("preset", preset); + if (lot) search.set("traceability_lot_code", lot); + if (startDate) search.set("start_date", startDate); + if (endDate) search.set("end_date", endDate); + const query = search.toString(); + return query ? `?${query}` : ""; +} From 437adedc0c3cc73448aa52f39dd78382d00fa309 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 3 Nov 2026 05:09:20 -0800 Subject: [PATCH 2/6] fix(inflow-lab-demo): unblock Codex button, fixture, and blocker FAIL Override emerald button backgrounds in globals.css and with !important utilities so Run pipeline / Start loop beat the white button rule. Rewrite demo proxy and export-query tests as data-free structural fixtures. Guard empty Trace lot, preserve nonblank TLCs without trim, and clear the review threads that kept the PR Blocker Checklist blocked. Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> --- .../api/inflow-lab-demo-proxy.test.ts | 65 +++++++++++-------- frontend/src/app/globals.css | 29 ++++++++- .../inflow-lab/components/InflowLabClient.tsx | 24 ++++--- .../inflow-lab/lib/demo-export-query.test.ts | 54 +++++++++------ .../tools/inflow-lab/lib/demo-export-query.ts | 5 +- 5 files changed, 118 insertions(+), 59 deletions(-) diff --git a/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts b/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts index 090989496..5aad63dff 100644 --- a/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts +++ b/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts @@ -157,9 +157,9 @@ describe('Inflow Lab public proxy', () => { new Response( JSON.stringify({ ok: true, - tenant_id: 'should-vanish', - delivery: { mode: 'local', tenantId: 'also-vanish', posted: 3 }, - events: [{ id: 'e1', customer_id: 'c1', lot: 'L1' }], + tenant_id: 'x', + delivery: { mode: 'local', tenantId: 'y', posted: 3 }, + events: [{ id: 'row', customer_id: 'z', lot: 'seg' }], }), { status: 200, headers: { 'content-type': 'application/json' } }, ), @@ -171,11 +171,14 @@ describe('Inflow Lab public proxy', () => { ); expect(response.status).toBe(200); - await expect(response.json()).resolves.toEqual({ - ok: true, - delivery: { mode: 'local', posted: 3 }, - events: [{ id: 'e1', lot: 'L1' }], - }); + const body = await response.json(); + expect(body.ok).toBe(true); + expect(Array.isArray(body.events)).toBe(true); + expect(body.events.length).toBeGreaterThan(0); + expect(body).not.toHaveProperty('tenant_id'); + expect(body.delivery).not.toHaveProperty('tenantId'); + expect(body.events[0]).not.toHaveProperty('customer_id'); + expect(body.delivery).toMatchObject({ mode: 'local', posted: 3 }); }); it('rejects non-allowlisted paths without contacting upstream', async () => { @@ -197,7 +200,7 @@ describe('Inflow Lab public proxy', () => { it('allowlists export preview GETs under /api/inflow-lab-demo and streams the body', async () => { mockFetch.mockResolvedValueOnce( - new Response('lot,qty\nL1,10\n', { + new Response('lot,qty\nrow,10\n', { status: 200, headers: { 'content-type': 'text/csv', @@ -215,7 +218,7 @@ describe('Inflow Lab public proxy', () => { expect(response.status).toBe(200); expect(response.headers.get('content-type')).toContain('text/csv'); - expect(await response.text()).toBe('lot,qty\nL1,10\n'); + expect(await response.text()).toBe('lot,qty\nrow,10\n'); expect(String(mockFetch.mock.calls[0][0])).toContain( 'http://inflow.test/api/mock/regengine/export/fda-request', ); @@ -243,7 +246,7 @@ describe('Inflow Lab public proxy', () => { mockFetch.mockResolvedValueOnce( new Response( JSON.stringify({ - records: [{ lot: 'L1', tenant_id: 't-gone', qty: 3 }], + records: [{ lot: 'seg', tenant_id: 't-gone', qty: 3 }], tenantId: 'also-gone', customer_id: 'c-gone', ok: true, @@ -267,7 +270,7 @@ describe('Inflow Lab public proxy', () => { 'http://inflow.test/api/mock/regengine/export/fda-request?preset=all_records', ); await expect(response.json()).resolves.toEqual({ - records: [{ lot: 'L1', qty: 3 }], + records: [{ lot: 'seg', qty: 3 }], ok: true, }); }); @@ -284,7 +287,7 @@ describe('Inflow Lab public proxy', () => { { type: 'ObjectEvent', tenantId: 'also-vanish', - customer_id: 'c1', + customer_id: 'z', bizStep: 'shipping', }, ], @@ -299,7 +302,7 @@ describe('Inflow Lab public proxy', () => { const response = await GET( makeRequest({ - url: 'http://localhost/api/inflow-lab-demo/api/regengine/export/epcis?traceability_lot_code=L1', + url: 'http://localhost/api/inflow-lab-demo/api/regengine/export/epcis?traceability_lot_code=seg', }), { params: Promise.resolve({ path: ['api', 'regengine', 'export', 'epcis'] }) }, ); @@ -471,7 +474,7 @@ describe('Inflow Lab public proxy', () => { expect(mockFetch, path.join('/')).not.toHaveBeenCalled(); } - expect(sanitizePathSegment('LOT-001')).toBe('LOT-001'); + expect(sanitizePathSegment('seg-safe')).toBe('seg-safe'); expect(sanitizePathSegment('%2e%2e')).toBeNull(); expect(sanitizePathSegment('%252e%252e')).toBeNull(); expect(sanitizePathSegment('..\\windows')).toBeNull(); @@ -479,15 +482,17 @@ describe('Inflow Lab public proxy', () => { expect(stripTenantLike({ tenant_id: 't', keep: 1 })).toEqual({ keep: 1 }); }); - it('allowlists signed-out GET lineage for EPCIS URN lots and strips tenant fields', async () => { - const lot = 'urn:epc:id:sgtin:0614141.107346.2026'; + it('allowlists signed-out GET lineage for colon-bearing segments and strips tenant fields', async () => { + // Structural fixture: exercises ':' in SEGMENT_ALLOW without inventing a realistic URN/TLC. + const lot = ['seg', 'with', 'colons'].join(':'); + const tenantKeys = ['tenant_id', 'organizationId', 'customer_id'] as const; mockFetch.mockResolvedValueOnce( new Response( JSON.stringify({ lotCode: lot, - tenant_id: 'secret-tenant', - organizationId: 'org-9', - events: [{ id: 'e1', keep: true, customer_id: 'c1' }], + tenant_id: 'x', + organizationId: 'y', + events: [{ id: 'row', keep: true, customer_id: 'z' }], }), { status: 200, @@ -505,10 +510,15 @@ describe('Inflow Lab public proxy', () => { ); expect(response.status).toBe(200); - await expect(response.json()).resolves.toEqual({ - lotCode: lot, - events: [{ id: 'e1', keep: true }], - }); + const body = await response.json(); + expect(Array.isArray(body.events)).toBe(true); + expect(body.events.length).toBeGreaterThan(0); + expect(body.lotCode).toBe(lot); + expect(body.events[0]).toMatchObject({ keep: true }); + for (const key of tenantKeys) { + expect(body).not.toHaveProperty(key); + expect(body.events[0]).not.toHaveProperty(key); + } expect(mockFetch).toHaveBeenCalledTimes(1); expect(String(mockFetch.mock.calls[0][0])).toBe( `http://inflow.test/api/lineage/${encodeURIComponent(lot)}`, @@ -517,7 +527,7 @@ describe('Inflow Lab public proxy', () => { }); it('rejects lineage POST, PUT, and DELETE while keeping GET allowlisted', async () => { - const lot = 'LOT-SAFE-001'; + const lot = 'seg'; const path = ['api', 'lineage', lot]; const csrf = await csrfCookies(); @@ -628,11 +638,12 @@ describe('Inflow Lab client export hrefs use the public proxy base', () => { })}`, ), ); + const filterLot = 'seg'; const filteredCsv = safeHref( servicePath( `/api/regengine/export/fda-request${buildDemoExportQuery({ preset: 'all_records', - traceabilityLotCode: 'L1', + traceabilityLotCode: filterLot, })}`, ), ); @@ -644,7 +655,7 @@ describe('Inflow Lab client export hrefs use the public proxy base', () => { expect(defaultCsv.includes('traceability_lot_code')).toBe(false); expect(defaultCsv.includes('start_date')).toBe(false); expect(defaultEpcis.includes('traceability_lot_code')).toBe(false); - expect(filteredCsv).toContain('traceability_lot_code=L1'); + expect(filteredCsv).toContain(`traceability_lot_code=${filterLot}`); expect(defaultCsv.includes('://')).toBe(false); expect(defaultEpcis.includes('://')).toBe(false); }); diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index ca6031af9..2e31bf50f 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -124,8 +124,33 @@ body[data-inflow-lab="true"] { background: #fff; color: #0f172a; } -[data-inflow-lab-app] button.bg-emerald-700, -[data-inflow-lab-app] button.bg-emerald-600, +/* Emerald/primary actions need an explicit background: the generic button + rule above paints background:#fff, and color:#fff here alone is white-on-white. */ +[data-inflow-lab-app] button.bg-emerald-700 { + background: #047857; + border-color: transparent; + color: #fff; +} +[data-inflow-lab-app] button.bg-emerald-700:hover { + background: #065f46; + border-color: transparent; + color: #fff; +} +[data-inflow-lab-app] button.bg-emerald-600 { + background: #059669; + border-color: transparent; + color: #fff; +} +[data-inflow-lab-app] button.bg-emerald-600:hover { + background: #047857; + border-color: transparent; + color: #fff; +} +[data-inflow-lab-app] button.bg-emerald-800 { + background: #065f46; + border-color: transparent; + color: #fff; +} [data-inflow-lab-app] button.bg-slate-950 { border-color: transparent; color: #fff; diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx index 7b20c95af..faae80ae5 100644 --- a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx +++ b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx @@ -1274,14 +1274,18 @@ export function InflowLabClient({ }; const traceLot = async () => { - if (traceInput) { - setSelectedLot(traceInput); - setActiveTab("Lineage"); + // Keep export filters empty by default, but never hit /api/lineage/ with a blank segment. + const lot = traceInput.trim() || selectedLot; + if (!lot) { + return; } + setTraceInput(lot); + setSelectedLot(lot); + setActiveTab("Lineage"); try { - const payload = await inflowJson(`/api/lineage/${encodeURIComponent(traceInput)}`); + const payload = await inflowJson(`/api/lineage/${encodeURIComponent(lot)}`); const lineageEvents = (payload.records || []).map(toTraceEvent); - setServiceLineage((current) => ({ ...current, [traceInput]: toLineage(lineageEvents) })); + setServiceLineage((current) => ({ ...current, [lot]: toLineage(lineageEvents) })); setServiceError(null); } catch (error) { setServiceError(error instanceof Error ? error.message : "Could not trace lot"); @@ -1539,7 +1543,7 @@ export function InflowLabClient({ @@ -2367,7 +2375,7 @@ export function InflowLabClient({