e4ee584e865d878408bf35bcdcc73a8210381cdb
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e4ee584e86 |
Fix six low-severity issues from the full-session code review
- PricingEngine::round_to_99(): ceil($price) - 0.01 undershoots whenever $price's cents are already .99 or higher — an exact integer (ceil() equals floor(), landing a full cent below $price) or, more subtly, any fractional price above X.99 itself (a division result, not something pre-rounded to 2 decimals, e.g. 20.995 -> old formula gave 20.99, below the input). Rewritten as floor()+0.99, bumped by 1 if still under $price. Verified against 8 cases including both boundary classes: every result now >= its input. - CoverSync::attach_cover(): wp_generate_attachment_metadata()'s return value was never checked. Assumed it'd return empty on failure — verified directly it does NOT: fed it 2000 bytes of garbage and got back ['filesize' => 2000], no width/height, since GD/Imagick couldn't decode it. Old code would report "attached" for a degraded image with no dimensions/srcset. Now checks for width+height specifically, and cleans up the orphaned attachment on failure so a re-run retries the product. Verified both the corrupt-image rejection (no orphan left, no thumbnail set) and that a real image still attaches normally. - Makefile: the per-invocation .env.$(ENV).compose file (holds every API key and both DB passwords, stripped of DB_PASSWORD/DB_ROOT_PASSWORD only) was never cleaned up, left at default 644 in the repo root after every `make` command. Now chmod 600 on creation and removed at the end of every target, preserving the underlying command's exit code through the cleanup. Verified both the happy path (file gone after, exit 0) and the failure path (bad ENV: file still cleaned up, real exit code still propagates through make). - docker-compose.yml: added a healthcheck to the wordpress service (bash's /dev/tcp against php-fpm's port 9000 — no HTTP endpoint to hit directly, and no `nc` in this image; verified it correctly succeeds once php-fpm is listening and fails against a closed port) and switched cron's and caddy's depends_on (across all three env overlays) from bare container-started to condition: service_healthy. Previously both could start against a wordpress container that had started but wasn't actually ready yet. Verified via a full down/up cycle: db+redis healthy, then wordpress starts and becomes healthy, only then do cron and caddy start. - .env.dev/.env.staging/.env.production chmod'd 600 (were 644) — same plaintext-credential content as secrets/<env>/, which is already 700/644 at the directory/file level respectively for a different reason (container UID readability); these have no such constraint, only the host CLI reads them. Also removed a stray .env.staging.compose left over from before the Makefile fix above existed. Noted the convention in .env.example so newly created env files follow it too. |
||
|
|
df34fe8e54 |
Fix five high-severity bugs from the full-session code review
- ProductSync: bsc_work.wc_product_id could go stale if a product was ever
deleted out-of-band (wp-admin, a cleanup script). wc_get_product() then
correctly detects "no product" and creates a new one, but the repoint was
gated behind `if (!$existing_id)` — which was already true, so the stale
ID never got corrected. Every future sync repeated this, one duplicate
product per run. Fixed by making the repoint unconditional (cheap,
idempotent UPDATE either way). Reproduced the exact scenario against dev
(deleted a product out-of-band, ran sync-products twice) and confirmed:
one repoint, zero duplicates, product count and per-work product count
both correct across repeated runs.
- Commands.php (sync-hardcover-tags): wp_set_object_terms() with an empty
array clears the taxonomy rather than leaving it alone — verified
directly. Hardcover legitimately returns no moods/content-warnings for
plenty of books, so a --force re-sync could silently wipe existing tags,
including anything hand-tagged. Fixed by skipping the call per-category
when that category's array is empty. Verified via a direct eval test:
pre-existing genre tag survives a sync where genre comes back empty,
mood still gets set normally.
- docker-compose.yml: two stray root-owned secrets/db_password and
secrets/db_root_password directories were already sitting on disk —
Docker auto-creating a bind-mount source as a directory from an earlier
manual `docker compose` call that ran without ENVIRONMENT exported
(reproducing the exact bug already fixed once this session). Removed
the stray dirs and changed every `${ENVIRONMENT}` in a volume mount to
`${ENVIRONMENT:?ENVIRONMENT must be set}` so Compose now hard-fails
instead of silently defaulting to empty. Verified: unset ENVIRONMENT now
fails config validation with a clear error; set, it still works.
- HARDCOVER_API_TOKEN moved to the same _FILE secrets pattern already used
for DB_PASSWORD/DB_ROOT_PASSWORD (docker-compose.yml, deploy.sh,
HardcoverAdapter.php) — it was a live, consumed secret still going
through Compose's ${VAR} interpolation, exposed to the same
mangling bug already fixed for the DB passwords, plus visible via
`docker inspect`. deploy.sh now writes secrets/<env>/hardcover_api_token
(optionally empty). Verified via deploy.sh dev + wp eval: empty file ->
is_configured() false, a real token value -> true.
- backup.sh: mariadb-dump had no --single-transaction, so a dump against a
live site would either table-lock for its duration or produce a
non-atomic/inconsistent dump. Added; verified a real dump still runs
clean and produces a valid, restorable-looking .sql.gz.
|
||
|
|
dcad1b7512 |
Product cover images via Open Library; Gutenberg literature filter; cron root fix
Cover sync (OpenLibraryAdapter + CoverSync + sync-covers command): fetches a real cover from Open Library's free, keyless, explicitly-licensed-for- this-use Covers API and attaches it as a genuine Media Library attachment (not a hotlinked <img> — WooCommerce's shop loop/gallery/structured data all need a real _thumbnail_id). ISBN-first, title/author-search fallback via the confirmed cover_i field, matching the pattern already established in HardcoverAdapter. Two real bugs found and fixed while verifying this against the actual catalog, not assumed: - A Range-header HEAD-equivalent probe (added to avoid double-fetching) caused Open Library's server to redirect with a misleading content-type, producing a false positive on a known-fake ISBN. Removed — fetch once, verify the real bytes. - Their search endpoint has genuine transient failures under repeated querying (same request, same input, failed then succeeded seconds later) — added retry-with-backoff on network errors/5xx, matching how the rest of this codebase already treats transient failures as retriable rather than fatal. Also found chasing what looked like a third cover-sync bug, but wasn't one: uploads/2026/08 was owned by root, silently blocking www-data-run wp-cli from writing new files. Root cause was a gap in the earlier root-hardening pass (docker-compose.yml, Commands.php) — it fixed our own deploy.sh/backup.sh/Makefile invocations but missed the cron sidecar's own internal process, which was still running its wp-cli loop as root via a leftover --allow-root. Fixed at the container level (`user: www-data` on the cron service) since it's a plain shell loop with none of php-fpm's master-process-needs-root-to-drop-privileges concern. Gutenberg importer: filters to actual literature via LoCC (Library of Congress Classification) — verified directly against the real catalog that novels consistently get a P* code while government documents/ speeches/law get E/JK/KF/DA and never a P code. Removes the Declaration of Independence, Bill of Rights, etc. from what was importing as "books." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
71b8065f20 |
AO3-style tag browsing: taxonomies, display, Hardcover sync (schema unverified)
Four flat taxonomies attached to product (bsc_genre, bsc_mood, bsc_content_warning, bsc_tag) reuse WordPress's native taxonomy archive system for the "click a tag, see everything with it" browsing AO3 is known for — no custom archive templates or query logic needed. Verified: all four register correctly, render as clickable chips on the product page (content warnings get a distinct notice instead of just another chip), and the archive page + term count both work end-to-end with real test data. HardcoverAdapter + `wp bookstore sync-hardcover-tags` pull genre/mood/ content-warning/freeform tags from Hardcover's API to populate these. This part is explicitly NOT verified against a live response — Hardcover's API is in beta with informal docs, and there was no API token available to confirm the exact query/response shape. Flagged clearly in the adapter itself; needs a real token + introspection query before trusting the field-parsing logic in production. ISBN-first-then-title/author matching handles both real future ISBNs and the current Gutenberg-synthetic catalog's fake ones. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
f65581bc50 |
Permanently eliminate the "$X variable not set" warning, and fix Caddy auto-HTTPS on private IPs
Two separate leaks were causing the same cosmetic-but-annoying warning to
survive every previous fix attempt:
1. Compose's own `secrets:` block reads the referenced file's *content*
as part of its config model, and applies the same interpolation
warning to it — even with DB_PASSWORD fully removed from every ${VAR}
and --env-file path. Switched from Compose's native `secrets:` to plain
bind mounts at the same /run/secrets/* paths: a bind mount only ever
touches the file's path, never its content, so it's immune. (Verified
this precisely with an isolated repro before rolling it out — the two
mechanisms behave differently even though they look equivalent.)
2. caddy's `env_file: .env.staging` (a leftover from the since-removed
basic-auth setup) loaded the *raw*, unfiltered env file directly,
bypassing deploy.sh/backup.sh/restore.sh's filtered-copy mechanism
entirely. Caddy only ever needed SITE_DOMAIN; switched to passing that
one value directly instead of the whole file.
Also fixed Caddyfile.staging: the site address had no explicit scheme, so
Caddy's automatic-HTTPS logic still applied to a private IP (registering
its own internal CA and redirecting HTTP->HTTPS) — not the "plain HTTP
only" behavior I'd assumed and told the user earlier. Prefixed with
`http://` to genuinely disable automatic HTTPS for this LAN-only box.
Verified end-to-end: fresh deploy with dollar-sign DB passwords produces
zero warnings, serves HTTP 200 on plain http:// with no redirect, and the
DB connection genuinely authenticates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
3876a6ba30 |
Fix DB password corruption via Docker secrets file mechanism
The reported bug (a generated password containing "$BRjTx3tlmSpz" got
silently blanked, breaking the DB connection) is the same Compose
interpolation issue as the earlier bcrypt hash, but this time in fields
that are genuinely user-chosen and can't just be avoided by convention.
Switched DB_PASSWORD/DB_ROOT_PASSWORD to Docker's official `_FILE`
secrets convention (MARIADB_PASSWORD_FILE / WORDPRESS_DB_PASSWORD_FILE),
backed by Compose's native `secrets:` mechanism: deploy.sh writes the raw
value to secrets/<env>/db_password, and the container reads that file
directly — the value never passes through Compose's ${VAR} interpolation
at all. Verified end-to-end with an actual `$`-containing password,
including a full deploy → backup → restore → still-serving round trip.
Also fixed along the way (found while actually testing, not assumed):
- Makefile never exported ENVIRONMENT, so `make up` alone (bypassing
deploy.sh) would have left the new secrets path unresolved.
- deploy.sh chmod'd the secret files 600, unreadable by the container's
own UID (www-data) — fixed to 644, relying on the containing directory
(700) to keep other host users out instead.
- backup.sh/restore.sh still called `mysqldump`/`mysql`, which don't
exist in the mariadb:11 image under those names — renamed to
mariadb-dump/mariadb. (This means neither script had actually
succeeded before now; both are verified working end-to-end here.)
The supplier/payment API keys remain passed the old way — nothing reads
them yet (bookstore-core is still a stub), so there's no live bug to fix
there; noted in .env.example that the same _FILE pattern should be used
once that code exists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
c9d637c907 |
Week 1 infrastructure: Docker environments, deploy pipeline, bookstore-core scaffold
Docker Compose environments for dev/staging/production (MariaDB, Redis, Caddy, Action Scheduler cron sidecar), an idempotent deploy script, git-hook-based deploy pipeline, backup/restore scripts, and the bookstore-core plugin stub with WooCommerce HPOS compatibility declared. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |