Commit Graph
471 Commits
Author SHA1 Message Date
bermudalamb b3bf42b859 Merge pull request 'ci: distinguish why the QA build daemon is unreachable (#25)' (#44) from ci/qa-build-diagnostics into main
SonarQube Analysis / sonarqube (push) Canceled after 11s
Tests / backend-integration (push) Canceled after 0s
Tests / frontend-e2e (push) Canceled after 0s
Tests / backend-unit (push) Canceled after 15s
Reviewed-on: #44
2026-08-18 08:18:42 -05:00
bermudalambandClaude Opus 5 711f59c0a1 ci: distinguish why the QA build daemon is unreachable (#25)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m40s
Tests / backend-unit (pull_request) Successful in 48s
Tests / frontend-e2e (pull_request) Canceled after 0s
Tests / backend-integration (pull_request) Canceled after 34m15s
The first dispatch failed at the wait step with nothing to act on. The
run logs showed the dind service container was never created — no service
startup output at all, and teardown reporting "No such container" for the
ID it had recorded — which is what act_runner does when it refuses a
privileged container.

From the failing step, that is indistinguishable from dockerd simply
being slow, so the step now says which one it is: if the service host
resolves, the container exists and dockerd is not serving plain TCP on
2375; if it does not resolve, the service never started and the runner
needs container.privileged.

Also raises the wait from 30s to 90s. The NAS took the full 30s before
failing, so the old ceiling was too close to the observed time to
distinguish slow from broken.

The docker CLI fallback is retained: the runner image has no docker
binary, and the static install worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:12:11 -05:00
bermudalamb 42152e791c Merge pull request 'feat: customer password reset via email round-trip (#32)' (#43) from feature/password-reset into main
SonarQube Analysis / sonarqube (push) Successful in 2m38s
Tests / backend-unit (push) Successful in 45s
Tests / frontend-e2e (push) Failing after 7m3s
Tests / backend-integration (push) Failing after 3h12m32s
Reviewed-on: #43
2026-08-17 19:15:57 -05:00
bermudalambandClaude Opus 5 2a20c0e05b ci: manual workflow to build and publish the QA image (#25)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m34s
Tests / backend-unit (pull_request) Successful in 36s
Tests / backend-integration (pull_request) Failing after 3h3m42s
Tests / frontend-e2e (pull_request) Failing after 7m32s
Adds a workflow_dispatch job that builds a chosen ref, pushes it to the
Gitea container registry as :qa plus a commit-sha tag, and emails when it
is ready. It deliberately does not restart the QA stack — redeploying
stays a human action in Portainer.

The build runs against a Docker-in-Docker service rather than the NAS's
Docker socket. Mounting the host socket into the runner would give every
workflow on every branch root-equivalent control of the NAS, production
included; pushing to a registry means the image does not need to survive
in the build daemon.

The QA stack now pulls that image instead of requiring a local build. The
previous arrangement meant the image existed only if someone remembered
to build it, which produced two confusing failures already: a Docker Hub
"pull access denied" when the tag was missing, and a silent stale-image
deploy when the build had not been rerun.

Two runner capabilities cannot be verified from here — privileged service
containers for dind, and a docker CLI in the runner image. The workflow
checks both and fails with an explanation rather than a connection
refused, and validates all five required secrets and variables up front
rather than part-way through a build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:10:04 -05:00
bermudalambandClaude Opus 5 db7c61c89d feat: customer password reset via email round-trip (#32)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m57s
Tests / backend-unit (pull_request) Successful in 53s
Tests / frontend-e2e (pull_request) Failing after 7m40s
Tests / backend-integration (pull_request) Failing after 3h14m41s
Adds "Forgot password?" to the login page, a request page, and a reset
page reached by a one-hour, single-use token delivered by email. Reuses
customer_tokens with a new password_reset kind alongside verify_email.

The request endpoint always answers 200, whether or not the address has
an account, so it cannot be used to test addresses for membership. Note
/register still reveals existence through its 409 on a duplicate, so this
protection is currently partial; closing that is its own change.

Completing a reset deletes every session for that customer. A reset
prompted by a compromise has to evict the intruder, and leaving a 30-day
cookie alive would defeat the point. It also marks the address verified,
since receiving the mail is exactly what verification proves, and
supersedes any outstanding token so an older link in the inbox cannot be
resurrected.

Introduces the first rate limiting in the codebase, on the request
endpoint only. The limiter is keyed on caller *and* submitted address:
keying on IP alone would let one person lock out everyone behind the same
proxy, and everything arrives via Nginx Proxy Manager. Applying that same
limiter to the reset endpoint, which carries no address, collapsed every
caller into one shared bucket -- so that endpoint is deliberately
unlimited instead, protected by a 32-byte single-use token whose bcrypt
work only runs after the token matches.

The e2e tests read the issued token directly from Postgres rather than
through a test-support endpoint. An endpoint returning a reset token for
an arbitrary address is account takeover for every customer if it is ever
reachable, and an environment gate is thin protection against that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:36:19 -05:00
bermudalamb c04f4a1370 Merge pull request 'fix(admin): theme, American English, and inventory/reservation tooling (#27)' (#30) from fix/admin-theme-and-filters into main
SonarQube Analysis / sonarqube (push) Successful in 3m28s
Tests / backend-unit (push) Successful in 46s
Tests / backend-integration (push) Failing after 1m37s
Tests / frontend-e2e (push) Failing after 9m7s
Reviewed-on: #30
2026-08-17 15:56:59 -05:00
bermudalambandClaude Opus 5 f537314259 fix(admin): theme, American English, and inventory/reservation tooling (#27)
SonarQube Analysis / sonarqube (pull_request) Successful in 4m23s
Tests / backend-unit (pull_request) Successful in 1m6s
Tests / backend-integration (pull_request) Failing after 4m55s
Tests / frontend-e2e (pull_request) Failing after 12m41s
Seven reported items, of which the first four had two root causes.

The active tab was invisible in dark mode because colorPrimary was
hardcoded to #1a1a1a in both themes. The accent now inverts with the
theme, and colorTextLightSolid inverts with it, or a near-white accent
would get antd's default white label and disappear.

The Category tab, Tag tab, and item-form category selector ignored the
theme entirely. antd declares main: lib/index.js and module: es/index.js,
so importing from 'antd' resolves to the ES build while 'antd/lib/...'
loads the CommonJS one — two copies, two React contexts, and no
ConfigProvider for anything deep-imported. Switching those files to
antd/es/* keeps the deep-import convention and shares the instance. This
was introduced by my own use of the lib path; es is correct under Vite.
Two storefront components had the same latent bug.

"Colour" is now "Color".

The Customers tab shows how many items each customer is holding, as a
link opening the item list with a Release button. Release mirrors the
customer's own cart removal — drop the cart row, return the item to
available, guarded on 'reserved' so it can never resurrect a sold item —
and deliberately sends no email about an action the customer did not
take. The count is a subquery rather than another join, which would have
multiplied rows and inflated order_count and total_spent_cents.

The Inventory tab filters by category, tags, price, and status, reusing
the storefront's parser and query builder so the two cannot drift.
Reserved is one option in a Status filter rather than a standalone toggle.

Also fixes two defects the screenshots exposed: the reserved-count link
bubbled to the row handler and opened the customer drawer behind the
dialog, and .admin-category-node had no CSS at all, so the tree node name,
item count, and actions ran together as one string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:49:05 -05:00
bermudalamb b6ef2bb63c Merge pull request 'docs: make QA review a standing step in the change checklist' (#29) from fix/logout-customer-reset into main
SonarQube Analysis / sonarqube (push) Successful in 3m53s
Tests / backend-unit (push) Successful in 52s
Tests / backend-integration (push) Failing after 1m50s
Tests / frontend-e2e (push) Failing after 9m56s
Reviewed-on: #29
2026-08-17 14:28:33 -05:00
bermudalambandClaude Opus 5 c546bd5141 docs: make QA review a standing step in the change checklist
Records the two-environment layout and a numbered checklist every change
follows, with QA review as a required stop rather than a judgement call.
Production is not where a bad deploy should be found, which is what
happened with the categories/tags release.

Production promotion now retags the image QA reviewed rather than
rebuilding, so what ships is exactly what was tested, and the README
carries the full command sequence with a verification gate at each step.

Also records the two gates that have already failed here: confirming a
pushed commit is actually on the branch, and that a schema/code ordering
problem cannot be caught by any local suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 14:28:33 -05:00
bermudalamb a37270f9bb Merge pull request 'fix: reset the header and return home when a customer logs out (#21)' (#28) from fix/logout-customer-reset into main
SonarQube Analysis / sonarqube (push) Successful in 3m9s
Tests / backend-unit (push) Successful in 57s
Tests / backend-integration (push) Failing after 1m27s
Tests / frontend-e2e (push) Failing after 6m56s
Reviewed-on: #28
2026-08-17 14:07:11 -05:00
bermudalambandClaude Opus 5 dd51621956 fix: reset the header and return home when a customer logs out (#21)
SonarQube Analysis / sonarqube (pull_request) Successful in 3m47s
Tests / backend-unit (pull_request) Successful in 45s
Tests / backend-integration (pull_request) Failing after 1m40s
Tests / frontend-e2e (pull_request) Failing after 9m31s
Logging out destroyed the server session and navigated home, but never
told CustomerAuthContext, so `customer` stayed in React state and the
header kept offering "My Account" instead of "Log in" and "Sign up". A
reload appeared to fix it, because fetchMe then returned null, which is
why the symptom looked intermittent. The cart badge had the same cause:
CartContext only clears its items once `customer` goes null.

Logout now lives on the auth context, which clears `customer` itself
rather than triggering a refetch — a refetch would leave a window where
the session is gone but the UI still shows the customer signed in.

logoutCustomer also ignored res.ok. A failed logout leaves the session
cookie valid, so reporting success signed the customer back in on their
next reload. It now rejects, and the account page reports the failure and
stays put instead of pretending. Logout is not routed through handle()
because the endpoint answers 204 with no body.

Navigation uses replace, so Back no longer returns to the account page,
which would only bounce to /login now the session is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:55:36 -05:00
bermudalamb 8a4d6dbc6c Merge pull request 'Fix/deploy migration safety' (#26) from fix/deploy-migration-safety into main
SonarQube Analysis / sonarqube (push) Successful in 3m15s
Tests / backend-unit (push) Successful in 42s
Tests / backend-integration (push) Failing after 1m14s
Tests / frontend-e2e (push) Failing after 7m30s
Reviewed-on: #26
2026-08-17 12:31:44 -05:00
bermudalambandClaude Opus 5 c01e434747 fix(qa): build the QA image before deploying the stack (#25)
SonarQube Analysis / sonarqube (pull_request) Successful in 3m48s
Tests / backend-unit (pull_request) Successful in 1m18s
Tests / backend-integration (pull_request) Failing after 4m12s
Tests / frontend-e2e (pull_request) Failing after 12m52s
Deploying the stack first failed with "pull access denied for
redefined-designs, repository does not exist or may require docker
login". The README had the Portainer stack creation ahead of the build,
so no local redefined-designs:qa image existed and Compose fell back to
pulling from Docker Hub, where the repository does not exist.

The build step now comes first in the one-time setup, and the review
workflow says to rebuild before restarting the stack — the stack builds
nothing itself and would otherwise run whatever was last tagged :qa.

pull_policy: never makes the failure legible: a missing local image now
reports itself as missing rather than as a registry authentication
problem. Noted as safe to remove if the NAS's Compose ever rejects the
key, since it only affects the error message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:29:14 -05:00
bermudalambandClaude Opus 5 47f00a3485 feat(qa): add the QA environment stack definition (#25)
A disposable stack for reviewing merged-but-undeployed changes online,
running alongside production on the same NAS.

Every value that could collide with production is changed: container
names, host port (32751), volume paths, database name, and image tag.
The Postgres data directory is the critical one — sharing production's
would mean QA writing into production's database files.

QA deliberately differs from production in three ways. DEMO_MODE=true
with no PayPal credentials, so the full cart and checkout path is
exercisable with no route to live PayPal. No SMTP configuration, so a QA
run cannot email anyone; the mailer already degrades gracefully when
unset. And restart: "no", so a NAS reboot does not quietly bring QA back
up and leave it running.

The stack must be named redefined-designs-qa in Portainer: the stack name
becomes the compose project name, and reusing production's would make
compose reconcile the two against each other and remove the production
containers.

README covers the one-time NAS directory setup, the review workflow, and
how to reset QA data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 11:17:52 -05:00
bermudalambandClaude Opus 5 e40a3d5e1a docs: archive the categories and tags design mockups (#23)
The wireframes behind the storefront filter layout decisions lived only in
.superpowers/brainstorm/, which is gitignored, so they were lost to anyone
reading the repo.

That directory stays ignored — it also holds a brainstorming-session token,
PID files, and absolute local paths, none of which belong in the repo. The
mockups themselves are design artifacts, so they are copied into the specs
directory and wrapped as standalone pages: the tool serves them as fragments
inside its own frame, so its style tokens and toggleSelect helper are inlined
to make them open in a browser with no server and no network.

Both rejected layouts and the rejected mobile variant are kept alongside the
chosen ones — the comparison is the part worth preserving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 11:12:32 -05:00
bermudalambandClaude Opus 5 77e58c0b92 fix(admin): confirm writes succeeded and allow inline category creation (#23)
Reported from testing: the item form said "Item added" for a save that
never happened.

saveItem and the other admin calls returned res.json() without checking
res.ok, so a 4xx/5xx resolved normally and every caller reported success
for a write the server had rejected. That is worse than failing outright,
because nothing prompts the user to look for the missing row. All admin
calls now throw on a non-OK response, and the handlers report the error,
keep the form open so entered values survive, and only claim success once
the server has accepted the write. Mark sold/available previously did
nothing visible on failure at all.

Categories can now be created from the item form, as tags already could.
Previously a category that did not exist yet meant abandoning a
half-filled form for the Categories tab. New categories are created at
the top level; nesting stays in the Categories tab.

The control lives in its own component: inline, every keystroke
re-rendered the whole Inventory component and rebuilt the category tree,
which visibly jittered the open popup. It sits above the tree rather than
below it, where a long list both hid it and made its position depend on
the list's measured height. The tree no longer expands everything on
open, which does not scale past a screenful; it has search instead.

The app now honours prefers-reduced-motion by disabling antd transitions,
and the e2e suite runs with that preference set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 11:07:44 -05:00
bermudalambandClaude Opus 5 c77fdad2b9 fix: run migrations on boot and stop failures rendering as empty (#23)
The storefront showed no inventory after deploying the categories/tags
release. No data was lost: the code queried categories/item_tags/
items.category_id against a database where the migration had not been
run, and that failure was invisible at every layer.

Three changes, each addressing one layer:

Migrations now run at container start, so deployed code cannot be ahead
of the schema and the easily-forgotten manual `docker exec migrate.js
up` step disappears. migrate.js waits for Postgres to accept
connections first, since the NAS brings the DB container up slower than
the app, and still exits non-zero so a bad migration stops the
container rather than serving a half-migrated schema.

Express 4 does not forward a rejected async handler, and no error
middleware was mounted, so a failing query never responded at all. Async
routes are now wrapped and an error middleware guarantees a 500. A hung
request is indistinguishable from an empty result in the UI, which is
how a schema mismatch came to read as "the store has no items".

The storefront now separates "request failed" from "no items" and offers
a retry. fetchItems/fetchFilterOptions throw on a non-OK response rather
than returning the parsed error body, which would have been set as the
item list and crashed the grid on .map.

Also restores the project-context update from 7fb5764, which was left
out of PR #24 and ended up dangling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:41:24 -05:00
bermudalamb 5ba33899c2 Merge pull request 'Feature/categories and tags' (#24) from feature/categories-and-tags into main
SonarQube Analysis / sonarqube (push) Successful in 3m3s
Tests / backend-unit (push) Successful in 36s
Tests / frontend-e2e (push) Failing after 4m13s
Tests / backend-integration (push) Failing after 3h14m50s
Reviewed-on: #24
2026-08-17 09:49:37 -05:00
bermudalambandClaude Opus 5 d28fb5634a feat(ui): storefront filters and admin category/tag management (#23)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m49s
Tests / backend-unit (pull_request) Successful in 37s
Tests / backend-integration (pull_request) Failing after 3h2m23s
Tests / frontend-e2e (pull_request) Failing after 3m54s
Storefront gains a Filters drawer holding the category tree, colour-coded
tag pills, and a price range, with applied filters shown as removable
chips. Filter state lives in the URL query string, so a filtered view is
shareable and the back button works. Item cards now show their category
and tags.

Admin gains Categories and Tags tabs, and the item form gains a category
TreeSelect plus a tags Select that creates new tags on the fly.

The admin category tree tracks expansion in state rather than using
defaultExpandAll: that prop is evaluated once at mount, so a branch added
afterwards rendered collapsed and its children were unreachable. Creating
or moving a node now expands its parent. Caught by the new admin e2e spec.

The chip row is marked as a named group so its "Clear all" stays
distinguishable from the drawer's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 09:34:37 -05:00
bermudalambandClaude Opus 5 9222e97deb feat(api): categories, tags, and storefront item filters (#23)
Adds a self-referencing categories tree, a tag registry with
deterministic colours, and item_tags, plus admin CRUD for both.

GET /api/items now accepts category, tags, min_price and max_price.
Category matching walks the subtree with a recursive CTE so selecting a
parent includes everything filed beneath it; tags match with AND via a
count check, since ANY() alone would return items carrying only one of
them. Malformed filter params return 400 rather than being ignored, so a
broken link doesn't quietly list the whole catalogue.

GET /api/filters serves the drawer its tree, tags, and price bounds in
one request.

Item image/tag aggregation moves from LEFT JOIN + GROUP BY to scalar
subqueries. Joining two one-to-many relations multiplies their rows, so
an item with 2 images and 3 tags would have repeated every image three
times once tags were added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 09:13:51 -05:00
bermudalambandClaude Opus 5 766358a9fe docs: design spec for categories and tags (#23)
Records the resolved requirements for issue #23: manual category tree
(no rule engine), single category per item with descendant matching,
central tag registry with hashed-then-overridable colours, AND semantics
for multi-tag filtering, and a drawer-plus-chips storefront filter UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 09:04:33 -05:00
bermudalamb cd07f9a1c5 Merge pull request 'test: disambiguate the Email locator in auth.spec' (#22) from fix/auth-spec-email-locator into main
SonarQube Analysis / sonarqube (push) Successful in 5m36s
Tests / backend-unit (push) Successful in 1m31s
Tests / backend-integration (push) Failing after 3h10m32s
Tests / frontend-e2e (push) Failing after 16m17s
Reviewed-on: #22
2026-08-15 18:37:38 -05:00
bermudalambandClaude Opus 5 a240d86762 test: disambiguate the Email locator in auth.spec
SonarQube Analysis / sonarqube (pull_request) Successful in 5m18s
Tests / backend-unit (pull_request) Successful in 51s
Tests / backend-integration (pull_request) Successful in 3m25s
Tests / frontend-e2e (pull_request) Successful in 12m21s
getByLabel('Email') matched two elements on /register -- the email input and
the marketing consent checkbox, whose label "Send me occasional emails"
contains the substring. Playwright's strict mode failed both tests that
filled it.

getByRole('textbox', ...) narrows by role, so the checkbox no longer collides.
Applied to the /login usage too, so the same concept reads the same way
throughout the file.

These failures were latent: the frontend-e2e job died at the build step
before it ever reached the tests, so CI never reported them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 18:35:43 -05:00
bermudalamb 02c2c74013 Merge pull request 'fix: add the missing /verify-email page' (#20) from fix/verify-email-page into main
SonarQube Analysis / sonarqube (push) Successful in 4m48s
Tests / backend-unit (push) Successful in 52s
Tests / backend-integration (push) Successful in 1m28s
Tests / frontend-e2e (push) Failing after 7m22s
Reviewed-on: #20
2026-08-15 18:23:11 -05:00
bermudalambandClaude Opus 5 b7c8c37447 fix: add the missing /verify-email page
SonarQube Analysis / sonarqube (pull_request) Successful in 3m42s
Tests / backend-unit (pull_request) Successful in 59s
Tests / backend-integration (pull_request) Successful in 1m30s
Tests / frontend-e2e (pull_request) Failing after 5m21s
Verification emails linked to /verify-email?token=..., but no such route
existed in main.tsx and nothing in the frontend ever called
POST /api/customers/verify-email. The SPA catch-all served index.html, no
route matched, and the page rendered blank -- so the token was never
redeemed and accounts stayed unverified forever.

The gap was invisible until SMTP was configured, because no verification
email had ever actually been delivered.

Adds VerifyEmail.tsx (verifying / verified / failed states), a verifyEmail
call in customerApi, and the route. The request is pinned to a single
firing via a ref: the endpoint deletes the token on success, so StrictMode's
double effect invocation in dev would otherwise overwrite the success state
with "invalid or expired token".

Verified end to end against a local stack: a real token flips
customers.email_verified to true and is consumed from customer_tokens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 18:16:32 -05:00
bermudalamb e7fb7271f0 Merge pull request 'fix: unset NODE_ENV in e2e job so devDependencies install' (#19) from fix/e2e-devdeps-and-summarize into main
SonarQube Analysis / sonarqube (push) Failing after 5s
Tests / backend-unit (push) Successful in 1m11s
Tests / backend-integration (push) Successful in 1m32s
Tests / frontend-e2e (push) Failing after 4m42s
Reviewed-on: #19
2026-08-15 08:56:25 -05:00
bermudalambandClaude Opus 5 ec63b9cdb0 fix: unset NODE_ENV in e2e job so devDependencies install
npm treats NODE_ENV=production as --omit=dev, so `npm install` in the
frontend-e2e job skipped typescript and the build died on `tsc: not found`.
The same env var would have stripped vite and @playwright/test from the
frontend install, and flipped the session cookie to Secure on a run served
over plain http.

The reported summarize crash was a symptom: the job aborted before Playwright
ran, but Summarize is `if: always()` and threw ENOENT on the missing JSON,
burying the real failure. Both summarize scripts now report the missing file
and exit 0 -- the job still fails via its own step.

Also split build from start, replaced `sleep 3` with a readiness poll against
/api/config, and dump the backend log when e2e fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 08:56:25 -05:00
bermudalamb 96edce5159 Merge pull request 'refactor: delete superseded checkout routes and dedupe cart checkout' (#17) from refactor/remove-dead-checkout-routes into main
SonarQube Analysis / sonarqube (push) Successful in 5m12s
Tests / backend-unit (push) Successful in 57s
Tests / backend-integration (push) Successful in 1m47s
Tests / frontend-e2e (push) Failing after 1m31s
Reviewed-on: #17
2026-08-14 17:59:42 -05:00
bermudalambandClaude Opus 5 977489cbc0 refactor: delete superseded checkout routes and dedupe cart checkout
routes/paypal.ts and routes/demo.ts were the pre-cart single-item
checkout flow. Nothing has imported them since the cart flow landed:
app.ts mounts only cartCheckout, the frontend calls /api/checkout/cart/*,
and no test touches them. They duplicated PAYPAL_BASE, getAccessToken,
and a second handler for the /webhooks/paypal mount.

Also extract openCheckout() from /paypal/create and /demo/purchase in
cartCheckout.ts, which repeated the same address-ownership check, cart
lock, and checkouts/checkout_items inserts. It returns a discriminated
union so callers keep control of the transaction and the response. Add
CartItem/LockedCart interfaces, dropping the (it: any) casts.

Note: paypal.ts was the only writer of items.reserved_until and
items.paypal_order_id. Those columns are now write-dead; the schema is
left alone for a separate migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:59:42 -05:00
bermudalamb 0935bc14f1 Merge pull request 'chore: sync package-lock with declared dependencies' (#18) from chore/sync-package-lock into main
SonarQube Analysis / sonarqube (push) Successful in 6m27s
Tests / backend-unit (push) Successful in 2m10s
Tests / backend-integration (push) Successful in 2m3s
Tests / frontend-e2e (push) Failing after 46s
Reviewed-on: #18
2026-08-14 17:58:14 -05:00
bermudalambandClaude Opus 5 b0a38d0ac6 chore: sync package-lock with declared dependencies
node-cron, node-pg-migrate, and @types/node-cron are declared in
package.json but were missing from the lockfile, so npm ci fails and a
plain npm install silently rewrites the lock. Regenerated to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:58:14 -05:00
bermudalamb ecaef5dc6d Merge pull request 'fix: resolve SonarQube hotspots for upload size and X-Powered-By' (#16) from fix/sonarqube-hotspots into main
SonarQube Analysis / sonarqube (push) Successful in 5m51s
Tests / backend-unit (push) Successful in 1m5s
Tests / backend-integration (push) Successful in 3m7s
Tests / frontend-e2e (push) Failing after 1m4s
Reviewed-on: #16
2026-08-14 17:24:06 -05:00
bermudalambandClaude Opus 5 ab1024a1b6 fix: resolve SonarQube hotspots for upload size and X-Powered-By
SonarQube Analysis / sonarqube (pull_request) Successful in 7m11s
Tests / backend-unit (pull_request) Successful in 2m18s
Tests / backend-integration (pull_request) Successful in 2m28s
Tests / frontend-e2e (pull_request) Failing after 59s
- S5693: MAX_IMAGE_BYTES was 8 * 1024 * 1024 (8,388,608), just over the
  8,000,000-byte ceiling the rule treats as safe, so the hotspot on the
  multer storage config never cleared. Use 8_000_000.
- S5689: Express advertises its stack in X-Powered-By by default, which
  tells an attacker what to aim exploits at. Disable the header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:18:42 -05:00
bermudalamb 9e4adb0586 Merge pull request 'docs: update project context with deployment lessons and cart architecture' (#15) from docs/update-project-context into main
SonarQube Analysis / sonarqube (push) Successful in 6m3s
Tests / backend-unit (push) Successful in 1m15s
Tests / backend-integration (push) Successful in 3m4s
Tests / frontend-e2e (push) Failing after 51s
Reviewed-on: #15
2026-08-14 17:02:16 -05:00
bermudalamb 05441f78ad docs: update project context with deployment lessons and cart architecture
SonarQube Analysis / sonarqube (pull_request) Successful in 6m2s
Tests / backend-unit (pull_request) Successful in 1m5s
Tests / backend-integration (pull_request) Successful in 3m29s
Tests / frontend-e2e (pull_request) Failing after 44s
2026-08-14 16:58:59 -05:00
bermudalamb 9d5e2daf6e Merge pull request 'fix: use node-pg-migrate instead of deleted init.sql in e2e workflow' (#14) from fix/e2e-workflow-migrations into main
SonarQube Analysis / sonarqube (push) Successful in 5m17s
Tests / backend-unit (push) Successful in 1m12s
Tests / backend-integration (push) Successful in 2m37s
Tests / frontend-e2e (push) Failing after 53s
Reviewed-on: #14
2026-08-14 16:21:40 -05:00
bermudalamb 483b1391a5 fix: use node-pg-migrate instead of deleted init.sql in e2e workflow
SonarQube Analysis / sonarqube (pull_request) Successful in 6m39s
Tests / backend-unit (pull_request) Successful in 1m24s
Tests / backend-integration (pull_request) Successful in 2m2s
Tests / frontend-e2e (pull_request) Failing after 56s
2026-08-14 16:18:20 -05:00
bermudalamb 1355b3076a Merge pull request 'fix: remove stale single-item demo-checkout tests, add cart integration tests' (#13) from fix/stale-demo-checkout-tests into main
SonarQube Analysis / sonarqube (push) Successful in 22m36s
Tests / backend-unit (push) Successful in 1m45s
Tests / backend-integration (push) Successful in 2m6s
Tests / frontend-e2e (push) Failing after 1m1s
Reviewed-on: #13
2026-08-14 16:12:29 -05:00
bermudalamb 1c46632e8f fix: remove stale single-item demo-checkout tests, add cart integration tests
SonarQube Analysis / sonarqube (pull_request) Successful in 13m27s
Tests / backend-integration (pull_request) Successful in 6m32s
Tests / frontend-e2e (pull_request) Failing after 2m57s
Tests / backend-unit (pull_request) Successful in 1m19s
2026-08-14 16:11:20 -05:00
bermudalamb 9de3e6a2fa Merge pull request 'Fix/cart back navigation' (#12) from fix/cart-back-navigation into main
SonarQube Analysis / sonarqube (push) Successful in 2m38s
Tests / backend-unit (push) Successful in 30s
Tests / backend-integration (push) Failing after 45s
Tests / frontend-e2e (push) Failing after 26s
Reviewed-on: #12
2026-08-14 15:37:21 -05:00
bermudalamb 1421694a0c fix: add back-to-shop navigation on cart page
SonarQube Analysis / sonarqube (pull_request) Successful in 3m55s
Tests / backend-unit (pull_request) Successful in 39s
Tests / backend-integration (pull_request) Failing after 1m6s
Tests / frontend-e2e (pull_request) Failing after 1m5s
2026-08-14 15:36:30 -05:00
bermudalamb 86f48ed3f8 fix: add back-to-shop navigation on cart page 2026-08-14 15:20:11 -05:00
bermudalamb cc3798a7f8 Merge pull request 'fix: cart badge not updating after account creation due to stale closure' (#11) from fix/cart-badge-stale-closure into main
SonarQube Analysis / sonarqube (push) Successful in 3m4s
Tests / backend-unit (push) Successful in 50s
Tests / backend-integration (push) Failing after 52s
Tests / frontend-e2e (push) Failing after 45s
Reviewed-on: #11
2026-08-14 15:15:12 -05:00
bermudalamb b85cdb9c44 fix: cart badge not updating after account creation due to stale closure
SonarQube Analysis / sonarqube (pull_request) Successful in 7m25s
Tests / backend-unit (pull_request) Successful in 1m46s
Tests / backend-integration (pull_request) Failing after 3m6s
Tests / frontend-e2e (pull_request) Failing after 1m16s
2026-08-14 15:13:55 -05:00
bermudalamb 853a962eb5 Merge pull request 'fix: TypeScript type mismatch between UspsValidationResult and local fallback object' (#10) from fix/usps-type-mismatch into main
Tests / frontend-e2e (push) Failing after 49s
SonarQube Analysis / sonarqube (push) Successful in 3m24s
Tests / backend-unit (push) Successful in 40s
Tests / backend-integration (push) Failing after 1m2s
Reviewed-on: #10
2026-08-14 14:49:05 -05:00
bermudalamb b73b472d80 fix: TypeScript type mismatch between UspsValidationResult and local fallback object
SonarQube Analysis / sonarqube (pull_request) Successful in 3m12s
Tests / backend-unit (pull_request) Successful in 57s
Tests / backend-integration (pull_request) Failing after 1m7s
Tests / frontend-e2e (pull_request) Failing after 38s
2026-08-14 14:47:52 -05:00
bermudalamb 4f055a2fcd Merge pull request 'feat: add customer cart with expiry, shipping addresses with USPS validation, and multi-item PayPal checkout' (#9) from feature/cart-shipping-checkout into main
SonarQube Analysis / sonarqube (push) Failing after 1m13s
Tests / backend-unit (push) Successful in 41s
Tests / backend-integration (push) Failing after 1m4s
Tests / frontend-e2e (push) Failing after 1m12s
Reviewed-on: #9
2026-08-14 14:05:09 -05:00
bermudalamb 9ab689e624 feat: add customer cart with expiry, shipping addresses with USPS validation, and multi-item PayPal checkout
SonarQube Analysis / sonarqube (pull_request) Failing after 59s
Tests / backend-unit (pull_request) Successful in 34s
Tests / backend-integration (pull_request) Failing after 1m33s
Tests / frontend-e2e (pull_request) Failing after 1m5s
2026-08-14 14:02:25 -05:00
bermudalamb 433d7e1db5 Merge pull request 'fix: Dockerfile' (#8) from fix/Dockerfile into main
SonarQube Analysis / sonarqube (push) Successful in 4m25s
Tests / backend-unit (push) Successful in 55s
Tests / backend-integration (push) Successful in 1m4s
Tests / frontend-e2e (push) Failing after 55s
Reviewed-on: #8
2026-08-14 10:58:33 -05:00
bermudalamb 845b1f29b9 fix: Dockerfile
SonarQube Analysis / sonarqube (pull_request) Successful in 3m3s
Tests / backend-unit (pull_request) Successful in 2m0s
Tests / backend-integration (pull_request) Successful in 1m50s
Tests / frontend-e2e (pull_request) Failing after 1m0s
2026-08-14 10:58:07 -05:00