> *This was generated by AI during triage.* > **Read first (re-triage, 2026-11-04 00:27 UTC, `origin/main` 13ba121).** The [Agent brief comment](https://github.com/hislordshipprof/african-market-AI-Ecommerce/issues/209#issuecomment-6619154143) is now the contract. Where it disagrees with the text below, the comment wins. Corrections to the text below: > - **Counts are stale and moving.** Now 57 missing out of 171 rows (158 distinct keys), not 58.

R2 holds 114 objects (113 `.webp` plus `catalog/alligator-pepper.jpg`, uploaded today), and 14 rows, not 15, point at `storefront-test.jpg`. Do not hard-code counts. > - **Hosts.** `storefrontPhoto` resolves `the https scheme /catalog/ ` and bare `catalog/ ` to an R2 key. Those URLs must be checked; they are not `not-r2`. > - **`APP_RUNTIME_PASSWORD` is required.** Under plain `tsx`, `NODE_ENV` is unset, and `runtimeDatabaseUrl` throws at db-module import without it.

Check env before importing the db module. > - **Check order.** A HEAD 404 cannot tell `NoSuchKey` from `NoSuchBucket`, so run `HeadBucket` first. Classify by `$metadata.httpStatusCode`: R2 reports 403 as `name: "Unknown"`. > - **No `ListObjectsV2` alternative.** Use HEAD per distinct key; this withdraws the addendum's option. > - `scripts/ticket-env.txt` has no 209 line yet.

The builder adds it, including `APP_RUNTIME_PASSWORD`. ## Parent #205 ## Agent Brief **Category:** bug **Summary:** Add a read-only script that lists every master product whose catalog photo is missing from R2, run it against the dev database, and paste the list in the PR.

Verified on `origin/main` 6983701 and the dev Neon branch from `.env.local` on 2026-11-03. **Current behavior (verified):** - `MasterProduct.imageUrl` (`master_products.image_url`) holds either `/api/catalog-image/catalog/ ` or nothing. In dev, 171 of 562 rows have a route URL (167 active, 4 rejected), and none carries a query string or another URL shape. - A read-only probe (HeadObject per key) found **58 of the 171 keys missing in R2**, `catalog/biltong.jpg` among them.

All 58 were clean 404s, with no 403s or other errors. Every missing name sorts A-K, which suggests an interrupted upload batch. - 15 of the missing rows are "Fluted Pumpkin Leaves" pointing at `catalog/storefront-test.jpg`.

They leaked from interrupted runs of `src/lib/storefront/service.test.ts` against the dev database, and 28 listings link to them. - The catalog-image route returns 404 both for a missing key and for any R2 error, a missing `R2_BUCKET`, or a key that fails its allowed-key rule (`catalog/ .(jpg|png|svg|webp)`). So route status alone cannot tell a missing photo from an outage. - `storefrontPhoto` accepts keys containing `.` in the name (`[a-zA-Z0-9._-]`) but the route does not.

Such a key renders as a broken photo even when the object exists. - `master_products` has RLS: `app_user` may SELECT every row, and writes need `app.is_platform_admin = 'true'`. `platform.masterProduct` (in `src/lib/db.ts`) runs as `app_user` without that flag, so it can read but not write. - `Listing` has no image column. Photos come only from the master product or the `/images/products/` cutout map. **Decisions taken during triage (owner away):** 1.

The source of truth is R2 `HeadObject` on each distinct key, not the HTTP route, so no running server is needed and an outage is never reported as a missing photo. 2. Read master products only through `platform.masterProduct` (runtime role, no platform-admin flag), so the database itself enforces read-only. Never use `platformAdmin`, `DIRECT_URL`, or the owner role. 3. Scope is master products only.

Categories (Wikimedia URLs or cutouts, none in R2), store banners and store collections are not checked. **Behavior to build:** - The command is run with `.env.local` loaded, the same way existing scripts load env (`dotenv -e .env.local -- tsx …`).

It is read-only. - Every master product with an `imageUrl` is classified as one of: - `ok`: the key exists. - `missing`: R2 returns 404 / `NotFound`. - `invalid-key`: the URL resolves to a catalog key that the route would refuse. - `not-r2`: a `/images/products/` cutout, another remote URL, or `R2_PUBLIC_URL`-based URLs when that var is set.

Resolve R2 URLs with the same rules as `storefrontPhoto`, including `the https scheme /catalog/ `. - Each distinct key is checked once, with bounded concurrency (8 or fewer). - Output is sorted by name then id. Each line has status, name, id, key and reason, followed by counts per class. - Exit 0 when the check completed, whether or not photos are missing.

Exit non-zero, without printing a partial list as if complete, on: a missing env key, a DB error, or any R2 response other than 200 or 404 (403, 5xx, throttling, network). Name the failing key or reason. **Key interfaces:** - A pure core function. It takes `{ products: { id, name, status, imageUrl }[], headObject: (key) => Promise }` (throwing on any other outcome) and returns the classified rows plus counts.

The CLI wraps it with `platform.masterProduct.findMany` and an S3 `HeadObjectCommand` adapter built like `getR2Client()`. **Seams:** - Core function: unit tests with a fake `headObject`. - DB read: an integration test against the real lane database. It seeds master products with platform-admin access, then reads them back through the script's reader (`platform.masterProduct`).

Through that same reader, a create must fail and an update must change 0 rows, which proves the reader cannot write. - R2: faked at the `headObject` boundary in tests. The real R2 run is manual, against the dev branch; its output goes in the PR. - Store-owned tables: none are read or written. `master_products` is platform-shared (no `store_id`), so no `storeScoped()` cross-store test applies.

Add a test that the reader uses only `platform.masterProduct`, so nobody later adds a listings join without RLS review. - Auth paths: none touched. **Acceptance criteria:** - [ ] Given products whose keys are present, missing, malformed (`catalog/a.b.jpg`, `ca

---

## Vectle citations

- Exact version: <https://vectle.com/skills/skl_2VPfZ9DQG7_tAEbxlL7UwA/versions/skv_ZUJCfOAp_xXc9eL3__JFMQ>
- Canonical HTML skill page: <https://vectle.com/skills/skl_2VPfZ9DQG7_tAEbxlL7UwA>
- Current Markdown URL: <https://vectle.com/skills/skl_2VPfZ9DQG7_tAEbxlL7UwA/markdown>

## Selected public attribution and original notices/references

```json
{
  "author": "Fin",
  "client": null,
  "notices_and_references": []
}
```
