Skip to content

feat(suppliers): agreed sending hours and live late CTE metric - #3575

Merged
PetrefiedThunder merged 9 commits into
claude/supplier-data-qualityfrom
claude/supplier-agreed-sending-hours
Oct 25, 2026
Merged

PetrefiedThunder merged 9 commits into
claude/supplier-data-qualityfrom
claude/supplier-agreed-sending-hours

Conversation

@PetrefiedThunder

@PetrefiedThunder PetrefiedThunder commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

STACKED. Based on claude/supplier-data-quality (#3574), which is stacked on #3572. Retarget this PR to main after #3572 and #3574 merge.

Do not merge until C.Lee approves the production migration.

What changed

The late CTE measure on the supplier data quality view now works. Each supplier facility can store an agreed sending time in hours. A CTE record counts as late when it is received (created_at) more than agreed_sending_hours after its event_time. The label is unchanged (LATE_CTE_LABEL): this is our measure against the agreed sending time, not an FDA rule.

  • Schema: public.supplier_facilities.agreed_sending_hours integer NULL CHECK (agreed_sending_hours > 0). No backfill. Alembic b005ddbf1dba (v136, down revision 41f96588d87c, the single head before this PR) and supabase/migrations/20261024070000_supplier_agreed_sending_hours.sql, both with a downgrade. SupplierFacilityModel gains the column and the named CHECK.
  • Metric (services/shared/supplier_data_quality.py):
    • fetch_sending_hours is a SELECT filtered on tenant, supplier user and facility ids.
    • timed_records gives the denominator: records received in the window that have both event_time and created_at.
    • late_records gives the numerator: created_at - event_time > agreed_sending_hours. A record received exactly at the agreed hours is on time.
    • late_metric: with no hours set, the status is not_enough_data, the counts are null and the reason is LATE_CTE_NO_HOURS_REASON. With hours set, it uses the same share_metric rule as the other measures, so the status stays not_enough_data below MIN_RECORDS timed records.
    • late_cte is added to DRILLDOWN_METRICS.
  • API (services/admin/app/supplier_compliance_routes.py):
    • New PUT /v1/supplier/facilities/{facility_id}/agreed-sending-hours with body {"agreed_sending_hours": int | null}. It has the same guards as the other facility routes: tenant context is required, the facility id must parse, and _get_supplier_facility_or_404 checks tenant and owning supplier user. Validation is strict: a positive integer up to the Postgres integer maximum, or null to clear. Strings, floats, booleans, zero, negatives and a missing key all return 422.
    • GET /v1/supplier/data-quality returns agreed_sending_hours for each facility.
    • GET /v1/supplier/data-quality/{facility_id}/records?metric=late_cte lists the late records. It returns 409 when the facility has no agreed hours.
  • Route security manifest: I added the new tenant scoped PUT to docs/security/route-security-manifest.json by hand, with the same classification, permissions and adapter as the sibling PUT .../ftl-categories. Counts are now 258 enabled and 205 tenant scoped, updated in tests/security/test_route_security_manifest.py. The generator could not run locally because the local venv has no opentelemetry.instrumentation. CI should confirm the file with scripts/generate_route_security_manifest.py --check.
  • UI (frontend/src/app/dashboard/suppliers/data-quality.tsx):
    • The late CTE row shows "Agreed sending time: N hours after the event" or "Not set", with Set or Edit.
    • The edit form accepts whole hours above zero, with Save, Clear (sends null) and Cancel. After a save the page reloads the data quality summary.
    • With no hours set, the row shows "Not enough data." followed by the reason.
    • Late records can be drilled into (event and received times) once at least one is late.
    • Added apiClient.setFacilityAgreedSendingHours and AgreedSendingHoursResponse. SupplierDataQuality.agreed_sending_hours and 'late_cte' are added to SupplierQualityDrilldownMetric.

Migration SQL (up)

alter table public.supplier_facilities
    add column if not exists agreed_sending_hours integer null
        constraint supplier_facilities_agreed_sending_hours_check
            check (agreed_sending_hours > 0);

Idempotent: the inline CHECK is only created together with the column, so a second run does nothing. Existing rows get NULL. Nothing is backfilled and no row is rewritten.

Downgrade SQL

alter table public.supplier_facilities
    drop constraint if exists supplier_facilities_agreed_sending_hours_check;
alter table public.supplier_facilities
    drop column if exists agreed_sending_hours;

The downgrade drops only what this revision added. It deletes any agreed sending hours entered after the upgrade.

Read only verification SQL (for C.Lee, after applying)

-- 1. Column exists, integer, nullable.
select column_name, data_type, is_nullable
  from information_schema.columns
 where table_schema = 'public'
   and table_name = 'supplier_facilities'
   and column_name = 'agreed_sending_hours';

-- 2. CHECK constraint present with the expected definition.
select conname, pg_get_constraintdef(oid)
  from pg_constraint
 where conrelid = 'public.supplier_facilities'::regclass
   and conname = 'supplier_facilities_agreed_sending_hours_check';

-- 3. No backfill: every existing row is NULL right after applying.
select count(*) as total_rows,
       count(agreed_sending_hours) as rows_with_hours
  from public.supplier_facilities;

-- 4. Alembic is at the new head (if applied through alembic).
select version_num from alembic_version;

Expected results:

  1. One row: integer, YES.
  2. One row: CHECK ((agreed_sending_hours > 0)).
  3. rows_with_hours = 0.
  4. b005ddbf1dba.

Test results

All tests are data free. Ids come from uuid4 / crypto.randomUUID, and counts are derived from generated input lengths or MIN_RECORDS.

Check Result
pytest services/shared/tests/test_supplier_data_quality.py 41 passed
services/admin/tests/test_supplier_data_quality_routes.py (stub runner) 30 passed
tests/migrations/test_destructive_downgrade_documented_2827.py 3 passed
Single Alembic head (parsed from alembic/versions) b005ddbf1dba only
vitest supplier-data-quality, suppliers-metrics, suppliers-removal 3 files, 25 passed (19 in supplier-data-quality)
tsc --noEmit -p . Only the existing src/instrumentation.ts(26,5) error
eslint on touched files 0 errors (1 existing warning in page.tsx, line 532)
design-lint Clean (656 files)
ruff check + ruff format on touched Python files Clean

These could not run locally because the venv is missing packages; CI needs to run them:

  • tests/security/test_route_security_manifest.py: needs opentelemetry.instrumentation.
  • tests/migrations/test_alembic_chain_health.py: the repo alembic/ folder shadows the alembic package in this venv.
  • test_supplier_onboarding_routes.py, test_supplier_facilities_routes_1342.py, test_qa5_supplier_integrity.py: need PyJWT.

No database was contacted.

Remaining risk

  • Any authenticated supplier user can set hours on their own facilities. No tenant owner role is required, which matches the other facility settings such as FTL categories. Cross tenant and cross user writes return 404 and are covered by tests.
  • The late measure trusts created_at as the received time. A record sent through a bulk upload long after the event counts as late, which is the intended behavior.

Note

Medium Risk
Adds tenant-scoped facility settings and late-CTE logic tied to timestamps, plus broad UX/copy and navigation changes that affect onboarding paths and CI gates.

Overview
Adds per-facility agreed sending hours so the supplier data quality late CTE metric can be computed and edited in the dashboard (set/clear hours, drill into late records), with schema (supplier_facilities.agreed_sending_hours), Alembic v136, documented PUT /v1/supplier/facilities/{facility_id}/agreed-sending-hours, and data_gap_events / facility column updates in expected_schema.json.

In parallel, the PR tightens product copy and removes demo fabrications: a new Copy gate CI workflow runs scripts/copy-lint against a baseline; developer docs and ingest samples use <field> placeholders instead of TEST- lots; recall report, demo banner, pricing, and import flows drop sample/simulation UI; EPCIS is labeled (Preview) across marketing and dashboard copy.

Inflow Lab is retired from the product surface: dashboard nav and integrations links are removed, /dashboard/inflow-lab returns 404, connector docs/OG assets and large test suites are deleted, and dashboard CTAs point at Import (/ingest) instead of the workbench.

Engine Review Pass now reacts to issue comments, accepts more engine names, and requires a full 40-character commit SHA in REVIEW: markers (exact match, not prefix).

Reviewed by Cursor Bugbot for commit c77c400. Bugbot is set up for automated code reviews on this repo. Configure here.

…metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
regengine Ready Ready Preview Oct 25, 2026 3:39am UTC

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 1
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-24T08:05:24.843469Z 11eb280 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment on lines +316 to +317
agreedSendingHours={summary.agreed_sending_hours ?? null}
onSendingHoursSaved={onSendingHoursSaved}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing or clearing the agreed sending hours while late records are expanded leaves the old records visible, even when the refreshed metric has no late records and no Hide records button.

Fix on Vercel

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 11eb280. Configure here.

minRecords: number;
lateCteLabel: string;
agreedSendingHours: number | null;
onSendingHoursSaved?: () => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale late records after hours change

Medium Severity

Saving a new agreed sending time reloads the summary counts but leaves MetricRow drilldown state in place. The expanded late list still shows records from the previous threshold, and hide/show does not refetch while items remain. If the new late count is zero, canDrill hides the toggle so that stale list cannot be dismissed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 11eb280. Configure here.

@PetrefiedThunder PetrefiedThunder left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta review of #3575 against claude/supplier-data-quality (4332a82) at 11eb280.

Code: sound.

  • PUT isolation: _get_supplier_facility_or_404 (tenant + supplier_user) runs before any write. Foreign and other-tenant facilities return 404 with no commit. The body is strict int, > 0 and <= int4 max, and null clears the value.
  • Migration: nullable int with CHECK > 0, idempotent, and the Alembic and Supabase versions match. The downgrade drops the constraint, then the column.
  • Late metric: the numerator counts records with created_at - event_time > agreed hours. The denominator is records that have both times. With no hours set, the status is not_enough_data with a reason. The drill-down uses the same late_records and returns 409 when no hours are set.
  • Tests: no invented literals. AGREED_HOURS = MIN_RECORDS, times come from datetime.now, ids are uuid4/randomUUID, and the invalid inputs are deliberate.

FAIL: the manifest test is stale.

  • tests/security/test_route_security_manifest.py:195-196 still asserts len(dual) == 54 and len(mutations) == 23. The checked-in manifest marks the new PUT as tenant_dual_session_csrf (docs/security/route-security-manifest.json:2970), so the real counts are 55 and 24, and this test will fail. Bump both and add a comment.

CI: Vercel and CodeRabbit pass. Most required checks are pending, and no backend suite has run on this stacked base. No migration was applied.

REVIEW: FAIL codex 11eb280

…CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

@PetrefiedThunder PetrefiedThunder left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta review of #3575: 11eb280 -> 8cc9a3b. Only tests/security/test_route_security_manifest.py changed.

The test now asserts len(dual) == 55 and len(mutations) == 24. Counting the checked-in manifest JSON gives 258 enabled operations, 55 tenant_dual_session_csrf and 24 mutations, so they match. My earlier finding is fixed, and everything else is as in my last review.

CI: Vercel, Vercel Preview Comments and CodeRabbit pass; Bugbot, Non-main PR Secret Scan and report are pending. No backend suite has run on this stacked base, so merging needs green backend CI after it is retargeted to main. No migration was applied.

REVIEW: PASS codex 8cc9a3b

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Blocker Checklist

Head SHA: c77c4005c573bacfe0e88ac1123c78966926bd54

Status: Blocked (3 open blockers)

Review Threads (2 unresolved)

Changes Requested (0)

  • No reviews requesting changes

Blocked Engines (1)

  • Engine codex blocked at 11eb280 (current head: c77c400): View verdict: REVIEW: FAIL codex 11eb28005771d0977e1be3c05b68f416e2064822

…r-agreed-sending-hours; move Supabase migration to 20261024080000

Alembic b005ddbf1dba already revises main head 41f96588d87c. The Supabase file
moves from 20261024070000 to 20261024080000 so it no longer collides with

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
#3576 and sorts after 20261024060000 (data_gap_time_to_fix); #3576 takes 20261024090000.
PetrefiedThunder added a commit that referenced this pull request Oct 25, 2026
…ocument-receipts; rechain migration after #3575

Alembic a3f7ce361b0e now revises b005ddbf1dba (#3575, supplier agreed sending
hours), which revises main head 41f96588d87c, giving one linear head. File
renamed v136 -> v137. Supabase file moves 20261024070000 -> 20261024090000 to
sort after #3575 (20261024080000). Requires #3575 to merge first.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
@PetrefiedThunder

Copy link
Copy Markdown
Collaborator Author

REVIEW: FAIL deepseek 8eaf374

Reviewed by hand: Mac load was 34–46, over the 28 limit, so no DeepSeek pass.
Blocking:

  • alembic/versions/20261024_b005ddbf1dba_v136_supplier_agreed_sending_hours.py:9 adds public.supplier_facilities.agreed_sending_hours, but docs/schema/expected_schema.json at this head doesn't include it (0 matches). The repo-root schema parity audit (scripts/schema_parity_audit.py check) already fails on main's data_gap_events and will flag this column too. Regenerate the snapshot from a freshly migrated DB in this PR (python scripts/schema_parity_audit.py snapshot).
    Verified OK:
  • Chain: main has 103 revisions with head 41f96588d87c. b005ddbf1dba (down_revision 41f96588d87c, line 23), then feat(epcis): document receipts and live EPCIS failure metric #3576's a3f7ce361b0e, gives a single head a3f7ce361b0e with no missing parents. No revision or Supabase timestamp collisions with main (main's last Supabase migration is 20261024060000; 080000 is unused).
  • Merge delta: the only change is the documented Supabase rename 070000 -> 20261024080000. No stale 070000 references.
  • Schema-only migration (column plus CHECK > 0), no seed or synthetic data. Non-test code is clean.

PetrefiedThunder and others added 5 commits October 25, 2026 02:59
* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
… ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Comment thread scripts/copy-lint/baseline.json Resolved
…p walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
@PetrefiedThunder

Copy link
Copy Markdown
Collaborator Author

REVIEW: PASS deepseek c77c400

Delta review from 16f29dc (main merge) to c77c400, covering 0aafb55 and c77c400. Reviewed by hand (load was 23.9, under the limit, but a manual pass was enough for a 6-file delta).

  • docs/schema/expected_schema.json:6810 adds public.supplier_facilities.agreed_sending_hours as {nullable: true, type: integer}. That matches alembic/versions/20261024_b005ddbf1dba_v136_supplier_agreed_sending_hours.py:32 (integer null, CHECK > 0) and supabase/migrations/20261024080000. The snapshot diff is just these 4 added lines, so it's a clean regeneration. This fixes the earlier FAIL at 8eaf374.
  • scripts/copy-lint/baseline.json: no file/rule count went up. The total went from 288 to 136.
  • frontend/src/app/dashboard/suppliers/data-quality.tsx:20 now reads "EPCIS (Preview) files failing validation", and frontend/src/app/ingest/page.tsx:695 reads "EPCIS (Preview) files are all or nothing". Both follow the Preview label convention, and the test at frontend/src/tests/pages/ingest-epcis.test.tsx:139 was updated to match.
  • frontend/src/app/docs/walkthrough/page.tsx:66-67: the em dash is gone. None of the 3 touched pages has an em dash left.
  • Real data: no test facilities or synthetic values were added to prod code, copy or migrations.
  • CI: Copy gate passed, and so did the security and tenant-isolation checks. The only failures are Engine Review Pass, PR Blocker Checklist and Head SHA Check Guard, which are review gates. Note that no backend or schema-parity job ran for this PR (base is claude/supplier-data-quality). Parity was checked by hand above, and it must still go green on the PR to main.

@PetrefiedThunder
PetrefiedThunder merged commit 61bb729 into claude/supplier-data-quality Oct 25, 2026
27 of 36 checks passed
@PetrefiedThunder
PetrefiedThunder deleted the claude/supplier-agreed-sending-hours branch October 25, 2026 03:51
PetrefiedThunder added a commit that referenced this pull request Oct 25, 2026
* feat(suppliers): track data gap time to fix per supplier

Open one gap event per flagged missing KDE or failed CTE validation, close it only when a passing record for the same lot and CTE arrives, and store a receipt hash of the closing record. Adds a per supplier view with open gaps, median time to fix, oldest open gap and a 24 hour at risk flag citing 21 CFR 1.1455(c)(1). Migration included but not applied.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(alembic): add v135 data_gap_events revision and redact gap write errors

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(suppliers): flag gaps open at a records request response due time

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(suppliers): add per supplier data quality view

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(suppliers): generate ids at runtime in data quality tests

Replace hand written UUIDs, record ids, lot codes and facility ids with uuid4 and crypto.randomUUID values, and assert on values derived from the generated inputs.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(suppliers): derive data quality test counts from generated inputs

Counts now come from lengths of generated id arrays or from MIN_RECORDS, DEFAULT_WINDOW_DAYS and MAX_WINDOW_DAYS. KDE values, reasons, tenant and user ids are generated. Removes the Date.now based numerator.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(suppliers): agreed sending hours and live late CTE metric (#3575)

* feat(suppliers): agreed sending hours per facility and live late CTE metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(security): count the agreed sending hours PUT as a dual session CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): real-data follow-up to #3471 (v3) (#3544)

* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): Engine Review Pass picks up real PASS markers (#3577)

* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* feat(copy): SEL-452 copy gate with capabilities registry and baseline ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(schema): snapshot supplier_facilities.agreed_sending_hours (v136)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): label EPCIS as Preview in supplier quality and ingest, drop walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(suppliers): lock agreed sending hours to tenant admins (#3584)

* feat(suppliers): agreed sending hours per facility and live late CTE metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(security): count the agreed sending hours PUT as a dual session CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): real-data follow-up to #3471 (v3) (#3544)

* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): Engine Review Pass picks up real PASS markers (#3577)

* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* feat(copy): SEL-452 copy gate with capabilities registry and baseline ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(schema): snapshot supplier_facilities.agreed_sending_hours (v136)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): label EPCIS as Preview in supplier quality and ingest, drop walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(suppliers): lock agreed sending hours to tenant admins

Suppliers no longer set their own late CTE threshold. The write moves to
PUT /v1/supplier/admin/facilities/{facility_id}/agreed-sending-hours,
gated by the tenant Owner or Admin role check from tenant settings (#1386)
and scoped to facilities in the caller's tenant. The supplier dashboard
shows the agreed time read only.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(suppliers): scope facility lookup in query and test real tenant admin membership

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* test(suppliers): generate test emails at runtime (#3587)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(epcis): document receipts and live EPCIS failure metric (#3576)

* feat(suppliers): agreed sending hours per facility and live late CTE metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(epcis): record a receipt for every EPCIS document and live failure metric

Adds fsma.epcis_document_receipts with forced RLS and tenant policies (Alembic and Supabase migrations with downgrades), writes an accepted or rejected receipt with a sanitized error summary from the EPCIS ingest routes, and computes the EPCIS failure metric.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(epcis): size receipt batches from MIN_RECORDS and check partial counts are ints

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(security): count the agreed sending hours PUT as a dual session CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): real-data follow-up to #3471 (v3) (#3544)

* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): Engine Review Pass picks up real PASS markers (#3577)

* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* feat(copy): SEL-452 copy gate with capabilities registry and baseline ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(schema): snapshot supplier_facilities.agreed_sending_hours (v136)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): label EPCIS as Preview in supplier quality and ingest, drop walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

This branch was successfully deployed

1 active deployment
Preview — c77c4005 Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant