# Follow-Up Tickets

Project-local backlog for issues discovered during ticket work, manual QA, and automated watcher triage.

## Purpose

Use this file to capture **newly discovered work** that is **out of scope for the current ticket**. This is the repo-local holding area before a real Jira ticket exists.

## Rules

- Add one entry per distinct issue.
- Prefer evidence over speculation.
- Include exact log lines, commands, or file paths when available.
- Do **not** fix the issue from this file.
- Do **not** create Jira tickets automatically from this flow.
- The watcher may write only to this markdown file: `docs/FOLLOW_UP_TICKETS.md`.
- If a Jira ticket is ever needed, a human creates it later.
- Update an existing entry instead of creating duplicates for the same root problem.

## Entry Template

```md
## FUP-YYYYMMDD-HHMM-<slug>
- **Status:** OPEN
- **Priority:** HIGH | MEDIUM | LOW
- **Source:** docker watcher | manual QA | ticket work | CI | other
- **Repository:** virtual-sales-rep
- **Area:** postgres | frontend | graphql-server | docker | unknown
- **Summary:** One-sentence description of the issue.
- **Impact:** What breaks or risks user/dev workflows.
- **Evidence:**
  - Command/log/file proving the issue.
  - Additional concrete evidence.
- **Suggested next steps:**
  1. First verification step.
  2. Likely fix path.
  3. Validation after fix.
- **First seen:** ISO-8601 timestamp
- **Last seen:** ISO-8601 timestamp
- **Watcher fingerprint:** stable dedupe key
- **Related ticket:** TBC
```

---

## Open Follow-Ups

<!-- New entries are appended below this line by humans or automation. -->

## FUP-20260703-1348-rds-cart-order-schema-distilled-vs-legacy

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work (carried from PR #26, superseded by INT-2851 branch)
- **Repository:** virtual-sales-rep
- **Area:** postgres
- **Summary:** RDS `cart_items` / `order_items` are a distilled subset of the legacy Snowflake `CART_DETAIL` / `ORDER_DETAIL` tables and diverge in both column set and naming.
- **Impact:** Fields present in the Domino-derived legacy tables are silently absent from the RDS sidecar (INT-2851 hit this: `BULK_FACTOR_QT` was missing, breaking loose-charge math). Future features and any RDS ↔ `VIRTUALSALESREP` replication will keep discovering per-column gaps until the schemas are reconciled.
- **Evidence:**
  - Snowflake `VIRTUALSALESREP.DEV.CART_DETAIL` has ~39 columns (incl. `BULK_FACTOR_QT`, `LOOSE_CHARGE_QT`, `LOOSE_CHARGE_AM`, `MIN_ORDER_QT`, `MIN_ORDER_AM`, `LINE_NO`, `STATUS_CD`, `RED_PRICE_CD`, `ALTERNATE_CATLG_ID`, `INV_*`, `SHADOWBOX_*`, `CART_NEW_RELEASE_FLAG`).
  - INT-2851 added `bulk_factor_qt` + `loose_charge_qt` + `loose_charge_am` to `cart_items` and `bulk_factor_qt` to `order_items` (`infra/sql/005_loose_charge_bulk_factor.sql`) as a scoped fix, not a full alignment.
  - Full provenance-tagged register: `docs/cart-detail-divergences-INT-2851.html`.
- **Suggested next steps:**
  1. Decide whether RDS is the canonical cart/order store (distilled, purpose-built) or must mirror the legacy `CART_DETAIL`/`ORDER_DETAIL` structure/names.
  2. If mirroring: define the canonical RDS ↔ Snowflake column map and rename/extend the RDS tables + resolvers/data source/types accordingly.
  3. Tie into the existing replication-gap work (FUP-20260603-1405 / INT-2793).
- **First seen:** 2026-07-03T13:48:00-04:00
- **Last seen:** 2026-07-03T13:48:00-04:00
- **Watcher fingerprint:** rds-cart-order-schema-distilled-vs-legacy
- **Related ticket:** INT-2793

## FUP-20260610-1545-units-percent-source-field-unresolved

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Summary:** The requested "units percent" product field from the June 4 layout review is not yet mapped because the authoritative source column and business meaning are still unknown.
- **Impact:** The frontend cannot surface the requested value without risking the wrong field or label, so the meeting request remains only partially complete.
- **Evidence:**
  - June 4 meeting notes call out missing "units percent" as requested surfaced data.
  - Current GraphQL product shape in `packages/vsr-graphql-server/src/schema/product.graphql` does not expose any matching field.
  - Current product resolver/data-source chain surfaces `priceCode`, `configurationName`, `labelName`, `genreName`, `returnsAllowed`, and stock data, but no evident `units percent` mapping.
- **Suggested next steps:**
  1. Confirm the exact business meaning of "units percent" with the source-data owner.
  2. Identify the authoritative Snowflake/PRODUCT source column or derivation rule.
  3. Add the field to the GraphQL schema and frontend only after the source mapping is verified.
- **First seen:** 2026-06-10T15:45:00-04:00
- **Last seen:** 2026-06-10T15:45:00-04:00
- **Watcher fingerprint:** units-percent-source-field-unresolved
- **Related ticket:** TBC

## FUP-20260610-1546-restock-warning-business-rule-pending

- **Status:** OPEN
- **Priority:** LOW
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** frontend
- **Summary:** The business intent for the "Email me when available" / "Ships when available" warning remains pending confirmation, so the implementation preserved current behavior and only changed placement.
- **Impact:** The UI layout is improved, but final warning visibility may still need a follow-up change after business review.
- **Evidence:**
  - June 4 meeting notes explicitly marked warning visibility for further discussion with Dan Tus.
  - Frontend implementation moved the message into the purchase card but did not change the underlying behavior rule.
- **Suggested next steps:**
  1. Confirm the final visibility rule with Dan Tus or the owning product stakeholder.
  2. Update the row/detail presentation logic once the rule is settled.
  3. Re-validate new releases, out-of-stock rows, and product detail views after that change.
- **First seen:** 2026-06-10T15:46:00-04:00
- **Last seen:** 2026-06-10T15:46:00-04:00
- **Watcher fingerprint:** restock-warning-business-rule-pending
- **Related ticket:** TBC

## FUP-20260610-1547-genre-metadata-source-accuracy

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Summary:** Genre accuracy issues raised in the June 4 review appear to stem from source metadata rather than the current frontend rendering path.
- **Impact:** Genre filters, labels, and counts can still look wrong in the UI even when the frontend renders the available data correctly.
- **Evidence:**
  - June 4 meeting notes call out misclassified genres and incomplete counts, including the specific example of unexpected "Acoustic Rock" assignments.
  - Current frontend already surfaces `genreName`; no direct rendering bug was identified in the implementation pass.
  - Existing resolver path maps `genreName` directly from upstream product data, so incorrect values will flow through unchanged.
- **Suggested next steps:**
  1. Validate the authoritative source table/column and sync logic for genre metadata.
  2. Correct the upstream mapping or ingest rule feeding `genre_nm`.
  3. Re-validate genre filters, sidebar counts, and product metadata after the upstream fix lands.
- **First seen:** 2026-06-10T15:47:00-04:00
- **Last seen:** 2026-06-10T15:47:00-04:00
- **Watcher fingerprint:** genre-metadata-source-accuracy
- **Related ticket:** TBC

## FUP-20260603-1405-vsr-rds-snowflake-replication-gap

- **Status:** OPEN
- **Priority:** HIGH
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Summary:** RDS owns cart/order/notification mutations, but no replication path exists yet back to `VIRTUALSALESREP.{ENV}`.
- **Impact:** Snowflake-side consumers and reporting on cart/order/notification state remain stale or empty until replication is implemented.
- **Evidence:**
  - Architecture decision captured during INT-2699 work: reads from `VIRTUALSALESREP.*`, writes to RDS sidecar.
  - Snowflake DEV has mutation target tables present (`CART_HEADER`, `CART_DETAIL`, `ORDER_HEADER`, `ORDER_DETAIL`, `VSR_NOTIFICATION`) but currently no rows.
- **Suggested next steps:**
  1. Select replication strategy (Fivetran / DMS / app dual-write / event-driven).
  2. Define canonical column mapping RDS ↔ Snowflake for cart/order/notification payloads.
  3. Implement and validate environment-aware sync (`DEV`/`QA`/`PROD`).
- **First seen:** 2026-06-03T14:05:00-04:00
- **Last seen:** 2026-06-03T14:05:00-04:00
- **Watcher fingerprint:** vsr-rds-snowflake-replication-gap
- **Related ticket:** INT-2793

## FUP-20260603-1406-v-products-min-order-mapping-missing

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Summary:** `V_PRODUCTS.min_order_in` is currently a NULL stub because an equivalent `PRODUCT` source column is not yet confirmed.
- **Impact:** Min-order charge behavior can diverge between legacy and replatform logic if the upstream flag is not mapped correctly.
- **Evidence:**
  - `infra/sql/snowflake-views.sql` defines `NULL::VARCHAR AS min_order_in` with TODO reference.
  - `PRODUCT` column inspection did not reveal `MIN_ORDER_IN` under that exact name.
- **Suggested next steps:**
  1. Identify authoritative `PRODUCT` column or derivation rule for min-order flag.
  2. Replace NULL stub in `V_PRODUCTS` with mapped value.
  3. Add resolver-level validation test for min-order surcharge behavior.
- **First seen:** 2026-06-03T14:06:00-04:00
- **Last seen:** 2026-06-03T14:06:00-04:00
- **Watcher fingerprint:** v-products-min-order-mapping-missing
- **Related ticket:** INT-2795

## FUP-20260603-1407-v-merchandise-rollup-join-validation

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Summary:** `V_MERCHANDISE_ROLLUP` currently uses a provisional self-join (`child.ROLLUP_NO = parent.SELECTION_ID`) that must be validated against live data.
- **Impact:** Merchandise bundles/options may render incorrectly if parent-child linkage is wrong.
- **Evidence:**
  - `infra/sql/snowflake-views.sql` includes TODO to validate `ROLLUP_NO` relationship.
  - No explicit sequence/order column was found; `sequence_no` is currently NULL stub.
- **Suggested next steps:**
  1. Sample rollup products from `PRODUCT` and verify expected parent-child grouping.
  2. Confirm ordering semantics (replace NULL `sequence_no` when source is identified).
  3. Add fixture-based query tests for rollup output shape.
- **First seen:** 2026-06-03T14:07:00-04:00
- **Last seen:** 2026-06-03T14:07:00-04:00
- **Watcher fingerprint:** v-merchandise-rollup-join-validation
- **Related ticket:** INT-2795

## FUP-20260603-1408-v-inventory-qty-source-unresolved

- **Status:** OPEN
- **Priority:** HIGH
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Summary:** `V_INVENTORY` has NULL `cds_qty` and `vsr_qty` placeholders because qty source-of-truth is not yet wired in the VSR-only model.
- **Impact:** UI inventory quantities and any quantity-driven behaviors are incomplete/inaccurate.
- **Evidence:**
  - `infra/sql/snowflake-views.sql` defines `NULL::NUMBER AS cds_qty` and `NULL::NUMBER AS vsr_qty` with TODO notes.
  - Current fallback derives only `available_in` from `PRODUCT.STATUS_CD`.
- **Suggested next steps:**
  1. Confirm authoritative qty source for VSR runtime reads.
  2. Replace placeholder fields in `V_INVENTORY` with live qty mapping.
  3. Validate parity against expected inventory outputs in UI.
- **First seen:** 2026-06-03T14:08:00-04:00
- **Last seen:** 2026-06-03T14:08:00-04:00
- **Watcher fingerprint:** v-inventory-qty-source-unresolved
- **Related ticket:** INT-2793, INT-2795

## FUP-20260603-1409-vsr-control-and-log-ownership-clarified

- **Status:** OPEN
- **Priority:** LOW
- **Source:** ticket work
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Summary:** Ownership clarified: `VSR_CONTROL` is Snowflake read-only config; `VSR_LOG` remains Snowflake-only legacy audit log and may not be app-read.
- **Impact:** Prevents accidental scope creep into unnecessary RDS table creation and keeps integration focus on required runtime data.
- **Evidence:**
  - User decision recorded and captured in related Jira updates.
  - `VSR_CONTROL` and `VSR_LOG` table structures reviewed in `VIRTUALSALESREP.DEV`.
- **Suggested next steps:**
  1. Implement only the `VSR_CONTROL` read path needed for runtime flags.
  2. Do not mirror `VSR_LOG` into RDS.
  3. Revisit `VSR_LOG` only if a concrete app feature requires it.
- **First seen:** 2026-06-03T14:09:00-04:00
- **Last seen:** 2026-06-03T14:09:00-04:00
- **Watcher fingerprint:** vsr-control-and-log-ownership-clarified
- **Related ticket:** INT-2795

## FUP-20260603-1600-cds-inventory-scheduled-sync

- **Status:** OPEN
- **Priority:** HIGH
- **Source:** ticket work (INT-2699 inventory architecture discussion)
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Assign to:** Rob
- **Summary:** Implement the scheduled sync job that keeps the `PRODUCT` table stock qty field (and/or a dedicated RDS `inventory` table) current with live CDS inventory, replacing the NULL stubs in `V_INVENTORY`.
- **Background:**
  Inventory source is the `PRODUCT` table in `VIRTUALSALESREP.DEV`, refreshed on a schedule by a CDS/Domino sync job. `V_INVENTORY` currently reads `STATUS_CD` as a coarse availability flag; `cds_qty` / `vsr_qty` are NULL stubs. The sync job schedule and the exact stock qty column name in `PRODUCT` need to be confirmed and wired. See ADR D007 in `research/DECISION_LOG.md`.
- **Display rules to implement (server-side, GraphQL resolver):**
  - Admin users → raw `cds_qty` / `vsr_qty` values
  - Non-admin users → derived `stockLevel`: `IN_STOCK` / `LOW_STOCK` / `OUT_OF_STOCK`
  - LOW_STOCK threshold: TBD — confirm with Rob / product owner before implementing
- **Suggested next steps:**
  1. Confirm the authoritative stock qty column name(s) in `VIRTUALSALESREP.DEV.PRODUCT`.
  2. Confirm existing sync job schedule (frequency, trigger). Document in `research/DECISION_LOG.md`.
  3. Update `V_INVENTORY` to surface the confirmed qty column(s) in place of NULL stubs.
  4. Confirm LOW_STOCK threshold with product owner; implement `stockLevel` enum logic in `SnowflakeDataSource.getInventory()` or the `Product.stockStatus` resolver.
  5. Validate role-gating: admin context → qty values; non-admin context → `stockLevel` only.
- **First seen:** 2026-06-03T16:00:00-04:00
- **Last seen:** 2026-06-03T16:00:00-04:00
- **Watcher fingerprint:** cds-inventory-scheduled-sync
- **Related ticket:** INT-2793, INT-2733, ADR D007

## FUP-20260603-1601-inventory-intersession-race-condition

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work (INT-2699 inventory architecture discussion)
- **Repository:** virtual-sales-rep
- **Area:** graphql-server
- **Assign to:** Rob
- **Summary:** Inter-session inventory race condition: orders submitted directly to CDS by other parties between PRODUCT table refresh cycles are invisible to the VSR until the next sync, allowing two concurrent VSR sessions to both see and reserve the same stock.
- **Background:**
  The VSR reads inventory from a scheduled snapshot (`PRODUCT` table sync from CDS). Any order submitted directly into CDS — by another VSR session, a phone order, a manual entry — reduces real CDS inventory without the VSR knowing until the next scheduled refresh. Two concurrent VSR sessions can therefore both see stock as available, add the same UPC to cart, and both submit successfully to CDS. CDS may only be able to fulfil one. This is a known accepted risk for the POC (see ADR D007). The legacy Domino system avoided this with live per-row CDS calls, which is not replicated in the replatform.
- **Potential mitigations (evaluate post-POC):**
  - Pre-submit CDS inventory check in `submitOrder` before writing ORDER_HEADER to RDS
  - Event-driven inventory sync triggered by CDS order-status webhook
  - Optimistic locking: reserve qty in RDS at `addToCart` time, reconcile against CDS at checkout
  - Accept the race for POC (current posture) — physical media B2B, low concurrency, CDS team can handle backorder cases manually
- **First seen:** 2026-06-03T16:01:00-04:00
- **Last seen:** 2026-06-03T16:01:00-04:00
- **Watcher fingerprint:** inventory-intersession-race-condition
- **Related ticket:** INT-2793, INT-2733, FUP-20260603-1600, ADR D007

## FUP-20260603-1800-ephemeral-dev-user-debt

- **Status:** OPEN
- **Priority:** HIGH
- **Source:** ticket work (INT-2699 userId wiring)
- **Repository:** virtual-sales-rep
- **Area:** graphql-server, postgres, frontend
- **Summary:** DEV_MODE uses browser-fingerprint UUIDs to auto-provision placeholder `users` + `accounts` rows — these placeholder rows accumulate as untracked debt and must be scrubbed before INT-2697 auth wiring.

### What the system does

| Layer | File | Behaviour |
| ----- | ---- | --------- |
| Browser | `apps/vsr/src/utils/devUserId.ts` | Derives a fingerprint-seeded UUID from browser signals (timezone, screen, language), caches in `localStorage["vsr:deviceId"]`. Regenerates from same signals after storage wipe (new nonce, new UUID). |
| Apollo link | `apps/vsr/src/apollo/apolloClient.ts` | Sends UUID as `x-dev-user-id` header on every request when `VITE_DEV_MODE=true`. |
| Server context | `packages/vsr-graphql-server/src/middleware/authGuard.ts` | `buildContext()` calls `getOrCreateDevUser(deviceId)` for each unknown UUID; skipped when `DEV_MODE=false`. |
| RDS provisioning | `packages/vsr-graphql-server/src/dataSources/RdsDataSource.ts` | `getOrCreateDevUser(deviceId)` inserts into `accounts` (account_no=`DEV-{shortHandle}`, type=`internal`) and `users` (email=`dev+{uuid}@vsr.local`), returns the resulting `user_id`. ON CONFLICT resolves cleanly. |

### Debt created

1. **Dangling placeholder rows** — every unique device UUID that hits the server in DEV_MODE creates a permanent row in `users` and `accounts`. These have no real auth binding, no email, and no account_user_access row.
2. **No FK to `account_user_access`** — `getOrCreateDevUser` does not create an `account_user_access` row, so `requiresAccountAccess` bypasses are permanently required for these users in lab.
3. **All placeholder rows have `account_type = 'internal'`** — test scenarios that require `retail` account type will behave differently for placeholder users vs real users.
4. **Cart FK isolation only** — two browser tabs on the same machine will share the same `localStorage` entry and therefore the same `user_id`. Two different browsers (or incognito) will get different UUIDs and different carts.

### Migration path (INT-2697)

When `@theorchard/suite-auth` + Permissions Platform wiring is implemented:

1. **Remove `DEV_MODE` branch from `buildContext`** — replace with real JWT validation. The `userId` returned from context will be the real `users.user_id` (matched by email from JWT claims after provisioning).
2. **Delete `apps/vsr/src/utils/devUserId.ts`** — no longer needed.
3. **Delete `getOrCreateDevUser` from `RdsDataSource`** — no longer needed.
4. **Scrub placeholder rows** — run `DELETE FROM users WHERE email LIKE 'dev+%@vsr.local'` on DEV/QA before promoting to UAT. Cascades delete `carts`, `cart_items`, `order_headers`, `order_items` for those users.
5. **Remove `x-dev-user-id` from `apolloClient.ts`** — keep `x-dev-account-id` and `x-dev-ship-to` only if a reduced DEV bypass is still wanted for ship-to selection.

### Scrub query (safe to run on DEV, never on PROD)

```sql
-- Preview
SELECT user_id, email, account_no FROM users WHERE email LIKE 'dev+%@vsr.local';
-- Delete (cascades to carts, cart_items, orders)
DELETE FROM users WHERE email LIKE 'dev+%@vsr.local';
DELETE FROM accounts WHERE account_no LIKE 'DEV-%';
```

- **Suggested next steps:**
  1. Track this entry as a pre-INT-2697 pre-condition.
  2. Add scrub step to INT-2697 acceptance criteria.
  3. Validate no real user rows match the `dev+%@vsr.local` pattern before running scrub.
- **First seen:** 2026-06-03T18:00:00-04:00
- **Last seen:** 2026-06-03T18:00:00-04:00
- **Watcher fingerprint:** ephemeral-dev-user-debt
- **Related ticket:** INT-2697, INT-2699

## FUP-20260603-1801-thumbnail-url-not-resolving

- **Status:** INVESTIGATED — CDN auth required (see below)
- **Priority:** MEDIUM
- **Source:** ticket work (INT-2699 manual QA)
- **Repository:** virtual-sales-rep
- **Area:** graphql-server, frontend
- **Summary:** Product thumbnails do not display. `THUMBNAIL_IN` is a Y/N flag in Domino — it is NULL for all 42,057 products in Snowflake DEV. The CDN URL must be constructed from UPC, but the Domino CDN requires session auth.

### What's mapped

| Domino field | Snowflake column | GraphQL field | Resolver |
| ------------ | ---------------- | ------------- | -------- |
| `THUMBNAIL_IN` | `thumbnail_in` | `thumbnailUrl` | Constructed from UPC (see below) |

### Investigation findings (2026-06-08)

1. `SELECT DISTINCT THUMBNAIL_IN FROM VIRTUALSALESREP.DEV.PRODUCT` → **0 non-null rows** (42,057 total). The flag is not synced from Domino.
2. Confirmed from `research/old_site/VSR.dxl`: `THUMBNAIL_IN` is a Y/N attachment flag. The actual image URL is constructed from UPC:

   `https://www.redb2b.com/red/redmedstext.nsf/UPC/{UPC_CD}/$FILE/thumbnail135.jpeg`

3. `curl -sI` of the above URL → `HTTP/1.1 200 OK` but `Content-Type: text/html; charset=UTF-8` — the Domino server returns an HTML auth-redirect for unauthenticated requests. **Images require Domino session auth.**
4. Resolver updated to construct URLs from UPC (`productResolvers.ts`). Frontend components updated with `onError` fallback to placeholder div when CDN is unreachable.

### Blockers

- **Domino CDN**: images are served from `redb2b.com/red/redmedstext.nsf/UPC/{UPC_CD}/$FILE/thumbnail135.jpeg` — returns HTML auth-redirect for unauthenticated requests. Not viable for the replatform.
- **Orchard digital catalog**: `images.theorchard.io` serves v1/v2 cover art but coverage is only 8 of 365 active VSR products. VSR products are physical-only and mostly not in the Orchard digital catalog. CDN_URL is null in Snowflake for all 8 (legacy pre-2017 records). No UPC-based URL shortcut exists — hash-keyed only.
- **All image columns null in Snowflake**: `THUMBNAIL_IN`, `MERCH_IMAGE_URL`, `ATTACHMENT_LINK`, `ORCD_PROJECT_ID` are all null for all 365 active products in `VIRTUALSALESREP.DEV.PRODUCT`. The Domino sync does not carry file attachments into Snowflake.

### Path to resolution

**Only viable option:** One-time migration — bulk-export Domino NSF thumbnails to S3/CloudFront, keyed by UPC. Estimated effort: 1-2 days (NSF export script + S3 upload + CloudFront CDN setup). This is a separate ticket.

The resolver and frontend are ready: `thumbnailUrl` is constructed from UPC in `productResolvers.ts`. Once a new CDN serves images at `https://{cdn}/{upc}.jpg`, update the URL template in the resolver — nothing else needs to change.

### Current state (placeholder)

Frontend degrades gracefully: `onError` handler replaces any broken `<img>` with a placeholder div. Product rows and detail pages show an empty thumbnail cell — no broken image icons.

- **First seen:** 2026-06-03T18:01:00-04:00
- **Last updated:** 2026-06-09T09:45:00-04:00
- **Watcher fingerprint:** thumbnail-url-not-resolving
- **Related ticket:** INT-2699

## FUP-20260609-1125-thumbnail-backend-dataset-cdn-pipeline

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work, manual QA
- **Repository:** virtual-sales-rep
- **Area:** data-pipeline, snowflake, graphql-server
- **Summary:** Build a backend-owned thumbnail dataset and non-auth CDN path keyed by UPC so the replatform can render product images without a live Domino session.
- **Impact:** Product images only work today through a dev-only authenticated proxy to Domino. Production and QA cannot rely on Domino session cookies, and the current Snowflake dataset has no canonical thumbnail URL field to serve to GraphQL.
- **Evidence:**
  - `VIRTUALSALESREP.DEV.PRODUCT` has **365 active products**, but `THUMBNAIL_IN`, `MERCH_IMAGE_URL`, `ATTACHMENT_LINK`, and `ORCD_PROJECT_ID` are null or non-usable for image serving.
  - `THUMBNAIL_IN` is a Domino Y/N attachment flag, not a usable URL. The real source path is `red/redmedstext.nsf/UPC/{UPC_CD}/$FILE/thumbnail135.jpeg`, which requires authenticated access.
  - Authenticated Domino fetch succeeds for known UPC `803341226357` (`image/jpeg`), proving source images exist in NSF attachments, but that metadata is not represented in the backend dataset.
  - The Orchard digital image catalog is not a viable substitute: only 8 of 365 active VSR products matched, and those legacy rows had no usable CDN URL.

### Backend dataset work required

- Define a canonical image model in `VIRTUALSALESREP`. Preferred shape: add a dedicated `PRODUCT_IMAGE` / `PRODUCT_MEDIA` table keyed by UPC. Minimum fields: `upc_cd`, `thumbnail_url`, `source_system`, `source_attachment_name`, `content_type`, `etag`, `last_modified_at`, `synced_at`, `status` (`available` | `missing` | `error`). Do **not** overload `THUMBNAIL_IN` with URL semantics; keep it as the raw source flag if needed for audit/debugging.

- Bulk-export existing Domino NSF thumbnail attachments from `redmedstext.nsf`, keyed by UPC, into stable object storage such as `s3://<bucket>/vsr/thumbnails/{upc}.jpg`. Record explicit `missing` status for products with no upstream attachment so the app can distinguish "no source image" from sync failure.

- Publish the exported assets through a non-auth CDN. The final URL must be browser-safe and must not depend on Domino cookies, VPN-only origin fetches, or opaque per-session hashes.

- Backfill and expose the URL in the serving dataset. Populate the new image table (or a new `PRODUCT.thumbnail_url` column if the team chooses the simpler shape), then update `V_PRODUCTS` to expose a canonical `thumbnail_url` field consumed directly by GraphQL.

- Add recurring sync and validation. Run a scheduled job that ingests new/changed attachments from Domino until cutover, emits counts per run (`available`, `missing`, `failed`, `changed`), and validates MIME type, image dimensions, and duplicate UPC collisions.

### Suggested next steps

1. Decide dataset shape: dedicated `PRODUCT_IMAGE` table vs `PRODUCT.thumbnail_url` column.
1. Build a DEV-only backfill job from `redmedstext.nsf` → S3/CloudFront and capture coverage metrics (`available` / `missing` / `failed`).
1. Extend `infra/sql/snowflake-views.sql` so `V_PRODUCTS` exposes the canonical image URL from the new dataset.
1. Switch `productResolvers.ts` from `/thumb/{upc}` dev proxy URLs to the dataset-backed CDN URL once the backfill exists.
1. Add QA checks that compare live Domino availability against dataset coverage before PROD cutover.

- **First seen:** 2026-06-09T11:25:00-04:00
- **Last seen:** 2026-06-09T11:25:00-04:00
- **Watcher fingerprint:** thumbnail-backend-dataset-cdn-pipeline
- **Related ticket:** INT-2699, FUP-20260603-1801

## FUP-20260708-1500-rds-migration-runner-adoption

- **Status:** OPEN
- **Priority:** MEDIUM
- **Source:** ticket work (INT-2851 dev RDS stand-up)
- **Repository:** virtual-sales-rep
- **Area:** postgres | docker
- **Summary:** Adopt a real migration runner for RDS schema management before QA/PROD. Dev schema was stood up manually via single-statement MCP `execute` calls because the `postgresql-mcp` server forbids multi-statement/batch/comment SQL by design.
- **Impact:** Manual single-statement application is fine for a one-time DEV stand-up but does not scale, has no migration history/versioning, and is unsafe for QA/PROD. No repeatable, transactional, auditable deploy path exists yet.
- **Evidence:**
  - `postgresql-mcp` 0.2.0 security rules: "Multiple statements are not allowed", "SQL comments are blocked" (verified against PyPI/GitHub JaviMaligno/postgres_mcp).
  - DEV schema applied 2026-07-08 as ~29 individual `execute` calls (migrations 001–005 folded).
  - No migration-tracking table exists on dev RDS.
- **Suggested next steps:**
  1. Select a runner (dbmate / Flyway / sqitch / a thin `psql -f` wrapper) — decide after the initial backend is stable and with user approval.
  2. Add a schema-version/migration-history table and backfill it to reflect the current 001–005 baseline already applied to DEV.
  3. Wire the runner for QA/PROD (pipeline) and keep DEV manual-or-runner per team preference.
  4. Record the decision in `docs/DECISION_LOG.md`.
- **First seen:** 2026-07-08T15:00:00-04:00
- **Last seen:** 2026-07-08T15:00:00-04:00
- **Watcher fingerprint:** rds-migration-runner-adoption
- **Related ticket:** TBC

## FUP-20260708-1501-committed-rds-service-passwords

- **Status:** ACKNOWLEDGED (accepted for now)
- **Priority:** LOW
- **Source:** ticket work (INT-2851 credential troubleshooting)
- **Repository:** virtual-sales-rep
- **Area:** postgres | security
- **Summary:** `infra/sql/rds-service-users.sql` (tracked) embeds DEV service-account credentials. Owner has accepted this for the current closed development environment; no action required now.
- **Impact:** Limited to the closed DEV context. Revisit only if the repository access model or environment scope changes.
- **Note:** Prevention going forward is the priority — do not introduce any new secrets/PII/user info into commits. Existing item is owner-accepted; not to be re-raised.
- **First seen:** 2026-07-08T15:01:00-04:00
- **Last seen:** 2026-07-08T15:01:00-04:00
- **Watcher fingerprint:** committed-rds-service-passwords
- **Related ticket:** TBC
