Complete workforce access and evidence-aware operations - #10
Merged
Conversation
Preview/commit API over the existing connector pipeline: header-alias mapping inference, per-row validation with provenance-preserving quarantine, idempotent re-commit, catalog population with identifier conflict detection, and an expiry-entry seam so dated stock exports feed expiry math. Ships the client-intake runbook, backup and restore-drill scripts, and the product readiness plan.
Store owners could already save ERP/POS credentials in the frontend, but had
no way to know whether the values actually worked before relying on them, and
adding a new poll-based system required hand-written frontend code (fields
were hard-coded twice: once in the frontend panel, once in the backend poll
service). This adds a single source of truth for credential fields
(shelfwise_connectors.connector_test.CREDENTIAL_FIELDS), exposes it via
GET /connectors/systems, and adds POST /connectors/{system}/credentials/test
which reuses each connector's real fetch_page(None) call to probe the source
system live - no cursor persisted, no record ingested - so a store owner gets
an honest pass/fail before saving, not just a "saved" confirmation.
ShelfWise deploys one backend instance per tenant - the tenant id and first owner login are environment configuration (SHELFWISE_TENANT_ID, SHELFWISE_LOGIN_EMAIL, SHELFWISE_LOGIN_PASSWORD_HASH), not something the running API can create for itself. That was previously a manual step: hand-deriving a scrypt hash and a Fernet-compatible secret. This adds scripts/provision_new_shop.py, which generates a ready-to-paste .env fragment for a new shop from just a company name and owner email in one command - the one remaining step before a shop is fully self-serve (connectors, cameras, CSV import, staff accounts already are, per the prior commit). Extracted the scrypt hashing app.py's /auth/login already used into shelfwise_backend/auth_credentials.py so the script and the login route use the exact same verified implementation instead of a second, driftable one.
CI's first run against the onboarding-wizard work (662612e) surfaced packaging debt local dev environments had masked: pyproject.toml declared cryptography>=42 (used by the connector credential encryption module) but requirements.txt - what CI and the backend Dockerfile actually install - did not, so a clean-environment wheel install failed to import that module. Adds the missing pin and a package-contract test that keeps every pyproject runtime dependency present in requirements.txt going forward, plus a bounded-deadline fix for a worker-journal reclaim test that could flake under full-suite thread contention.
Guided onboarding now requires confirming the current product-policy templates per category, stored tenant-scoped with central-schema RLS. A superseded template version stops satisfying readiness rather than silently reusing an old acknowledgement, so readiness is four required steps: company, store, data source, policies. Decision queues are personalized by the verified workforce role through one server-owned assignment matrix. queue_view=assigned derives strictly from TenantContext.role and never from a query parameter; queue_view=all still returns the complete tenant ledger so owner and auditor visibility is unchanged. Monthly value reporting separates verified recovered money from estimated exposure. Only explicitly recorded actual amounts contribute to verified_recovered; modeled approval outcomes remain separately labelled and are never summed into it. Operations controls add no paid dependency: a stdlib health monitor requiring both liveness and seed readiness, exiting non-zero and writing bounded incident receipts without copying response bodies or accepting credential-bearing webhook URLs, plus release/rollback and POPIA operator documentation. Gates: 921 passed / 21 skipped, Ruff clean, frontend typecheck and production build clean, capability contract 244, Playwright 10/10 on isolated ports 5187/8017.
Removes the last connector path that still needed a developer. Shopify, Square,
Lightspeed, and Yoco authenticate the sender with an HMAC signature rather than a
credential ShelfWise stores, so they were excluded from the credential panel and had to
reach /connectors/{system}/intake, which is gated by the operator's shared ingest API
key. A store owner could self-serve connect an ERP but not their own till.
An owner now provisions a per-tenant, per-system endpoint in the console and receives a
delivery URL and signing secret exactly once, then pastes both into that retailer's
webhook settings. POST /connectors/webhook/{endpoint_id} authenticates on that tenant's
signature alone and runs deliveries through the same _process_inbound_record pipeline as
the keyed intake route, so dedup, validation, and projection behaviour are identical.
Secrets are encrypted at rest with the existing credential-encryption mechanism and are
never returned again by any route. Like the edge-device registry, the table is
deliberately not RLS-scoped and says why: resolving a tenant from an opaque endpoint id
happens before any tenant is known, so an RLS predicate would match zero rows and break
every delivery. List and revoke are tenant-scoped with explicit application predicates.
Tests cover the properties that matter: a tampered body fails its signature, a revoked
endpoint stops accepting previously valid deliveries, tenants cannot list or revoke each
other's endpoints, a payload cannot name the tenant it lands in, an unknown endpoint is
indistinguishable from a bad signature, and poll-based systems are refused an endpoint
they could never deliver to.
Gates: 932 passed / 21 skipped, Ruff clean, frontend typecheck and production build clean,
capability contract 248, Playwright 11/11 on isolated ports 5187/8017.
provision_new_shop.py generated a shop's secrets but stopped there: an operator still had to paste the fragment into a .env, know which Compose file to run, and know how to tell when the stack was actually ready. That was the remaining developer step between downloading the application and signing in as the owner. scripts/start_shelfwise.py provisions the shop only if it has not been provisioned before, brings the Compose stack up, polls the real /health endpoint, and prints the console URL with the first-login credentials. An existing .env is never regenerated. Re-minting SHELFWISE_CREDENTIAL_ENCRYPTION_KEY would make every stored connector credential, webhook secret, and device secret permanently undecryptable, and a new password hash would lock the owner out of their own instance, so a re-run only restarts the stack. An empty .env still counts as unprovisioned so a half-created file is not mistaken for a configured shop. The health wait is bounded and fails closed, treating ok:false as not-ready rather than ready. The plaintext owner password never reaches the env file, only its scrypt hash, and a password the owner supplied is not echoed back as if generated. Missing Docker produces an installable instruction rather than a traceback. Verification boundary: the provisioning, health-gating, and Compose detection logic is unit-tested, but a live docker compose up was not executed here - the machine had zero free disk and an unresponsive Docker CLI. That end-to-end run remains outstanding and is recorded in HANDOFF.md rather than implied. Scope: this deploys ShelfWise on the machine that runs it, which is what a single shop evaluating the product needs. It is not a hosted multi-tenant sign-up service. Gates: 943 passed / 21 skipped, Ruff clean, frontend typecheck clean, capability contract 248.
The startup script was executed against real Docker on this machine. That run proved the .env-preservation guard outside of tests (it reported keeping the existing setup and left the file untouched) and proved the Docker-failure path prints its intended actionable message and exits non-zero instead of hanging or raising. The successful-start path is still unproven and is now recorded as such with the exact reason: this machine's C: drive fell to 297 MB free during the attempt and the Docker Desktop engine stopped responding on its named pipe. Replaced the earlier, vaguer note that implied the script had not been run at all.
mrlucas679
marked this pull request as ready for review
July 29, 2026 21:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Verification
External acceptance boundaries
Live SMTP delivery, retailer sandbox traffic, and MI300X/Fireworks inference require operator credentials and are intentionally not inferred from local or CI results.