0f4214128985265152331dc9c9ca7f79a03b518d
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0710cccac1 |
fix(qa): give the container the environment variables it now requires (#107)
QA refuses to start: "UPLOADS_DIR is required and is not set." #64 made UPLOADS_DIR always required, on the reasoning that its fallback of /app/uploads is correct inside the container and wrong everywhere else, so nothing should inherit it silently. That reasoning stands — but docker-compose.qa.yml had never set it either. QA was relying on exactly the fallback that change set out to stop people relying on, so the first deploy after it merged is the first one to fail. That is an incomplete check, and one that looked confident. #64 verified both CI workflows set every always-required variable and said so. CI is not what deploys. The compose file, which is, was never opened. ADMIN_GATE_SECRET was missing for a different reason, and the container reported it unset even after it was added to the Portainer stack. That is not a mistake, it is how compose works: stack variables are substituted into this file as ${VAR}, not handed to the container. A service receives exactly what its environment block lists. #87 already wrote that down; this is the first time it has bitten. The two get different treatment for a reason. UPLOADS_DIR is hardcoded because it is not a secret and because it has to match the right-hand side of the volume mapping — splitting one value across two places in the same file is how they drift apart. ADMIN_GATE_SECRET is interpolated from the stack so the secret itself never enters the repository, and the header comment now lists it among the required stack variables. Verified by feeding the environment docker compose config actually renders into validateEnv, the same function that was rejecting it: zero errors and zero warnings, the absence of warnings confirming the admin gate is now configured rather than merely quiet. Production runs from a stack outside this repository with the same history and will refuse to boot on its next rebuild unless UPLOADS_DIR is set there first. This commit does not fix that. Closes #107 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0c90e18205 |
feat: let QA send real email, guarded by a recipient allowlist (#87)
QA has never been able to send mail. The compose file set no SMTP variables and the mailer skips sending when it finds none, which was deliberate — a QA run must not be able to email a real customer if a fixture ever holds a real address. The cost is that four customer-facing flows have never been exercised anywhere but production: verification, password reset, favorite-sold alerts, and the cart-reminder cron that already has a known silent failure mode. MAIL_ALLOWLIST replaces the blanket mute. Unset means unrestricted, which is production and must stay so. Set means only matching recipients are delivered to; anything else is skipped with a [mail-blocked] warning naming the address and subject. An entry is either a full address, which also covers its plus-suffixed variants, or @domain for every mailbox there — plus-addressing is how these tests get written, and nobody should have to edit an allowlist to invent a new suffix mid-run. The guard sits in the mailer, not at the four call sites, so every sender is covered by construction and a fifth added later cannot bypass it by forgetting. It skips rather than throws: three callers already swallow send failures into a log, so throwing would mostly be caught anyway while risking a 500 on the signup path. The flow under test finishes and the log says why no mail arrived, which is exactly what was missing when QA was simply muted. Two details are load-bearing enough to state. Comparison is exact equality on both halves of the address rather than a suffix test, so a lookalike domain ending in an allowed one cannot get through — there is a test for that specifically. And a present-but-empty value refuses everyone rather than allowing everyone: writing MAIL_ALLOWLIST= expresses an intent to restrict, and reading it as "no restriction" would turn a typo into an outbound mail incident. This inverts the failure mode, so the allowlist is hardcoded in docker-compose.qa.yml rather than read from a stack variable. The safety property must not depend on remembering to set something in Portainer, where an omission would mean unrestricted sending from an environment full of fixtures. The comment says removing the line disables the restriction rather than the mail. QA points at Brevo, reusing the existing account rather than a separate QA sender — a deliberate choice that puts QA volume behind production's sending reputation and quota, acceptable for now. Host, port and secure are pinned in the compose because the mailer's fallbacks are Gmail's and Brevo needs 587 with STARTTLS; that mismatch fails at send time rather than at boot, which is #64's territory. Verified: 12 new unit tests on the matching function, which is where a mistake would actually be dangerous — 98 unit and 144 integration passing, lint 0 errors and 8 warnings unchanged, and the compose renders the expected values under docker compose config. Refs #87 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
71cbd142c3 |
fix: address the final review of the error boundaries (#62)
Five findings from a whole-branch review, two of which mattered. The catalogue boundary could not be recovered by the very controls it exists to keep alive. When the item grid threw, the header, filter chips and Clear filters stayed mounted — that was the point — but clicking Clear filters only changed the URL. A boundary does not reset on a client-side state change, so the fallback went on rendering over a catalogue that would by then have loaded perfectly well, and the only offered action reloaded the same failing URL. The shop read as permanently broken, which is the failure this whole change exists to prevent, reintroduced one level down. ErrorBoundary now takes an optional resetKey and clears itself when it changes; the catalogue boundary passes the filter key. The page and modal boundaries deliberately do not take one, because their escapes are hard navigations that remount the tree already — recorded on the prop so nobody completes the pattern by symmetry. The client-error endpoint could fill the disk. It is unauthenticated, each accepted report wrote about 8.7 KB, and Docker's default json-file driver has no size cap — so the rate limiter bounded a render loop, as its comment claims, but not a few hundred cheap source addresses. Stack and component stack now truncate at 1000 rather than 4000, which is still around fifteen frames and cuts the worst case to under 3 KB, and the QA compose file caps and rotates the log. Production is a Portainer stack outside this repository and needs the same option applied there; noted in the design doc rather than left implied. Three smaller things. A falsy thrown value defeated the boundary entirely: throw null is legal, and branching on the error object alone treated it as no error, re-rendered the children, threw again, and would have taken the root down — a blank page, the one outcome this is all here to avoid. The boundary now tracks hasError separately and synthesises a real Error for non-Error throws. The modal fallback announced its title twice to a screen reader, once as the dialog's name and once as the heading inside it, so the redundant dialog title is gone. And the design doc claimed the development-only detail shows the component stack when it only ever showed the message; corrected, with a note that the stack still reaches the server log, which is where it is useful. Verified after all five: backend lint 0 errors, 144 integration tests, frontend lint 0 errors and 31 warnings, 87 end-to-end tests, all against a freshly created database. Refs #62 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5a9ecefeba |
ci: build QA in Portainer from a git stack, drop the build workflow
The QA image is now built by Portainer from this repository rather than by Gitea Actions. Deployed as a Git repository stack, "Pull and redeploy" pulls the repo, builds from the Dockerfile, and recreates the containers in one action. This removes the runner from the loop entirely. Three dispatches failed without ever building: the runner refuses privileged containers, so the dind service was never created. Working around that needed either the host Docker socket mounted into the runner or privileged containers enabled runner-wide, and both hand every workflow on every branch root-equivalent control of the NAS, production included. Portainer already holds the socket — that is how it manages containers — so building there needs no new privilege at all. pull_policy: build is what keeps it honest. Without it the stack reuses whatever is tagged redefined-designs:qa, which is exactly how a redeploy appears to succeed while still serving old code — a failure this project has already hit twice. Deleting qa-build.yml also drops the registry, the REGISTRY_TOKEN and BREVO_API_KEY secrets, and the notification email. The email existed because CI worked asynchronously and had to tell you when it finished; redeploying from Portainer is synchronous, so the browser already does. Losing the per-commit image tags is a real cost — rollback becomes "rebuild from the ref you want" rather than retagging a specific build. README changes for this are deliberately not in this commit: that file also carries uncommitted work of Thom's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2a20c0e05b |
ci: manual workflow to build and publish the QA image (#25)
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> |
||
|
|
c01e434747 |
fix(qa): build the QA image before deploying the stack (#25)
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> |
||
|
|
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> |