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
@@ -1864,14 +1876,14 @@ export function InflowLabClient({
Preview test CSV or EPCIS JSON-LD filters. Formal production records are generated only from imported production data.
@@ -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.
-
+
Start loop
@@ -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.
-
- Preview test CSV
+
+ FDA export (CSV)
@@ -2651,7 +2667,7 @@ export function InflowLabClient({
Uses the same lot and date filters for interoperability testing. It is not production evidence.
- Preview test EPCIS
+ EPCIS export Preview
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({
@@ -1699,7 +1703,11 @@ export function InflowLabClient({
className={cn("min-w-0 border-slate-300 text-xs sm:w-[290px]", isStandalone ? "h-10" : "h-9")}
aria-label="Traceability lot code"
/>
-
+
Trace lot
@@ -2367,7 +2375,7 @@ export function InflowLabClient({
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
index 601050def..dba8c9020 100644
--- 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
@@ -3,14 +3,16 @@ 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");
+ const query = buildDemoExportQuery({
+ preset: "all_records",
+ traceabilityLotCode: "",
+ startDate: "",
+ endDate: "",
+ });
+ expect(query).toBe("?preset=all_records");
+ expect(query.includes("traceability_lot_code")).toBe(false);
+ expect(query.includes("start_date")).toBe(false);
+ expect(query.includes("end_date")).toBe(false);
});
it("returns an empty string when every filter is blank", () => {
@@ -18,23 +20,35 @@ describe("buildDemoExportQuery", () => {
expect(
buildDemoExportQuery({
preset: " ",
- traceabilityLotCode: " ",
+ 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",
- );
+ it("includes provided filters by shape without inventing lot or date values", () => {
+ const lot = "seg";
+ const start = "d0";
+ const end = "d1";
+ const query = buildDemoExportQuery({
+ preset: "all_records",
+ traceabilityLotCode: lot,
+ startDate: start,
+ endDate: end,
+ });
+ expect(query.startsWith("?")).toBe(true);
+ expect(query).toContain("preset=all_records");
+ expect(query).toContain(`traceability_lot_code=${lot}`);
+ expect(query).toContain(`start_date=${start}`);
+ expect(query).toContain(`end_date=${end}`);
+ });
+
+ it("preserves nonblank lot codes without trimming", () => {
+ const lot = " padded ";
+ const query = buildDemoExportQuery({ traceabilityLotCode: lot });
+ expect(query).toContain("traceability_lot_code=");
+ const params = new URLSearchParams(query.slice(1));
+ expect(params.get("traceability_lot_code")).toBe(lot);
});
});
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
index 41ff713eb..fe95a57a4 100644
--- a/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts
+++ b/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts
@@ -1,6 +1,7 @@
/**
* Build export query strings for the public Inflow Lab demo.
* Empty lot / date filters are omitted so downloads default to all demo events.
+ * Nonblank TLCs are passed through exactly as received (no trim/case-fold).
*/
export function buildDemoExportQuery(params: {
preset?: string;
@@ -10,11 +11,11 @@ export function buildDemoExportQuery(params: {
}): string {
const search = new URLSearchParams();
const preset = params.preset?.trim();
- const lot = params.traceabilityLotCode?.trim();
+ const lot = params.traceabilityLotCode;
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 (lot != null && 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();
From b9b45a97d2b56196820c0b7bdd16a37909252d5c Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Tue, 3 Nov 2026 05:19:49 -0800
Subject: [PATCH 3/6] fix(inflow-lab-demo): keep Trace from filling export
filters
Trace lot may fall back to selectedLot for lineage, but must not write
that into traceInput (CSV/EPCIS href driver). Restore exact toEqual
sanitized-body and query-string asserts with generic fixtures.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.../api/inflow-lab-demo-proxy.test.ts | 34 +++++++------------
.../inflow-lab/components/InflowLabClient.tsx | 7 ++--
.../inflow-lab/lib/demo-export-query.test.ts | 8 ++---
3 files changed, 19 insertions(+), 30 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 5aad63dff..ae065795f 100644
--- a/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts
+++ b/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts
@@ -171,14 +171,11 @@ describe('Inflow Lab public proxy', () => {
);
expect(response.status).toBe(200);
- 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 });
+ await expect(response.json()).resolves.toEqual({
+ ok: true,
+ delivery: { mode: 'local', posted: 3 },
+ events: [{ id: 'row', lot: 'seg' }],
+ });
});
it('rejects non-allowlisted paths without contacting upstream', async () => {
@@ -485,7 +482,6 @@ describe('Inflow Lab public proxy', () => {
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({
@@ -510,15 +506,10 @@ describe('Inflow Lab public proxy', () => {
);
expect(response.status).toBe(200);
- 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);
- }
+ await expect(response.json()).resolves.toEqual({
+ lotCode: lot,
+ events: [{ id: 'row', keep: true }],
+ });
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(String(mockFetch.mock.calls[0][0])).toBe(
`http://inflow.test/api/lineage/${encodeURIComponent(lot)}`,
@@ -652,10 +643,9 @@ describe('Inflow Lab client export hrefs use the public proxy base', () => {
'/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=${filterLot}`);
+ expect(filteredCsv).toBe(
+ `/api/inflow-lab-demo/api/regengine/export/fda-request?preset=all_records&traceability_lot_code=${filterLot}`,
+ );
expect(defaultCsv.includes('://')).toBe(false);
expect(defaultEpcis.includes('://')).toBe(false);
});
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
index faae80ae5..33bf3d555 100644
--- a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
+++ b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
@@ -1274,12 +1274,13 @@ export function InflowLabClient({
};
const traceLot = async () => {
- // Keep export filters empty by default, but never hit /api/lineage/ with a blank segment.
- const lot = traceInput.trim() || selectedLot;
+ // Lineage may fall back to selectedLot, but never copy that into traceInput —
+ // that field also drives CSV/EPCIS export filters (keep defaults unfiltered).
+ const typed = traceInput.trim();
+ const lot = typed || selectedLot;
if (!lot) {
return;
}
- setTraceInput(lot);
setSelectedLot(lot);
setActiveTab("Lineage");
try {
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
index dba8c9020..78c943fc0 100644
--- 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
@@ -37,11 +37,9 @@ describe("buildDemoExportQuery", () => {
startDate: start,
endDate: end,
});
- expect(query.startsWith("?")).toBe(true);
- expect(query).toContain("preset=all_records");
- expect(query).toContain(`traceability_lot_code=${lot}`);
- expect(query).toContain(`start_date=${start}`);
- expect(query).toContain(`end_date=${end}`);
+ expect(query).toBe(
+ `?preset=all_records&traceability_lot_code=${encodeURIComponent(lot)}&start_date=${encodeURIComponent(start)}&end_date=${encodeURIComponent(end)}`,
+ );
});
it("preserves nonblank lot codes without trimming", () => {
From 9b158ae3a7e39844bcbca89c8ce205f5d7f69c35 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Tue, 3 Nov 2026 05:25:40 -0800
Subject: [PATCH 4/6] fix(inflow-lab-demo): honor padded TLCs and clear
CodeRabbit nits
Detect blank Trace/export lots with trim only; keep nonblank padding.
Omit whitespace-only export lot filters. Rename helper href test so it
does not claim InflowLabClient render coverage.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.../__tests__/api/inflow-lab-demo-proxy.test.ts | 7 ++++---
.../inflow-lab/components/InflowLabClient.tsx | 4 ++--
.../tools/inflow-lab/lib/demo-export-query.test.ts | 14 +++++++++++---
.../app/tools/inflow-lab/lib/demo-export-query.ts | 8 +++++---
4 files changed, 22 insertions(+), 11 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 ae065795f..903c4fcbb 100644
--- a/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts
+++ b/frontend/src/__tests__/api/inflow-lab-demo-proxy.test.ts
@@ -589,9 +589,10 @@ describe('Inflow Lab public proxy', () => {
});
});
-describe('Inflow Lab client export hrefs use the public proxy base', () => {
- it('documents the public base + safe relative export paths', async () => {
- // Mirrors InflowLabClient: INFLOW_API + servicePath + safeHref.
+describe('Demo export query + public proxy path helpers', () => {
+ it('builds public-proxy-relative export paths from empty and filtered inputs', async () => {
+ // Documents INFLOW_API + servicePath + safeHref + buildDemoExportQuery shape
+ // (does not render InflowLabClient).
const INFLOW_API = '/api/inflow-lab-demo';
function servicePath(path: string) {
return `${INFLOW_API}${path}`;
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
index 33bf3d555..b55942621 100644
--- a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
+++ b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
@@ -1276,8 +1276,8 @@ export function InflowLabClient({
const traceLot = async () => {
// Lineage may fall back to selectedLot, but never copy that into traceInput —
// that field also drives CSV/EPCIS export filters (keep defaults unfiltered).
- const typed = traceInput.trim();
- const lot = typed || selectedLot;
+ // Trim only to detect blank; keep nonblank TLC padding for lineage.
+ const lot = traceInput.trim() ? traceInput : selectedLot;
if (!lot) {
return;
}
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
index 78c943fc0..1337f3036 100644
--- 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
@@ -27,7 +27,7 @@ describe("buildDemoExportQuery", () => {
).toBe("");
});
- it("includes provided filters by shape without inventing lot or date values", () => {
+ it("includes provided filters with an exact query string", () => {
const lot = "seg";
const start = "d0";
const end = "d1";
@@ -42,10 +42,18 @@ describe("buildDemoExportQuery", () => {
);
});
- it("preserves nonblank lot codes without trimming", () => {
+ it("omits whitespace-only lots and preserves nonblank padding exactly", () => {
+ expect(
+ buildDemoExportQuery({
+ preset: "all_records",
+ traceabilityLotCode: " ",
+ }),
+ ).toBe("?preset=all_records");
const lot = " padded ";
const query = buildDemoExportQuery({ traceabilityLotCode: lot });
- expect(query).toContain("traceability_lot_code=");
+ const expected = new URLSearchParams();
+ expected.set("traceability_lot_code", lot);
+ expect(query).toBe(`?${expected.toString()}`);
const params = new URLSearchParams(query.slice(1));
expect(params.get("traceability_lot_code")).toBe(lot);
});
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
index fe95a57a4..fb1b460fe 100644
--- a/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts
+++ b/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts
@@ -1,7 +1,8 @@
/**
* Build export query strings for the public Inflow Lab demo.
- * Empty lot / date filters are omitted so downloads default to all demo events.
- * Nonblank TLCs are passed through exactly as received (no trim/case-fold).
+ * Empty / whitespace-only lot and empty date filters are omitted so downloads
+ * default to all demo events. Nonblank TLCs are passed through exactly (no
+ * trim/case-fold of the stored value).
*/
export function buildDemoExportQuery(params: {
preset?: string;
@@ -15,7 +16,8 @@ export function buildDemoExportQuery(params: {
const startDate = params.startDate?.trim();
const endDate = params.endDate?.trim();
if (preset) search.set("preset", preset);
- if (lot != null && lot !== "") search.set("traceability_lot_code", lot);
+ // Whitespace-only is empty; nonblank TLCs keep exact padding/spacing.
+ if (lot != null && lot.trim() !== "") search.set("traceability_lot_code", lot);
if (startDate) search.set("start_date", startDate);
if (endDate) search.set("end_date", endDate);
const query = search.toString();
From 93062160a6745366faae68487c49fa633b3f2e19 Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Tue, 3 Nov 2026 05:34:20 -0800
Subject: [PATCH 5/6] fix(inflow-lab-demo): trim Trace lot for lineage
allowlist
SEGMENT_ALLOW rejects spaces, so Trace must trim before /api/lineage.
Export filters still use raw traceInput via buildDemoExportQuery, which
keeps nonblank padding and omits whitespace-only lots.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.../src/app/tools/inflow-lab/components/InflowLabClient.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
index b55942621..1025f809b 100644
--- a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
+++ b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
@@ -1276,8 +1276,9 @@ export function InflowLabClient({
const traceLot = async () => {
// Lineage may fall back to selectedLot, but never copy that into traceInput —
// that field also drives CSV/EPCIS export filters (keep defaults unfiltered).
- // Trim only to detect blank; keep nonblank TLC padding for lineage.
- const lot = traceInput.trim() ? traceInput : selectedLot;
+ // Lineage path is SEGMENT_ALLOW (no spaces): trim for the API/selection.
+ // Export hrefs still read raw traceInput via buildDemoExportQuery (padding kept).
+ const lot = traceInput.trim() || selectedLot;
if (!lot) {
return;
}
From 7c432610e5e6ea6f92b281d4d651befb80c6045c Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Tue, 3 Nov 2026 05:38:41 -0800
Subject: [PATCH 6/6] fix(inflow-lab-demo): trim export lot so spaces never
become +++
Whitespace-only Trace lots must not emit traceability_lot_code=+++.
Trim the lot value in buildDemoExportQuery and skip the filter when
empty. Align Trace lineage comments; data-free test covers spaces-only
omit and trimmed nonblank values.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.../inflow-lab/components/InflowLabClient.tsx | 4 ++--
.../inflow-lab/lib/demo-export-query.test.ts | 16 +++++++++-------
.../tools/inflow-lab/lib/demo-export-query.ts | 8 +++-----
3 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
index 1025f809b..485a64ea8 100644
--- a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
+++ b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
@@ -1276,8 +1276,8 @@ export function InflowLabClient({
const traceLot = async () => {
// Lineage may fall back to selectedLot, but never copy that into traceInput —
// that field also drives CSV/EPCIS export filters (keep defaults unfiltered).
- // Lineage path is SEGMENT_ALLOW (no spaces): trim for the API/selection.
- // Export hrefs still read raw traceInput via buildDemoExportQuery (padding kept).
+ // Trim for lineage SEGMENT_ALLOW (no spaces) and for selection matching.
+ // buildDemoExportQuery also trims lot filters and omits whitespace-only values.
const lot = traceInput.trim() || selectedLot;
if (!lot) {
return;
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
index 1337f3036..91660a988 100644
--- 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
@@ -42,19 +42,21 @@ describe("buildDemoExportQuery", () => {
);
});
- it("omits whitespace-only lots and preserves nonblank padding exactly", () => {
+ it("trims lot values and omits whitespace-only lots so spaces never become +++", () => {
+ // Spaces-only must not emit traceability_lot_code=+++.
expect(
buildDemoExportQuery({
preset: "all_records",
traceabilityLotCode: " ",
}),
).toBe("?preset=all_records");
- const lot = " padded ";
- const query = buildDemoExportQuery({ traceabilityLotCode: lot });
- const expected = new URLSearchParams();
- expected.set("traceability_lot_code", lot);
- expect(query).toBe(`?${expected.toString()}`);
+ expect(buildDemoExportQuery({ traceabilityLotCode: " \t " })).toBe("");
+
+ const query = buildDemoExportQuery({
+ traceabilityLotCode: " seg ",
+ });
+ expect(query).toBe("?traceability_lot_code=seg");
const params = new URLSearchParams(query.slice(1));
- expect(params.get("traceability_lot_code")).toBe(lot);
+ expect(params.get("traceability_lot_code")).toBe("seg");
});
});
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
index fb1b460fe..d9e498025 100644
--- a/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts
+++ b/frontend/src/app/tools/inflow-lab/lib/demo-export-query.ts
@@ -1,8 +1,7 @@
/**
* Build export query strings for the public Inflow Lab demo.
* Empty / whitespace-only lot and empty date filters are omitted so downloads
- * default to all demo events. Nonblank TLCs are passed through exactly (no
- * trim/case-fold of the stored value).
+ * default to all demo events. Lot values are trimmed before use.
*/
export function buildDemoExportQuery(params: {
preset?: string;
@@ -12,12 +11,11 @@ export function buildDemoExportQuery(params: {
}): string {
const search = new URLSearchParams();
const preset = params.preset?.trim();
- const lot = params.traceabilityLotCode;
+ const lot = params.traceabilityLotCode?.trim();
const startDate = params.startDate?.trim();
const endDate = params.endDate?.trim();
if (preset) search.set("preset", preset);
- // Whitespace-only is empty; nonblank TLCs keep exact padding/spacing.
- if (lot != null && lot.trim() !== "") search.set("traceability_lot_code", lot);
+ 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();