Repository navigation
P1 Catalog + Admin Core: catalog API, onboarding/staff backend, merchant admin UI + DoD e2e - #2
Merged
helgarpalmieri55 merged 47 commits intoJul 25, 2026
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
…ions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds /v1/admin/products (list/create/get/patch/archive) alongside a reusable assertProductLimit() plan-limit check, following the same tenantDb + parseOr400 + writeAudit patterns established for categories. - products.service.ts: search/status/pagination-aware list, slug auto-derivation with tenant-unique dedup (base, base-2, ... base-20, then 409 SLUG_TAKEN), categoryIds connect-on-create and replace-on-patch via the ProductCategory join, and archive (soft delete: status -> archived, not a hard delete). - plan-limits.ts: assertProductLimit() counts non-archived products against tenant_limits.productsMax (unlimited when no limits row exists), throwing 402 PLAN_LIMIT_EXCEEDED at/over the cap. Exported for reuse by the Task 8 CSV bulk-import commit path. - Both roles (owner/staff) can manage products, matching the categories controller's guard configuration. Test coverage in products.test.ts: create defaults, slug dedup, list search/status/pagination, plan limit enforcement, archive semantics (still readable, excluded from active list, frees up the limit), staff-role access, category connect/replace, cross-tenant 404s, and audit rows for create/update/archive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds PUT /v1/admin/products/:id/variants (atomic variant-set replace + product.options via the manual RLS transaction escape), image presign/ confirm/delete endpoints wired to StorageService with S3 HeadObject/ DeleteObject validation gates, and POST /v1/admin/products/:id/stock with InventoryMovement rows written in the same transaction as the stock update. Also adds the product_options migration and a PATCH same-slug regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Self-review surfaced two concurrency bugs: stock adjust computed the new stock value from a read taken before its transaction and wrote it as an absolute SET, so two concurrent adjustments could both pass the floor check against the same stale read and silently lose one of them; and the 8-image cap was only checked at presign time, so two concurrent confirms could both observe count<8 and both land, exceeding the cap. Fixes both with a single atomic conditional statement (UPDATE ... WHERE stock + delta >= 0 RETURNING stock) for stock, and a SELECT ... FOR UPDATE row lock serializing the count-check-then-insert for images — both inside the existing manual RLS transaction. Adds two regression tests exercising concurrent requests against each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Implements the P1a Task 8 / spec M2 CSV bulk-import acceptance criterion: GET /v1/admin/import/template, POST .../dry-run, and POST .../commit, with a pure csv-parser.ts (papaparse + a zod row schema in @ventia/core) that produces Spanish (es-CO) row-level errors. Commit runs entirely inside one manual RLS transaction (the ProductsService.update pattern) so a file with any row error, or one that would exceed the plan's product limit, leaves nothing persisted. assertProductLimit gained an additionalCount param so the commit path can count only CREATE rows against the limit. 500-row commit measured at ~900ms in this container, well under the 60s AC. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
…ody limit Addresses code review findings on the CSV import feature (Task 8): - Commit's per-row update was overwriting every column with the CSV row's create-time defaults, so a blank cell on an update (e.g. no track_inventory value) reset that field instead of leaving it alone. csv-parser.ts now records which optional columns were actually provided (ParsedRow.provided), and the commit path uses that to build a PATCH-like update: only name and price_cents are unconditional; stock/status/track_inventory/tax_rate/ description/images/categories are touched only when their column was non-blank, and slug is left alone (never re-derived from name) unless the slug column was explicit. - Category resolution in the commit transaction now keys its cache by slug (via slugify(name), with a P2002-catch-and-refetch fallback and a deterministic categoria-N fallback for names that slugify to '') instead of by lowercased name — fixes a same-file "Café" vs "Cafe" pair collapsing to two categories that then collide on Category's unique (tenantId, slug) index and abort the whole commit. - main.ts's express.json() body-size increase is now scoped to /v1/admin/import/* (mounted before the global default-limit parser, which body-parser skips re-running once a request's body is already parsed) instead of raising the limit for every route. - Parser surfaces a file-level row:0 error for a structurally malformed CSV (papaparse's own tokenizer errors), and the commit audit row's entity label is 'CsvImport' rather than the generic 'Product'. New/updated tests: PATCH-like update regression (blank cells preserved), accent/case category-collision regression, malformed-CSV file-level error, provided-flags assertions on the two existing defaulting tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Phase-review hardening wave (P1a):
- Reject cross-tenant/nonexistent categoryIds on product create and update
with a typed 400 VALIDATION_FAILED, checked via a tenant-scoped count
before the write instead of a foreign-key catch (create() previously
500'd here). Duplicate categoryIds in one request are deduped first so
they no longer trip the ProductCategory unique constraint and get
misreported as SLUG_TAKEN.
- Discriminate a product's P2002 into SKU_TAKEN vs SLUG_TAKEN instead of
always reporting SLUG_TAKEN. meta.target can't be used here: every write
runs as the ventia_app RLS role, and Postgres omits the constraint name
from unique-violation errors for roles without full table privileges
(verified directly against this schema/role) — so the conflict type is
determined with a targeted re-query instead.
- Add assertUuidOr404 and wire it into every :id/:imageId handler across
products/categories/variants/images/stock, so a malformed uuid path
param 404s like any other not-found id instead of 500ing on Prisma's
P2023.
- Validate the image confirm key's suffix (`{uuid}.{ext}`) after the
tenant/product prefix check, closing a path-traversal gap the prefix
check alone didn't cover.
- Un-archiving a product via PATCH (status -> non-archived) now runs the
plan-limit check before the write, since archived products don't count
toward the limit.
- Add the missing M2 AC test: deleting a category with products attached
returns 204 and leaves the products in place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Phase-review hardening wave (P1a), continued: - Remove `stock` from productUpdateSchema (kept on productInputSchema and the CSV row schema) so PATCH can no longer silently overwrite stock — POST /:id/stock and the CSV import path below are now the only ways to change it, both of which guarantee an InventoryMovement row. - CSV commit: an UPDATE row that changes an existing product's stock now writes an InventoryMovement (reason csv_import) for the delta; CREATE rows still set a baseline with no movement. - CSV commit/dry-run: a row that un-archives a product (explicit `status` column on a currently-archived product) now counts toward the plan limit alongside creates, matching PATCH's un-archive behavior. - Raise the commit transaction's timeout to 60s to match the CSV AC budget instead of Prisma's 5s interactive-transaction default. - resolveCategoryId: pre-check the `categoria-N` fallback slug (used for category names that slugify to '') against every slug already taken in the tenant, instead of attempting the create and trying to recover from a P2002 in the catch — a failed statement aborts the whole Postgres transaction, so that recovery could never actually run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
…ueness - Wrap each REVOKE in the 20260723205801 migration in a `to_regclass(...) IS NOT NULL` guard so `prisma migrate dev`/`reset`'s shadow-DB replay can't fail if migration ordering ever puts a REVOKE ahead of its table. Verified via `prisma migrate diff --shadow-database-url` (a non-interactive shadow-DB replay of all migrations) and via a fresh Testcontainers run, both green. The already-applied dev DB was recreated from scratch (docker volume dropped, migrations re-deployed, reseeded) since editing an applied migration invalidates its checksum. - Add `@@unique([tenantId, sku])` to Product (multiple NULLs still allowed). The migration was generated with `prisma migrate diff` against a real shadow database (this sandbox has no TTY, which `prisma migrate dev` requires) — the generated SQL is identical to what `migrate dev` would have produced, and it was applied to the dev DB via `prisma migrate deploy`. - Product create/update now discriminate the resulting P2002 into SKU_TAKEN vs SLUG_TAKEN (see the previous commit); CSV import's in-file duplicate-sku handling is unchanged (already rejected per-file by the parser). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds a Mailer interface + ConsoleMailer (MAILER DI token via MailerModule) and wires better-auth's emailVerification.sendVerificationEmail through it, so signup sends a Spanish verification email whose URL, when GET-ed, flips user.emailVerified. GET /v1/admin/me now also returns emailVerified, surfaced per-request rather than folded into AdminSessionContext. Also caps the CSV category base-slug suffix loop at 20 (mirroring the product path) with a Spanish row-level error, and completes a loose AdminSessionContext test cast that was missing email/role. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds P1b Task 4: GET/PATCH/PUT /v1/admin/settings routes (store info, theme, COD payments) gated to the owner role, delivering spec M8's "staff cannot reach settings" acceptance criterion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds the StaffInvite model (+ RLS policy in a follow-up create-only migration) and a staff/ module: owner-only invite/list/revoke/remove endpoints under /v1/admin/staff, plus a public POST /v1/staff/accept for a signed-in invitee to redeem their token. Seat checks count accepted staff memberships and pending invites against tenantLimits.staffSeats (absent row = unlimited). Invite tokens are 48 hex chars, stored only as a sha256 hash; the accept link doubles as the credential, so email match is deliberately not enforced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds POST /v1/admin/launch (owner-only), reusing the onboarding service's checklist computation: not-ready returns 422 LAUNCH_CHECKLIST_INCOMPLETE with per-item details, ready flips tenant.status to 'live' and writes a tenant.launch audit row, and a second call against an already-live tenant is idempotent (no recompute, no duplicate audit). Also lands suspended-tenant semantics: AdminSessionGuard now rejects non-GET /v1/admin/* requests with 403 TENANT_SUSPENDED when the session's tenant is suspended (one extra indexed PK read, skipped entirely for GET traffic), and the storefront home page renders "Tienda temporalmente no disponible" for a suspended tenant instead of the strict 503 deferred to the P2 storefront rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Records the onboarding wizard, staff invite/accept, and launch-checklist endpoints plus the console-transport mailer note, verified end-to-end via the P1b Task 7 M1 acceptance smoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Sets up @ventia/ui as a real package: Tailwind 4 styles.css with CSS variable theming (primary/background/foreground/radius per spec §5.4), a cn() class-merge util, and dependency-free shadcn-style primitives (Button, Input, Label, Card, Dialog, Select, Table, Badge, Alert, Spinner, FormField). Wires apps/admin onto it: Tailwind via @tailwindcss/postcss, next.config /api rewrite to API_INTERNAL_URL, a typed apiFetch/ApiError client, and an es-CO error-code-to-message mapper (lib/errors.ts) covering every error code the API currently emits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
When BrandingStep or PaymentsStep submit successfully, propagate the submitted values back into wizard's settings state so any later remount pre-fills from fresh data rather than the stale initial snapshot. This prevents silent overwrites of merchant edits on step revisits. - Add mergeSavedSettings and mergeSavedPaymentsSettings helpers to settings-merge.ts with unit tests (6 tests covering theme replace, payments merge, null prev). - Extend BrandingStep.onDone callback to carry submitted theme object. - Extend PaymentsStep.onDone callback to carry submitted codEnabled bool. - In wizard, add handleBrandingDone and handlePaymentsDone that merge settings before advancing to the next step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds apps/admin/app/(app)/productos (search, status filter, pagination, es-CO empty state) and app/(app)/categorias (list + create/rename/delete dialogs handling SLUG_TAKEN/VALIDATION_FAILED). New lib/format.ts::formatCOP renders integer cents as no-decimal es-CO COP, built RED-first in test/format.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Implements P1c Task 5: create/edit product pages under apps/admin. - apps/admin/app/(app)/productos/nuevo/page.tsx: create form validated client-side against @ventia/core's productInputSchema before submit. - apps/admin/app/(app)/productos/[id]/page.tsx + _components/: edit page with base fields (PATCH, stock omitted per productUpdateSchema), a variants editor (PUT /variants, replace-semantics warning), an image manager (presign -> raw-fetch PUT to the signed URL -> confirm -> delete), a stock adjuster (POST /stock, product or variant target), and an archive action (DELETE) with unarchive via the status select. - lib/format.ts: pesosToCents/centsToPesos (RED first in test/format.test.ts). - lib/variants.ts: buildVariantsPayload, filtering blank labels/rows and trimming (RED first in test/variants.test.ts). - lib/validation.ts, lib/product-labels.ts: shared zod-issue mapping and es-CO label constants for the two product pages. - packages/ui/src/button.tsx: adds an optional `href` prop (renders a styled <a> instead of <button>) per the T4 review rider, fixing the nested <a><Button/></a> in productos/page.tsx's call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Adds the equipo (staff invites/members/revoke/remove), aceptar-invitacion (public invite-accept flow), and configuracion (tienda/marca/pagos tabs) admin pages, plus a TDD'd inviteStatus() helper for the pending-invites table. Also collapses the importar page's pre-commit section into a success-only view with a reset button, per the T6 review rider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
- Tienda tab: send category/contactPhone/description unconditionally (not only when truthy) so blanking a field actually persists — PATCH /settings/store merges storeInfo in place, so an omitted key silently left the old value untouched behind a false "saved" confirmation. contactEmail stays conditional (its schema rejects an empty string). - Marca tab: wire VALIDATION_FAILED into per-field errors (logoUrl) and a colors-group alert, instead of collapsing to the generic "Revisa los campos marcados." message with nothing to point at. - aceptar-invitacion: map a malformed token's VALIDATION_FAILED (fails the 48-char check before the invite lookup runs) to the same INVITE_INVALID copy, rather than showing a validation message for a page with no form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
- Fix Finding 1: Keep all three tabs mounted with hidden attribute to preserve unsaved edits when switching tabs (instead of conditionally rendering which unmounts components) - Fix Finding 2: Add faviconUrl field to ThemeFormState and themeToFormState; update MarcaTab and BrandingStep to include favicon URL input fields and preserve faviconUrl in theme payloads - Extended theme-form tests to cover faviconUrl field (preserve when set, empty string default) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Playwright suite covering the full P1 Definition-of-Done: owner sign-up, onboarding wizard (store info, branding, 10-product CSV import, COD), email verification via the console-mailer log hook, launch, storefront subdomain resolution, and a staff invite/accept flow hitting the owner-only permission boundary. Also fixes two real bugs the e2e surfaced: better-auth rejecting admin sign-up/sign-in with 403 INVALID_ORIGIN (missing trustedOrigins for the admin app's own origin), and a turbo.json task-graph gap where typecheck could race a package's own build and fail looking for generated Next.js route types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
…ant slugs Staff-invite emails linked to a POST-only API endpoint that 404s on GET; point them at the admin app's client-rendered accept page instead. The verification email's callbackURL defaulted to an unqualified "/", which 303s a browser to the API's own origin instead of the admin app; the sendVerificationEmail hook now forces it to ADMIN_URL. Tenant slugs were otherwise-unvalidated free text that becomes a DNS subdomain label; constrain them to slugify's own alphabet and block infrastructure-reserved words (auto-derived collisions dedup-suffix instead of erroring). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
README: note the P1 suspended-storefront 200 (strict 503 deferred to P2's storefront rebuild, alongside archived-products-404), and add a Production notes section flagging that ADMIN_URL must be set in any real deployment or sign-ins fail 403 INVALID_ORIGIN. Amend the P1 design doc's storefront-503 sentence with the same P1 deviation note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012tUfx41TMxana567RboGMb
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
Phase P1 of Ventia per
docs/SPEC.md§11 (M1, M2, M8 partial), built with the superpowers workflow across three sub-plans, each with per-task TDD implementation + independent review, phase reviews, and a final whole-branch review ("Ready to merge: Yes").P1a — Catalog API backend
/v1/admin/*, narrowedAdminSessionContext), categories/products CRUD with plan limits (402 typed errors), slug/SKU semantics (@@unique(tenantId, sku)), archive-not-delete with un-archive limit accounting.Product.optionsmigration), stock adjustments withInventoryMovementledger (race-safe atomic SQL), image cap enforced under concurrency (FOR UPDATE).ventia_appgrants on system tables, shadow-DB-safe migrations (prisma migrate devrestored).P1b — Onboarding + Staff + lifecycle backend
ALREADY_HAS_TENANTgate incl. platform_admin), resumable wizard state, live launch checklist.INVITE_INVALID, transactional accept; invite email links to the real accept page.draft → live, verification-gated) and suspended semantics (admin GET-only, storefront unavailable message — strict 503 documented as P2 deviation).P1c — Merchant admin UI (es-CO)
packages/ui: Tailwind 4 + shadcn-style primitives (native-<dialog>accessible modal, ARIA-wired FormField)./apiproxy (better-auth cookies without CORS), typed fetch client with es-CO error mapping verified against actual API codes.trustedOriginsfor the admin origin; turbo typecheck/build race).Verification
pnpm turbo run lint typecheck build— 21/21;pnpm turbo run test— 8/8 packages: api 138, admin 93, core 17, db 16, storefront 2.bash scripts/e2e.sh— 2/2 specs (the P1 DoD path end-to-end through Caddy).ADMIN_URLrequired in production (README Production notes).Docs
docs/superpowers/specs/2026-07-23-p1-catalog-admin-design.md(with amendments)docs/superpowers/plans/2026-07-23-p1a-catalog-api.md,2026-07-24-p1b-onboarding-staff.md,2026-07-24-p1c-admin-ui.md🤖 Generated with Claude Code
https://claude.ai/code/session_012tUfx41TMxana567RboGMb
Generated by Claude Code