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>
117 lines
5.1 KiB
YAML
117 lines
5.1 KiB
YAML
# QA stack — a disposable copy of the app for reviewing merged-but-undeployed
|
|
# changes online. See issue #25.
|
|
#
|
|
# Deployed as its own Portainer stack, separate from production. Every value
|
|
# that could collide with production has been changed: container names, host
|
|
# port, volume paths, database name, and image tag. Do not copy a path or port
|
|
# back from the production stack — a shared Postgres data directory would mean
|
|
# QA writing into production's database files.
|
|
#
|
|
# Name the Portainer stack `redefined-designs-qa`, NOT `redefined-designs`.
|
|
# The stack name becomes the compose project name. Reusing production's name
|
|
# would make compose treat this as the same project and reconcile the two
|
|
# against each other — it would happily remove the production containers
|
|
# because they are not declared in this file.
|
|
#
|
|
# DEPLOY THIS AS A GIT REPOSITORY STACK, not from the web editor.
|
|
#
|
|
# Repository: https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs
|
|
# Reference: refs/heads/main
|
|
# Compose path: docker-compose.qa.yml
|
|
#
|
|
# The repository is public, so no credentials are needed. Portainer then does
|
|
# the whole cycle from one button: it pulls the repo, builds the image from the
|
|
# Dockerfile below, and recreates the containers. Nothing is built by hand on
|
|
# the NAS, and no image has to be pushed anywhere first.
|
|
#
|
|
# `pull_policy: build` matters. Without it the stack reuses whatever is already
|
|
# tagged redefined-designs:qa, which is how a redeploy can appear to succeed
|
|
# while still running old code. Leave any Portainer option that re-pulls images
|
|
# turned OFF — there is no registry to pull this image from.
|
|
#
|
|
# Required stack environment variables:
|
|
# QA_DB_PASSWORD — deliberately not named DB_PASSWORD, so pasting the
|
|
# production stack's variables here does nothing silently.
|
|
# PUBLIC_URL — the QA hostname, e.g.
|
|
# https://qa-redefined-designs.bermudalamb.synology.me
|
|
|
|
services:
|
|
redefined-designs-qa:
|
|
# Built from this repository by Portainer rather than pulled. The context is
|
|
# the repo root, which is where the Dockerfile lives — the same Dockerfile
|
|
# production uses, so QA and production images differ only in configuration.
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
image: redefined-designs:qa
|
|
# Always build; never reuse the existing tag.
|
|
pull_policy: build
|
|
container_name: redefined-designs-qa-syn
|
|
environment:
|
|
- TZ=America/Chicago
|
|
- PORT=3000
|
|
- PGHOST=redefined-designs-qa-db-syn
|
|
- PGPORT=5432
|
|
- PGUSER=redefined_qa
|
|
- PGPASSWORD=${QA_DB_PASSWORD}
|
|
- PGDATABASE=redefined_qa
|
|
|
|
# No PayPal credentials at all. DEMO_MODE lets the full cart and checkout
|
|
# flow run without them, so QA can exercise the whole purchase path with
|
|
# no way to reach live PayPal. Never set PAYPAL_ENV=live here. To test a
|
|
# real PayPal integration change, add sandbox credentials and set
|
|
# PAYPAL_ENV=sandbox — never the live ones.
|
|
- DEMO_MODE=true
|
|
|
|
# No SMTP configuration either. The mailer degrades gracefully when
|
|
# unconfigured: it logs a warning and skips sending. That is the desired
|
|
# behaviour here — a QA run must not be able to email real customers if
|
|
# a fixture ever contains a real address.
|
|
- SITE_CURRENCY=USD
|
|
- RESERVATION_MINUTES=15
|
|
- PUBLIC_URL=${PUBLIC_URL}
|
|
volumes:
|
|
# Separate uploads directory. Sharing production's would let a QA run
|
|
# write into, and a QA teardown delete, real product images.
|
|
- /volume1/configs/redefined-designs-qa/uploads:/app/uploads
|
|
ports:
|
|
# 32751, not production's 32750.
|
|
- 32751:3000
|
|
depends_on:
|
|
redefined-designs-qa-db-syn:
|
|
condition: service_healthy
|
|
# Not `unless-stopped`: QA is meant to be up only while a review is
|
|
# happening. `unless-stopped` would silently bring it back after every NAS
|
|
# reboot and leave it running indefinitely.
|
|
restart: "no"
|
|
# Docker's default json-file driver has no size cap. POST /api/client-errors
|
|
# is unauthenticated, so an unrotated log is a disk-filling vector on its
|
|
# own — see the error-boundary design doc's backend section. This does not
|
|
# cover production, which is a separate Portainer stack outside this repo;
|
|
# the same logging options need to be added there directly.
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: 10m
|
|
max-file: "3"
|
|
|
|
redefined-designs-qa-db-syn:
|
|
image: postgres:16
|
|
container_name: redefined-designs-qa-db-syn
|
|
environment:
|
|
- POSTGRES_USER=redefined_qa
|
|
- POSTGRES_PASSWORD=${QA_DB_PASSWORD}
|
|
- POSTGRES_DB=redefined_qa
|
|
- PGDATA=/var/lib/postgresql/data/pgdata
|
|
volumes:
|
|
# Distinct data directory from production's
|
|
# /volume1/configs/redefined-designs/postgres. This is the single most
|
|
# important difference in this file.
|
|
- /volume1/configs/redefined-designs-qa/postgres:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U redefined_qa -d redefined_qa"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
restart: "no"
|