---

title: Activity Log
tags: [audit, log, append-only]
created: 2026-05-28
---

# Activity Log

Append-only log of every `git push` from `backend` and `frontend`. Newest
entries on top. Maintained by agents per the rule in `../AGENTS.md`.

---

### 2026-06-07 — backend@e8cb64c, frontend@05e9bcd — DB audit bounded high cleanup and blog status enum

**Commits:** `e8cb64c` `05e9bcd`
**Touched:** backend `src/services/blog/blogPostgresSchema.ts`, `src/db/migrations/0027_blog_post_status_enum.sql`, `src/db/backfill/backfill-users.ts`, `src/db/repositories/drizzle/DrizzleUserRepo.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, targeted regression tests, `package.json`, `package-lock.json`; frontend `package.json`, `Dockerfile`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`, `RTK.md`
**Why:** Normalize the audit fixed table so early C/H closures are keyed by issue ID, close H38 by enforcing `blog_post_status` in runtime schema and migration, and keep residual H1/H30-H33-style paths bounded with regression coverage. Added the CI/build freeze rule to the canonical docs RTK so agents diagnose CI before changing build procedure.
**Verification:** backend `npm test -- --runInBand __tests__/blog-postgres-schema.test.ts __tests__/drizzle-payment-repo-export.test.ts __tests__/drizzle-user-repo.test.ts __tests__/drizzle-marketplace-repo-batch.test.ts __tests__/payment-migration.service.test.ts __tests__/auth-store-pg-query.test.ts __tests__/backfill-guard.test.ts` (7 suites / 52 tests), backend `npm run typecheck`, backend/frontend scoped `git diff --check`; frontend/backend version metadata confirmed at v2.9.43.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@55321c0, frontend@525d50a — restore last-green Woodpecker build procedure

**Commits:** `55321c0` `525d50a`
**Touched:** backend `Dockerfile.prod`, `.woodpecker/production.yml`, `package.json`, `package-lock.json`; frontend `.woodpecker/production.yml`, `Dockerfile`, `package.json`; docs `09 - Audits/Activity Log.md`, `11 - Testing/Smoke and Regression Procedure.md`
**Why:** Restore backend build/deploy procedure to match the last successful backend build at `5d7d2af` and restore the paired frontend production build shape from `ade7352`. The prior Docker cleanup/prune hardening commits were too broad for the CI authentication/storage investigation and have been superseded. Backend again uses the original two-stage Dockerfile with builder `npm ci`, transpile, production `npm ci --omit=dev`, and `dist` copy. Backend/frontend Woodpecker production pipelines again run the simple local Docker build and compose deploy without cleanup blocks.
**Verification:** backend/frontend `woodpecker-cli lint .woodpecker/production.yml`; backend production build files diff clean against `5d7d2af`; frontend production build files diff clean against `ade7352` except `NEXT_PUBLIC_APP_VERSION`; backend/frontend scoped `git diff --check`; backend/frontend version metadata confirmed at v2.9.42. Woodpecker backend `#93` passed clone, typecheck, build-and-deploy, and notify; frontend `#86` passed clone, build-and-deploy, and notify.
**Linked docs updated:** [[11 - Testing/Smoke and Regression Procedure]]

---

### 2026-06-07 — backend@9363d8c, frontend@03025c8 — Woodpecker Docker ENOSPC cleanup hardening

**Commits:** `e2c74f9` `d640ec1` `9363d8c` `03025c8`
**Touched:** backend `Dockerfile.prod`, `.woodpecker/production.yml`, `package.json`, `package-lock.json`; frontend `.woodpecker/production.yml`, `Dockerfile`, `package.json`; docs `09 - Audits/Activity Log.md`, `11 - Testing/Smoke and Regression Procedure.md`
**Why:** Woodpecker CI showed repeated Docker build `ENOSPC` failures across backend and frontend. Backend `89` failed in `get-version` from Mojtaba's malformed `package.json` and was repaired by the later backend version sync. Backend `87/88/90` and frontend `83` failed in Docker build/install layers because the arm64 CI host had severely exhausted Docker storage (`241GB` images with `237.6GB` reclaimable, plus `17.19GB` reclaimable build cache). Backend Docker build now avoids the second production-stage `npm ci` by pruning dev dependencies after transpile and copying the pruned runtime `node_modules`. Backend and frontend production pipelines now serialize Docker cleanup with a shared lock, prune old unused tagged images and build cache with timeouts, and avoid Docker volumes.
**Verification:** `woodpecker-cli` investigation with local `WOODPECKER_SERVER`/`WOODPECKER_TOKEN`; backend/frontend `woodpecker-cli lint .woodpecker/production.yml`; backend `DOCKER_BUILDKIT=1 docker build -f Dockerfile.prod -t escrow-backend-docker-smoke:2.9.40 .` passed and the smoke image was removed; backend/frontend scoped `git diff --check`; backend/frontend version metadata confirmed at v2.9.41. Follow-up Woodpecker pipelines `backend#92` and `frontend#85` are pending because the earlier killed frontend `#84` left `build-and-deploy` reported as running on the arm64 agent; agent restart or manual kill may be needed before the queued pipelines can prove green.
**Linked docs updated:** [[11 - Testing/Smoke and Regression Procedure]]

---

### 2026-06-07 — frontend@a433067, backend@9427009 — frontend Docker Yarn cache ENOSPC fix

**Commits:** `a433067` `9427009`
**Touched:** frontend `Dockerfile`, `package.json`; backend `package.json`, `package-lock.json`; docs `09 - Audits/Activity Log.md`, `11 - Testing/Smoke and Regression Procedure.md`
**Why:** The frontend production Docker build failed in the dependency install layer with `ENOSPC` while copying `country-flag-icons` from the persistent Yarn v6 cache into `/app/node_modules`. The frontend install layer now keeps the locked BuildKit/Yarn mutex hardening for the prior `ETXTBSY` failure, clears `/root/.yarn` before and after the frozen install, and uses `--link-duplicates` to reduce node_modules duplication. Backend version metadata was also synchronized to v2.9.39 after integrating the latest Forgejo change and repairing its truncated `package.json` manifest.
**Verification:** frontend deps-only Docker smoke derived from the production install stage passed through `yarn install --frozen-lockfile --production=false --network-timeout 600000 --link-duplicates`, confirmed `node_modules/country-flag-icons` exists, and confirmed `/root/.yarn/v6` is absent after install; Docker cleanup reclaimed 15.09GB build cache plus 1.961GB dangling images; frontend/backend scoped `git diff --check`; frontend/backend version metadata confirmed at v2.9.39. Pushed to Forgejo.
**Linked docs updated:** [[11 - Testing/Smoke and Regression Procedure]]

---

### 2026-06-07 — backend@957c356, frontend@f699b15 — DB audit very-high transaction closeout H16-H18

**Commits:** `957c356` `f699b15`
**Touched:** backend `src/services/marketplace/RequestTemplateService.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `src/db/repositories/interfaces/IMarketplaceRepo.ts`, `src/services/auth/authController.ts`, `src/services/auth/authStore.ts`, `__tests__/request-template-batch-convert-cache.test.ts`, `__tests__/auth-store-pg-query.test.ts`, `__tests__/db-audit-auth-controller-saves.test.ts`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close High H16-H18 from the DB Query & Schema Audit. Proposal-backed batch template conversions now create the request, offer, accepted-offer state, and selected-offer link in one serializable vital-db transaction while leaving template usage accounting explicitly non-vital. Template payment completion uses one serializable bulk status update with an all-rows-updated guard. Email-code registration now deletes the temp verification, increments referrer signup count, and persists the verified user plus refresh token in one serializable auth-store transaction.
**Verification:** backend `npm test -- --runTestsByPath __tests__/request-template-batch-convert-cache.test.ts __tests__/db-audit-money-flow-transactions.test.ts __tests__/db-audit-auth-controller-saves.test.ts __tests__/auth-store-pg-query.test.ts --runInBand` (4 suites / 31 tests), `npm run typecheck`, `scripts/smoke/db-audit-service-regressions.sh` (19 suites / 82 tests), backend/frontend scoped `git diff --check`; frontend/backend version metadata confirmed at v2.9.38. Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@259f3fb, frontend@d9a59bd — DB audit H19-H21 auth save consolidation

**Commits:** `259f3fb` `d9a59bd`
**Touched:** backend `src/services/auth/authController.ts`, `__tests__/db-audit-auth-controller-saves.test.ts`, `scripts/smoke/db-audit-service-regressions.sh`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close High H19-H21 from the DB Query & Schema Audit. Login, Google sign-in, and Telegram auth now use the token helper without immediate persistence, stage audited mutations, and perform one final user save through the transactional save path. Telegram Mini App retry behavior remains preserved with no replay/dedup rejection added.
**Verification:** backend `npm test -- --runTestsByPath __tests__/db-audit-auth-controller-saves.test.ts __tests__/auth-store-pg-query.test.ts --runInBand` (2 suites / 18 tests), `npm run typecheck`, `scripts/smoke/db-audit-service-regressions.sh` (19 suites / 77 tests), backend/frontend scoped `git diff --check`; frontend/backend version metadata confirmed at v2.9.37. Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@5d7d2af, frontend@ade7352 — DB audit H10 sweep balance probe parallelism

**Commits:** `5d7d2af` `ade7352`
**Touched:** backend `src/services/payment/wallets/sweepService.ts`, `__tests__/sweep-service.test.ts`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close High H10 from the DB Query & Schema Audit. Derived-destination sweeps now fan out ERC-20 token balance probes with bounded concurrency before preserving sequential sweep/broadcast/mark-success handling, reducing large sweep runs from one RPC round-trip per destination in series to a tunable parallel probe phase.
**Verification:** backend `npm test -- --runTestsByPath __tests__/sweep-service.test.ts --runInBand` (31 tests), `npm run typecheck`, `scripts/smoke/db-audit-service-regressions.sh` (18 suites / 73 tests), backend/frontend scoped `git diff --check`; frontend/backend version metadata confirmed at v2.9.36. Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@8835068, frontend@73d1407 — DB audit C2 chat query bounds closeout

**Commits:** `8835068` `73d1407`
**Touched:** backend `src/db/repositories/drizzle/DrizzleChatRepo.ts`, `src/db/schema/chat.ts`, `src/db/migrations/0026_chat_settings_archived_idx.sql`, `__tests__/drizzle-chat-repo.test.ts`, `__tests__/db-audit-high-indexes.test.ts`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/C2-DrizzleChatRepo-Partial-Fix-Report.md`, `09 - Audits/Activity Log.md`
**Why:** Finish C2 from the DB Query & Schema Audit and the partial-fix report. Chat reads now have a bounded query builder, SQL pagination for SQL-pushable `findForUser` predicates, `findOne` id/limit fast paths, chat `type` predicate pushdown, bounded fallback/search scans, and an index for archived-chat filtering.
**Verification:** backend `npm run typecheck`; `npm test -- --runTestsByPath __tests__/drizzle-chat-repo.test.ts __tests__/db-audit-high-indexes.test.ts --runInBand` (2 suites / 9 tests); `scripts/smoke/db-audit-service-regressions.sh` (18 suites / 73 tests); backend/frontend scoped `git diff --check`; frontend version metadata confirmed at v2.9.35. Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]], [[09 - Audits/C2-DrizzleChatRepo-Partial-Fix-Report]]

---

### 2026-06-07 — backend@c3ad979, frontend@a8791b1 — DB audit medium transaction closeout M13/M14/M17

**Commits:** `c3ad979` `a8791b1`
**Touched:** backend `src/services/marketplace/RequestTemplateService.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `src/db/repositories/interfaces/IMarketplaceRepo.ts`, `src/services/payment/paymentCoordinator.ts`, `src/services/auth/authStore.ts`, `src/services/user/userController.ts`, `__tests__/request-template-batch-convert-cache.test.ts`, `__tests__/payment-coordinator.test.ts`, `__tests__/auth-store-pg-query.test.ts`, `__tests__/db-audit-money-flow-transactions.test.ts`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close Medium M13/M14/M17 from the DB Query & Schema Audit. Single template conversion now creates the request and initial offer through one serializable vital-db repo transaction while leaving usage accounting on the intentionally non-vital side. PG payment completion now has a real notify-only follow-up path that skips DB writes. Profile email verification now promotes `pending_email` through one conditional SQL update with explicit conflict handling.
**Verification:** backend `npm run typecheck`, `npm test -- --runTestsByPath __tests__/request-template-batch-convert-cache.test.ts __tests__/payment-coordinator.test.ts __tests__/auth-store-pg-query.test.ts __tests__/db-audit-money-flow-transactions.test.ts --runInBand` (4 suites / 23 tests), `scripts/smoke/db-audit-service-regressions.sh` (18 suites / 69 tests); frontend version check confirmed no tracked `2.9.33` references outside `.git`. Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@5364704, frontend@c34ab0a — DB audit money-flow transaction closeout H14/H15/H27/H29

**Commits:** `5364704` `c34ab0a`
**Touched:** backend `src/services/payment/paymentCoordinator.ts`, `src/services/payment/request-network/requestNetworkWebhook.ts`, `src/services/payment/paymentController.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `__tests__/db-audit-money-flow-transactions.test.ts`, `__tests__/sec-022-rn-webhook-fail-closed.test.ts`, `scripts/smoke/db-audit-service-regressions.sh`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close High H14/H15/H27/H29 from the DB Query & Schema Audit. AML provider-fee ledger writes now happen inside the PG payment coordinator transaction. The legacy Request Network confirmation webhook routes through `PaymentCoordinator` instead of split payment/PR writes. Manual `verifyPayment` creates a non-terminal row and completes it through the coordinator. `updatePurchaseRequest` locks the purchase request and selected offer before changing `selectedOfferId`.
**Verification:** backend `npm run typecheck`, `npm test -- --runTestsByPath __tests__/db-audit-money-flow-transactions.test.ts __tests__/sec-022-rn-webhook-fail-closed.test.ts __tests__/payment-coordinator.test.ts --runInBand`, `scripts/smoke/db-audit-service-regressions.sh` (18 suites / 62 tests), scoped `git diff --check`; frontend scoped `git diff --check -- package.json Dockerfile`. Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@c39b14a — DB audit schema/precision batch M38/M40/M41

**Commits:** `c39b14a`
**Touched:** backend `src/services/marketplace/reviewStore.ts`, `src/db/repositories/drizzle/DrizzlePaymentRepo.ts`, `src/db/schema/trezorAccount.ts`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close Medium M38 by migrating review queries to UUID FK columns (`reviewer_user_id`) instead of legacy text columns; rebuild unique index on UUID. Close Medium M40 by computing `FundsLedgerBalance` derived fields with `decimal.js` instead of JS float64 to prevent rounding errors on 18-decimal crypto amounts. Close Medium M41 by documenting the concurrency requirement for `trezor_accounts.next_address_index` (SELECT...FOR UPDATE or sequence migration).
**Verification:** backend `npm run typecheck` (clean), `npm test -- --runTestsByPath __tests__/db-audit-critical-fks.test.ts __tests__/drizzle-marketplace-repo-batch.test.ts --runInBand` (2 suites / 8 tests passed). Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@4766eba, frontend@fccccbc — DB audit H22/H23/H28/H37 dispute integrity closeout

**Commits:** `8fc2309` `f5e53cb` `4766eba` `fccccbc`
**Touched:** backend `src/services/dispute/DisputeService.ts`, `src/db/repositories/drizzle/DrizzleDisputeRepo.ts`, `src/db/repositories/drizzle/DrizzleReleaseHoldRepo.ts`, `src/db/repositories/drizzle/DrizzleChatRepo.ts`, `src/services/admin/dataCleanupService.ts`, `src/services/admin/ttlCleanupJob.ts`, `src/db/schema/dispute.ts`, `src/db/migrations/0025_dispute_enums.sql`, `__tests__/db-audit-dispute-integrity.test.ts`, `scripts/smoke/db-audit-service-regressions.sh`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close High H22/H23/H28/H37. Dispute create now uses a serializable vital transaction for the dispute insert, immediate chat cleanup on rollback, and a TTL orphan-dispute-chat sweep for crash recovery across the split vital/non-vital pools. Dispute resolution and release-hold clearing run in one serializable vital transaction. Dispute timeline/evidence updates use atomic JSONB append expressions. Dispute status/priority/category are pgEnums.
**Verification:** backend `npm run typecheck`, `npm test -- --runTestsByPath __tests__/db-audit-dispute-integrity.test.ts --runInBand`, `scripts/smoke/db-audit-service-regressions.sh` (17 suites / 58 tests), scoped `git diff --check`; frontend scoped `git diff --check -- package.json Dockerfile`. Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@f5e53cb — DB audit medium batch M24/M30/M39

**Commits:** `f5e53cb`
**Touched:** backend `src/db/repositories/drizzle/DrizzlePaymentRepo.ts`, `src/db/schema/blogPost.ts`, `src/db/schema/notification.ts`, `src/db/migrations/0021_needy_carlie_cooper.sql`, `package.json`, `package-lock.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close Medium M24 by replacing in-memory deduplication with Postgres DISTINCT ON for payment latest-per-request lookups. Close Medium M30 by adding GIN index on blog_posts.tags and converting status to pgEnum. Close Medium M39 by converting notifications.type and notifications.category to pgEnum for DB-level type safety.
**Verification:** backend `npm run typecheck` (clean), `npm test -- --runTestsByPath __tests__/db-audit-critical-fks.test.ts __tests__/drizzle-marketplace-repo-batch.test.ts --runInBand` (2 suites / 8 tests passed). Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@8fc2309 — DB audit M43/M44 missing FKs + H37 dispute enums

**Commits:** `8fc2309`
**Touched:** backend `src/db/schema/purchaseRequest.ts`, `src/db/schema/dispute.ts`, `src/services/dispute/DisputeService.ts`, `src/db/repositories/drizzle/DrizzleDisputeRepo.ts`, `src/db/repositories/drizzle/DrizzleReleaseHoldRepo.ts`, `src/db/migrations/0020_luxuriant_queen_noir.sql`, `src/db/migrations/0025_dispute_enums.sql`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close Medium M43/M44 by adding FK constraints to purchase_requests and child tables (categoryId, selectedOfferId, deliveryInfo, deliveryAddress, sellerDeliveryInfo, deliveryAttempts, serviceInfo, specifications, preferredSellers). Also close High H37 by converting disputes status/priority/category from plain text to pgEnum. DisputeService now creates disputes through the transaction-bound Drizzle repo while preserving legacy chat compatibility.
**Verification:** backend `npm run typecheck` (clean), `npm test -- --runTestsByPath __tests__/db-audit-critical-fks.test.ts __tests__/drizzle-marketplace-repo-batch.test.ts --runInBand` (2 suites / 8 tests passed). Pushed to Forgejo.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@5752f13 — DB audit Low-priority batch (L1–L10)

**Commits:** `5752f13`
**Touched:** backend `src/services/payment/amnScanner/amnScannerPayInService.ts`, `src/db/repositories/drizzle/DrizzlePaymentRepo.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `src/db/repositories/drizzle/DrizzlePointsRepo.ts`, `src/db/schema/idMap.ts`, `src/db/schema/trezorAccount.ts`, `src/db/schema/sellerOffer.ts`, `src/db/schema/users.ts`, `src/db/migrations/0019_stormy_meltdown.sql`, `package.json`, `package-lock.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close the remaining 9 Low-priority findings from the DB Query & Schema Audit in a single batch: consolidate AMN scanner updates (L1), deduplicate getStats aggregate (L2), cap unbounded offer lookups (L3), add missing schema indexes (L5, L6), align seller offer numeric precision with project convention (L7), extract walletAddress for indexed payment matching (L8), remove dead query variable (L9), clamp leaderboard limit (L10). Also includes prior uncommitted audit work (chat SQL pushdown, auth batch hydration, review/level parallelization). Migration `0019_stormy_meltdown.sql` covers the new columns and indexes.
**Verification:** backend `npm run typecheck` (clean), `npm test -- --runTestsByPath __tests__/drizzle-payment-repo-export.test.ts __tests__/drizzle-user-repo.test.ts __tests__/drizzle-marketplace-repo-batch.test.ts __tests__/seller-offer-service.test.ts __tests__/db-audit-high-indexes.test.ts --runInBand` (5 suites / 16 tests passed). Push to origin failed due to remote network reset; commit is ready locally.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@b743b5e, frontend@f1e5f3a — DB audit C7 dispute relation FKs
**Commits:** `b743b5e` `f1e5f3a`
**Touched:** backend `src/db/schema/dispute.ts`, `src/db/migrations/0024_disputes_uuid_fks.sql`, `src/db/repositories/drizzle/DrizzleDisputeRepo.ts`, `__tests__/db-audit-critical-fks.test.ts`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Close Critical C7 from the DB Query & Schema Audit by converting dispute purchase-request/user relationship columns from loose text IDs to UUID FKs while keeping legacy Mongo ObjectId callers working through repo-level resolution and legacy display mapping.
**Verification:** backend `npm test -- --runTestsByPath __tests__/db-audit-critical-fks.test.ts --runInBand`, `scripts/smoke/db-audit-service-regressions.sh` (16 suites / 55 tests), `npm run typecheck`, scoped `git diff --check`; frontend scoped `git diff --check -- Dockerfile package.json`. Pushed to Forgejo; `origin` skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — frontend@607587c, backend@189b0ab — frontend Docker Yarn install hardening

**Commits:** `607587c` `189b0ab`
**Touched:** frontend `Dockerfile`, `package.json`; backend `package.json`, `package-lock.json`; docs `09 - Audits/Activity Log.md`
**Why:** The frontend production Docker build failed in the dependency install layer with `@sentry/cli` / Yarn `ETXTBSY` while unpacking executable files. The Dockerfile now activates the exact Yarn version in its own Corepack layer and serializes the shared BuildKit Yarn cache with a Yarn mutex during `yarn install`.
**Verification:** frontend isolated BuildKit dependency-layer build using the production install command passed through `[5/5] Building fresh packages` without the `ETXTBSY` failure; frontend `git diff --check -- Dockerfile package.json`; backend `git diff --check -- package.json package-lock.json`; version metadata reads `2.9.27` in both repos. Pushed to Forgejo; `origin` skipped.
**Linked docs updated:** None

---

### 2026-06-07 — backend@38d0e76, frontend@051681f — DB audit C6 notification user FK

**Commits:** `38d0e76` `051681f`
**Touched:** backend `src/db/schema/notification.ts`, `src/services/notification/notificationPostgresSchema.ts`, `src/db/repositories/drizzle/DrizzleNotificationRepo.ts`, `src/services/notification/NotificationService.ts`, `src/db/migrations/0023_notifications_user_uuid_fk.sql`, `__tests__/db-audit-critical-fks.test.ts`, `__tests__/drizzle-notification-repo-bulk.test.ts`, `scripts/smoke/db-audit-service-regressions.sh`, `package.json`, `package-lock.json`; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Restart Critical/High work from the canonical DB Query & Schema Audit after verifying M16. C6 closes notification recipient schema integrity by converting `notifications.user_id` to a UUID FK to `users(id)` while preserving legacy caller compatibility through repo-level user ID resolution.
**Verification:** backend `npm test -- --runTestsByPath __tests__/auth-store-pg-query.test.ts --runInBand` (M16 still green), `npm test -- --runTestsByPath __tests__/db-audit-critical-fks.test.ts __tests__/drizzle-notification-repo-bulk.test.ts __tests__/notification-service-repo.test.ts --runInBand`, `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (16 suites / 54 tests), `npm run typecheck`, scoped `git diff --check`; frontend `git diff --check package.json`. Pushed to Forgejo; `origin` skipped; direct `dev` remotes removed and not used.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@fcee958, frontend@d600fca — DB audit M16 deleted-email release atomicity

**Commits:** `fcee958` `d600fca`
**Touched:** backend `src/services/auth/authStore.ts`, `__tests__/auth-store-pg-query.test.ts`, `scripts/smoke/db-audit-service-regressions.sh`, `package.json`, `package-lock.json`; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Continue the next audit round after confirming M2 (`2abba67`) and M3 (`61aa42a`) were already correctly integrated and pushed. M16 closes the soft-deleted email release race by replacing the read-then-write flow with one conditional `UPDATE users ... WHERE email/status ... RETURNING` statement.
**Verification:** backend `npm test -- --runTestsByPath __tests__/auth-store-pg-query.test.ts --runInBand`, `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (15 suites / 53 tests), `npm run typecheck`, `git diff --check`; frontend `git diff --check package.json`. Forgejo was current before the commits and both code commits were pushed; direct `dev` SSH remote had timed out earlier and `origin` remained intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-07 — backend@2c5e80d, frontend@1f8fdc9 — DB audit Waves 5-6 chat create and points transaction enforcement

**Commits:** `2c5e80d` `1f8fdc9`
**Touched:** backend `src/db/repositories/drizzle/DrizzleChatRepo.ts`, `src/db/repositories/drizzle/DrizzleUserRepo.ts`, `src/db/repositories/drizzle/DrizzlePaymentRepo.ts`, `__tests__/drizzle-chat-repo.test.ts`, `__tests__/drizzle-user-repo.test.ts`, `package.json`, `package-lock.json`; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Continue the 8-wave Critical/High plan after pulling Mojtaba's Forgejo changes (`backend@6e097c5`, `frontend@bcf9f03`). Wave 5 closes H13 by inserting the chat welcome message atomically with the chat row; Wave 6 closes H25 by refusing point balance/ledger writes on an unbound repo. The backend commit also keeps the pulled PaymentRepo user-id batch resolver null-safe for typecheck.
**Verification:** backend `npm test -- --runTestsByPath __tests__/drizzle-chat-repo.test.ts __tests__/drizzle-user-repo.test.ts --runInBand`, `npm test -- --runTestsByPath __tests__/drizzle-payment-repo-export.test.ts __tests__/drizzle-chat-repo.test.ts __tests__/drizzle-user-repo.test.ts --runInBand`, `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (14 suites / 43 tests), `npm run typecheck`, `git diff --check`; frontend `git diff --check` for package bump. Pushed to Forgejo; direct `dev` SSH remote timed out and `origin` remained intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@f22794a/51ca048, frontend@4a86dc7 — DB audit Wave 4 delivery-code atomicity

**Commits:** `f22794a` `51ca048` `4a86dc7`
**Touched:** backend `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `__tests__/drizzle-marketplace-repo-batch.test.ts`, `package.json`, `package-lock.json`; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md`
**Why:** Continue the 8-wave Critical/High plan. Wave 4 closes H24 by making delivery-code verification a single conditional database update that consumes the code only when it is still unused, unexpired, and matches the submitted code; result rows are returned directly and a bounded read is used only after update misses to explain failure.
**Verification:** backend `npm test -- --runTestsByPath __tests__/drizzle-marketplace-repo-batch.test.ts --runInBand`, `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (14 suites / 40 tests), `npm run typecheck`, `git diff --check`; frontend `git diff --check` for package bump. Pushed to Forgejo; `origin` remained unavailable and was intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@61aa42a/885745e, frontend@c9e9ccf — DB audit Wave 3 points/referral consistency

**Commits:** `61aa42a` `885745e` `c9e9ccf`
**Touched:** backend `src/services/points/PointsService.ts`, `src/db/repositories/drizzle/DrizzlePointsRepo.ts`, `src/db/repositories/interfaces/IPointsRepo.ts`, `__tests__/points-referral-reward.test.ts`, `scripts/smoke/db-audit-service-regressions.sh`, `package-lock.json`; medium-batch files from `61aa42a`; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`
**Why:** Continue the 8-wave Critical/High plan. Wave 3 closes H11/H12/H26 by moving referral rewards to one serializable repo mutation that commits points, referralStats, and the ledger row together; the docs also catch up the pushed `61aa42a` medium batch.
**Verification:** backend `npm test -- --runTestsByPath __tests__/points-referral-reward.test.ts --runInBand`, `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (14 suites / 38 tests), `npm run typecheck`, `git diff --check`; frontend `git diff --check` for package bump. Pushed to Forgejo; `origin` remained unavailable and was intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@2abba67/3955430, frontend@698c4d7 — DB audit Wave 2 missing indexes

**Commits:** `2abba67` `3955430` `698c4d7`
**Touched:** backend `src/db/schema/fundsLedgerEntry.ts`, `src/db/schema/payment.ts`, `src/db/schema/purchaseRequest.ts`, `src/db/schema/sellerOffer.ts`, `src/db/migrations/0021_missing_indexes.sql`, `__tests__/db-audit-high-indexes.test.ts`, `scripts/smoke/db-audit-service-regressions.sh`, plus medium-batch query/index cleanup files from `2abba67`; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`
**Why:** Continue the 8-wave Critical/High plan. Wave 2 completes H39-H42 high missing indexes; the earlier `2abba67` backend push also landed medium query/index cleanup in v2.9.19.
**Verification:** backend `npm test -- --runTestsByPath __tests__/db-audit-high-indexes.test.ts --runInBand`, `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (13 suites / 37 tests), `npm run typecheck`, `git diff --check`; frontend `git diff --check` for package bump. Pushed to Forgejo; `origin` remained unavailable and was intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@5ff0013, frontend@8434f32 — DB audit Wave 1 unbounded read caps

**Commits:** `5ff0013` `8434f32`
**Touched:** backend `src/db/repositories/drizzle/DrizzlePaymentRepo.ts`, `src/db/repositories/drizzle/DrizzleUserRepo.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `src/services/payment/paymentController.ts`, `src/services/payment/migration/reportService.ts`, focused Jest tests, `scripts/smoke/db-audit-service-regressions.sh`, version files; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`
**Why:** Start the remaining Critical/High work in 8 waves. Wave 1 caps unbounded export/report/seller/template reads: payment export, seller discovery, active-template seller list/detail, and SHKeeper migration reporting.
**Verification:** backend `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (12 suites / 35 tests), `npm run typecheck`, `git diff --check`; frontend `git diff --check` for package bump. Pushed to Forgejo; `origin` remained unavailable and was intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@0835be9, frontend@f05b056 — DB audit marketplace batching batch

**Commits:** `0835be9` `f05b056`
**Touched:** backend `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `src/services/marketplace/categoryStore.ts`, `src/services/payment/paymentCoordinator.ts`, focused Jest tests, `scripts/smoke/db-audit-service-regressions.sh`, version files; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`
**Why:** Continue the 2026-06-06 DB audit: collapse `findAllPayments` buyer/seller N+1 into a joined read, batch template seller/category enrichment, bulk payment-coordinator rejected-seller notifications, and replace category-path ancestor walks with one recursive CTE.
**Verification:** backend `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh` (9 suites / 25 tests), `npm run typecheck`, `git diff --check`; frontend `git diff --check` for package bump. Pushed to Forgejo; `origin` remained unavailable and was intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@3ad3bbe, frontend@a78d2a9 — DB audit chat and notification bulk batch

**Commits:** `3ad3bbe` `a78d2a9`
**Touched:** backend `src/db/repositories/drizzle/DrizzleChatRepo.ts`, `src/db/schema/chat.ts`, `src/db/migrations/0020_chat_jsonb_indexes.sql`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `src/db/repositories/drizzle/DrizzleNotificationRepo.ts`, `src/services/notification/NotificationService.ts`, `src/services/notification/notificationController.ts`, focused Jest tests, `scripts/smoke/db-audit-service-regressions.sh`, version files; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`
**Why:** Continue the 2026-06-06 DB audit: push common chat filters into SQL, add chat JSONB GIN indexes, batch latest-payment lookup for buyer purchase request lists, and replace notification bulk mark/delete serial loops with capped set-based repo writes.
**Verification:** backend `npm test -- --runTestsByPath __tests__/drizzle-chat-repo.test.ts __tests__/drizzle-notification-repo-bulk.test.ts __tests__/notification-controller-bulk.test.ts __tests__/notification-service-repo.test.ts --runInBand`, `npm run typecheck`, `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh`, `git diff --check`; frontend package bump diff check. Pushed to Forgejo; `origin` remained unavailable and was intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@2a56f98, frontend@7b52dcf — DB audit service query batching batch 2

**Commits:** `2a56f98` `7b52dcf`
**Touched:** backend `src/services/notification/NotificationService.ts`, `src/services/marketplace/SellerOfferService.ts`, `src/services/marketplace/RequestTemplateService.ts`, `src/db/repositories/drizzle/DrizzleDisputeRepo.ts`, focused Jest tests, `scripts/smoke/db-audit-service-regressions.sh`, version files; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`
**Why:** Continue addressing the 2026-06-06 DB audit: bulk notification UUID normalization N+1, accepted-offer rejected-seller notification loop, template batch conversion double-fetch, and repeated dispute status count scans.
**Verification:** backend `BASE_URL=http://127.0.0.1:5001 scripts/smoke/db-audit-service-regressions.sh`, `npm run typecheck`, `git diff --check`; frontend package bump diff check. Pushed to Forgejo; `origin` remained unavailable and was intentionally skipped.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

### 2026-06-06 — backend@4aa6ccb, frontend@1b86a45 — Auth-store Postgres batching and transaction safety

**Commits:** `4aa6ccb` `1b86a45`
**Touched:** backend `src/services/auth/authStore.ts`, `__tests__/auth-store-pg-query.test.ts`, `__tests__/setup.ts`, `package.json`, `package-lock.json`; frontend `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`
**Why:** Address remaining DB audit findings for auth user hydration (`rowToUser` 1+3N queries), auth user save transaction safety, and token/passkey per-row insert loops.
**Verification:** backend `npm test -- --runTestsByPath __tests__/auth-store-pg-query.test.ts --runInBand`, `npm run typecheck`, `BASE_URL=https://dev.amn.gg bash scripts/smoke/auth-basic.sh`, `git diff --check`; frontend package bump diff check. Pushed to Forgejo; `origin` SSH port 222 was down during this session.
**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]]

---

## 2026-06-06 — DB Query & Schema Audit + Performance Fixes (v2.9.13)

**Commits:** backend `2484150`

**Audit:** Full multi-agent DB audit (9 agents, 104 findings) across all Drizzle repos, service layer, and schema files. Full report: [[DB Query & Schema Audit - 2026-06-06]].

**Fixes landed (v2.9.13):**
- Stats endpoint: 11 serial queries → 1 `GROUP BY status` (`countPurchaseRequestsByStatus`)
- Offer filter loop: per-offer `sameUser()` N×2 DB calls → resolve seller once, filter in memory
- Seller notification fan-out: in-JS `watchedCategories` filter + 100-row hard cap → SQL JSONB filter, no limit (silent miss for sellers >100 fixed)

**Top open findings from audit (prioritised for next sprint):**
- CRITICAL: `rowToUser` fires 1+3N queries on every auth path (`authStore.ts:345`)
- CRITICAL: `DrizzleChatRepo.findRows` loads entire chats table into memory (`DrizzleChatRepo.ts:544`)
- CRITICAL: `chats.participants` JSONB has no GIN index
- CRITICAL: `findAllPayments` 1+2N queries, unbounded
- CRITICAL: `notifications.user_id` is `text` not `uuid` — no FK
- CRITICAL: `disputes` FK columns are `text` — no referential integrity
- CRITICAL: `savePgUser` writes users/tokens/passkeys without a transaction
- HIGH (×8): Missing transactions on money paths (delivery code TOCTOU, double-accept race, referral reward, RN webhook, dispute create/resolve)

---

## 2026-06-06 — Full MongoDB Removal Complete

**Backend version:** 2.9.12  
**Scope:** backend runtime (all src/ files except seeds/scripts/backfill)

### What changed

- **MongoDB and Mongoose fully removed from the backend runtime.** No `from 'mongoose'` or `require('mongoose')` imports remain in any runtime source file.
- `src/models/` directory removed. `src/infrastructure/database/` (Mongoose connection) removed. `mongodb-memory-server` dev dependency removed.
- All 11 repository domains now use `DrizzleXxxRepo` exclusively. Dual-write wrappers decommissioned.
- `PaymentProvider` type and `payment_provider` pgEnum extended with `'escrow'` value.
- `PaymentBlockchain` interface extended: `blockNumber?`, `gasUsed?`, `isSimulated?`.
- `ChatService.ts`: Mongoose document methods (`.addMessage()`, `.markAsRead()`, `.pull()`) replaced with plain TypeScript array operations.
- `services/marketplace/routes.ts` (3300+ lines): All `PurchaseRequest`, `SellerOffer`, `Payment` Mongoose model calls replaced with repository pattern calls via `getMarketplaceRepo()` and `getPaymentRepo()`.
- `toIdString()` fixed to always return `string` (was `string | undefined`).
- `notificationBackfill.ts` deleted (Mongo source no longer exists).
- TypeScript compilation: **0 errors** on full `tsc --noEmit`.

### Key invariants established

- `deliveryDate` and delivery fields are nested inside `deliveryInfo`; use `updatePurchaseRequestDeliveryInfo()`.
- `PaymentDTO.amount` is a decimal string (not `{amount, currency}` object).
- User `_id` is legacy ObjectId kept as `legacy_object_id`; marketplace FKs use `user.pgId` (UUID).
- `PurchaseRequest` has no top-level `paymentId` field.
- `UpdateSellerOfferInput` and `UpdatePaymentInput` have no `updatedAt` field.

---

### 2026-06-06 — backend@ca2b1c4, frontend@aac3304 — Marketplace E2E notification smoke runner

**Commits:** `ca2b1c4` `aac3304`
**Touched:** backend `scripts/smoke/marketplace-e2e-notifications.mjs`, `scripts/smoke/marketplace-e2e-notifications.sh`, `package.json`, `package-lock.json`; frontend `package.json`, `Dockerfile`; docs `11 - Testing/Marketplace E2E Smoke Runner.md`, testing procedure pages.
**Why:** Implement the requested buyer/seller marketplace E2E runner with at least two sellers, notification assertions after every state-changing step, report generation, concurrency ramp controls, and BSC Testnet live-payment support. Rebased on newer Forgejo `main` and bumped backend/frontend to `2.9.2`.
**Verification:** backend `node --check scripts/smoke/marketplace-e2e-notifications.mjs`, `npm run typecheck`, `git diff --check`; frontend `git diff --check` and version/Dockerfile sanity; nick-doc `git diff --check`. Dev two-round `PAYMENT_MODE=record` report exposed HTTP 500 on legacy `POST /api/marketplace/payments` and missing selected-seller accepted notification. Direct-backend one-round `PAYMENT_MODE=status` reached final request status `delivered` with notification gaps recorded. Woodpecker build pending after push.
**Linked docs updated:** [[11 - Testing/Marketplace E2E Smoke Runner]], [[11 - Testing/Escrow Marketplace E2E Procedure]], [[11 - Testing/Smoke and Regression Procedure]], [[11 - Testing/Concurrency and Performance Profile]], [[11 - Testing/Testing Expansion Backlog]]

---

### 2026-06-06 — backend@31b285f, frontend@2a3e5c9 — BSC Testnet checkout UI support

**Commits:** `31b285f` `2a3e5c9`
**Touched:** frontend `src/web3/config.ts`, `src/web3/types.ts`, `src/sections/payment/checkout/rn-in-house-checkout-view.tsx`, `Dockerfile`, `package.json`; backend version files.
**Why:** After the scanner/backend tUSDT rail was corrected, the active in-house checkout still needed UI-side chain support for BSC Testnet. Wagmi now includes chain `97`, checkout labels render `BSC Testnet (97)` instead of a raw chain id, token contract rows show the exact ERC-20 address the backend supplied, and chain 97 tx/address links point at `testnet.bscscan.com`.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0`, focused `npx eslint src/web3/config.ts src/web3/types.ts src/sections/payment/checkout/rn-in-house-checkout-view.tsx`, `npm run build`, `git diff --check`; backend `npm run typecheck`, `git diff --check`. Woodpecker frontend #45 and backend #43 succeeded; live `/api/version` returns `2.8.118`; frontend root returns 200 with the Next bundle; backend/frontend/scanner containers are healthy.
**Linked docs updated:** [[04 - Flows/Payment Flow - Scanner]]

---

### 2026-06-06 — backend@99ae2db, frontend@94a99a1 — buyer delivery confirmation id-seam fix

**Commits:** `99ae2db` `94a99a1`
**Touched:** backend `src/services/marketplace/marketplaceController.ts`, backend version files; frontend `package.json`.
**Why:** Live BSC Testnet E2E rounds funded escrow and seller delivery successfully, but buyer `PATCH /api/marketplace/purchase-requests/:id/confirm-delivery` returned 403 because the route compared the buyer's session legacy ObjectId directly to the purchase request buyer id. The route now uses the same cross-store `sameUser()` helper already used by seller delivery/code gates.
**Verification:** backend `npm run typecheck`, `npm test -- --runTestsByPath __tests__/rn-in-house-checkout.test.ts __tests__/decentralized-payment-verifier.test.ts __tests__/amn-pay-adapter-intent.test.ts --runInBand`, `BASE_URL=https://dev.amn.gg bash scripts/smoke/bsc-testnet-payment-registry.sh`, `git diff --check`; frontend package version sanity check. Post-deploy retry of the two live E2E delivery confirmations pending.
**Linked docs updated:** [[04 - Flows/Delivery Confirmation Flow]]

---

### 2026-06-06 — backend@3e9a2f2, frontend@e4fa4de, scanner@1911c3a — BSC Testnet tUSDT contract corrected

**Commits:** `3e9a2f2` `e4fa4de` `e235286` `1911c3a`
**Touched:** backend `src/services/payment/requestNetwork/tokens.json`, `src/services/payment/requestNetwork/supportedChains.json`, `src/services/payment/decentralizedPaymentService.ts`, `src/services/payment/adapters/amnPayAdapter.ts`, `src/services/payment/amnScanner/amnScannerPayInService.ts`, `scripts/smoke/bsc-testnet-payment-registry.sh`, BSC Testnet payment tests, version files; frontend `package.json`; scanner `tokens.json`, `supported-chains.json`, `contracts/testnet/USDT.sol`.
**Why:** The funded dev wallet held Test USDT at `0x109F54Dab34426D5477986b0460aE5dFBA65f022`, while backend/scanner still resolved chain 97 `USDT` to `0x337610...`. Scanner `/balances/check` returned zero for symbol `USDT` until the registry was pointed at the actual tUSDT contract. The backend adapter now also passes explicit `scannerContext` into scanner intent creation so BSC Testnet pay-ins do not fall back to mainnet/global merchant-reference defaults (`undefined-c56-USDC`).
**Verification:** backend `npm test -- --runTestsByPath __tests__/rn-in-house-checkout.test.ts __tests__/decentralized-payment-verifier.test.ts __tests__/amn-pay-adapter-intent.test.ts --runInBand`, `BASE_URL=https://dev.amn.gg bash scripts/smoke/bsc-testnet-payment-registry.sh`, `git diff --check`; scanner `go test ./...`, `git diff --check`; frontend package version sanity check. Post-deploy scanner balance check pending.
**Linked docs updated:** [[04 - Flows/Payment Flow - Scanner]], [[08 - Operations/Scanner Operations]]

---

### 2026-06-06 — backend@810098f, frontend@5ccc15c, scanner@6897195 — BSC Testnet scanner rail aligned

**Commits:** `810098f` `5ccc15c` `6897195`
**Touched:** backend `src/services/payment/requestNetwork/tokens.json`, `src/services/payment/decentralizedPaymentService.ts`, `src/services/payment/safety/confirmationThresholdService.ts`, `scripts/smoke/{rn-intent.sh,bsc-testnet-payment-registry.sh}`, payment tests; frontend `package.json`, `Dockerfile`, Telegram WebApp typing; scanner `tokens.json`, `VERSION`.
**Why:** Dev BSC Testnet payments could create/scan against mismatched USDT contracts and wait for the mainnet-style 200 confirmation floor. Backend and scanner now agree on chain 97 USDT `0x337610d27c682E347C9cD60BD4b3b107C9d34dDd`, BSC Testnet verifier aliases/RPCs are first-class, and the chain 97 default confirmation floor is 5 for test flow confirmation.
**Verification:** backend `npm test -- --runTestsByPath __tests__/rn-in-house-checkout.test.ts __tests__/decentralized-payment-verifier.test.ts __tests__/confirmation-threshold-service.test.ts --runInBand`, `npm run typecheck`, `BASE_URL=http://127.0.0.1:5001 ./scripts/smoke/bsc-testnet-payment-registry.sh`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; scanner `go test ./...`. Woodpecker deploy pending at time of doc sync.
**Linked docs updated:** [[04 - Flows/Payment Flow - Scanner]], [[07 - Development/Environment Variables]]

---

### 2026-06-05 — backend@v2.8.94 — Log email verification code when no real SMTP (codes weren't reaching inboxes)

**Commits:** backend v2.8.94
**Touched:** `services/email/emailService.ts`
**Why:** After the hang fix (v2.8.93) the email-change flow worked but the code never arrived in the user's Gmail. Root cause: dev has no real SMTP configured, so `initializeTransporter` falls back to a nodemailer **Ethereal test account** (and a dummy stream on error) — mail is captured by a fake inbox, never delivered. Added an `isTestTransport` flag (set in the test-account and dummy fallbacks) and, in `sendVerificationCodeEmail`, a `console.log` of the code when in test mode: `🔑 [DEV] Email verification code for <email>: <code>`. Lets dev/test complete the verify flow from the server logs until real SMTP env (`SMTP_HOST`/`SMTP_USER`/`SMTP_PASS`) is set to deliver to real inboxes.
**Verification:** backend `npx tsc --noEmit` clean. After deploy: triggering an email change logs the 6-digit code; entering it in the Mini App completes verification. Real delivery still requires configuring SMTP env on dev (infra).

---

### 2026-06-05 — backend@v2.8.93, frontend@v2.8.109 — Email change no longer hangs; first-time "add email" label

**Commits:** backend v2.8.93, frontend v2.8.109
**Touched:** backend `services/user/userController.ts` (updateUserProfile + resendCurrentUserEmailVerification), `services/email/emailService.ts` (`sendEmail` timeout); frontend `sections/telegram/view/telegram-settings-view.tsx`, `locales/{fa,en,types}.ts` (`email_add`, `email_sending`)
**Why:** Changing the email in Mini App settings hung on «در حال ذخیره…» indefinitely. Root cause: `PUT /user/profile` (and the resend endpoint) `await`ed `emailService.sendVerificationCodeEmail`, which `await`s nodemailer `transporter.sendMail` against a slow/unreachable dev SMTP — the request blocked ~30s+ (confirmed: a live `PUT /user/profile` with a new email never responded within 30s). Fixes: (1) both controllers now persist `pendingEmail`+code synchronously and send the email **fire-and-forget** (`void …catch`), so the request returns immediately; (2) `sendEmail` wraps the SMTP send in a 15s `Promise.race` timeout as a backstop. Frontend: the send-code button's loading text was the generic «در حال ذخیره…» → now «در حال ارسال…» (`email_sending`); and on an account with no email the link now reads «افزودن ایمیل» (`email_add`) instead of «تغییر ایمیل».
**Verification:** backend `npx tsc --noEmit`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint — clean. After deploy: email change/add returns instantly and reveals the code-entry panel; email delivery is best-effort in the background (depends on dev SMTP being reachable).

---

### 2026-06-05 — backend@v2.8.92 — seedRequestTemplates: read categories via the non-vital pool

**Commits:** backend v2.8.92
**Touched:** `seeds/seedRequestTemplates.ts`
**Why:** Follow-up to the SEC-007 completion (v2.8.91). The request-templates seed looked up category ids with `getPostgresPool()` (vital pool), which 500s post-SEC-007 (`categories` is granted only to escrow_nonvital_user). Switched the seed's category query to `getNonvitalPostgresPool()` so the seed runs end-to-end (template inserts already go through the marketplace repo, which now routes request_templates to the non-vital pool).
**Verification:** backend `npx tsc --noEmit` clean. Seed now resolves categories without permission errors. (Live dev shops for seller/seller1/seller2 @amn.gg — 3 templates each, prices 0.01/0.02, 2 images each — were built via the seller API once v2.8.91 made the reads/writes work; public `…/request-templates/sellers` returns all 3 shops.)

---

### 2026-06-05 — backend@v2.8.91 — Complete SEC-007: route non-vital tables to the non-vital pool (fix 500s)

**Commits:** backend v2.8.91
**Touched:** `services/marketplace/categoryStore.ts`, `db/repositories/drizzle/DrizzleMarketplaceRepo.ts` (request_templates/categories/shop_settings/reviews queries), `DrizzleNotificationRepo.ts`, `DrizzleChatRepo.ts`, `DrizzleBlogRepo.ts`, `DrizzlePointsRepo.ts`
**Why:** SEC-007 (migration 0018) split the DB into `escrow_vital_user` (purchase_requests, payments, users, …) and `escrow_nonvital_user` (categories, request_templates, notifications, reviews, point_transactions, chats, blog_posts, shop_settings, level_configs). The migration was applied to the dev DB but the **code was never switched to the non-vital pool** for those tables — so the vital role hit `permission denied for table categories` (confirmed in logs) and **categories, request-templates, shop sellers, and notifications all 500'd for every user**. Routed each non-vital store/repo to `getNonvitalPostgresPool()` / `nonvitalDb` / `nonvitalPool`. In the mixed `DrizzleMarketplaceRepo`, only the `request_templates`/`categories`/`shop_settings`/`reviews` queries (incl. the legacy-id resolvers and the seller-ratings raw query) moved to the non-vital handle; `purchase_requests`/`seller_offers`/`payments`/`delivery_*` stay on the vital handle. `level_configs`/`shop_settings` stores were already on the non-vital pool.
**Verification:** backend `npx tsc --noEmit` clean. After deploy: `GET /marketplace/categories`, `/request-templates`, `/request-templates/sellers`, and `/notifications` return 200 instead of 500; template create/update works. Reviewed every remaining vital-`db` importer (disputes/payments/trezor/derived-destinations/user) — all are vital tables, correctly left on the vital pool. NOTE: the SEC-007 SQL (0018/0019) is NOT in the drizzle journal (stops at 0017) — it was applied out-of-band, so this code-side completion is the reliable fix (no new migration depends on an unknown runner).

---

### 2026-06-05 — backend@v2.8.90 — Login failed-attempt lockout now OFF by default (env-gated)

**Commits:** backend v2.8.90
**Touched:** `services/auth/authController.ts` (`login`)
**Why:** Rapid multi-account testing repeatedly tripped the email-based login lockout (5 fails / 15 min → HTTP 429), locking out even correct passwords. Gated the whole lockout behind `LOGIN_RATE_LIMIT_ENABLED`: when not `'true'` (the default) the login skips both the `checkLoginAttempts` 429 gate AND the per-email `incrementFailedLoginAttempt` increments (via a `trackFailedLogin()` helper), so a stale Redis lockout is ignored too. **Security note:** this disables brute-force protection on the password login by default — set `LOGIN_RATE_LIMIT_ENABLED=true` in the environment to restore it. Telegram-auth and password-reset rate limits are unchanged.
**Verification:** backend `npx tsc --noEmit` clean. After deploy: password login no longer returns 429 / locks out; setting the env flag re-enables the limiter.

---

### 2026-06-05 — backend@v2.8.89 — Gamification: extend levels from 5 to 10 tiers

**Commits:** backend v2.8.89
**Touched:** `seeds/seedLevels.ts`, `services/points/levelConfigStore.ts` (self-seed fallback)
**Why:** The level ladder only had 5 tiers (Bronze→Diamond); requested up to 10. Added زمرد/Emerald (100k), یاقوت/Ruby (200k), یاقوت کبود/Sapphire (350k), استاد/Master (600k), افسانه/Legend (1M), and closed Diamond's open-ended max at 99,999. Each new tier has progressive discount (15→30%) and full perks, a solar icon, and a colour. Updated both the startup seed (`seedLevels` runs on boot via `replaceLevelConfigs`, idempotent) and the PG self-seed fallback in `levelConfigStore` so a fresh DB also gets all 10.
**Verification:** backend `npx tsc --noEmit` clean. After deploy + boot: the points view shows the full 10-level ladder; «سطح بعدی» resolves correctly beyond Diamond.

---

### 2026-06-05 — backend@v2.8.88 — Bot launcher opens the Mini App (`/telegram`), not the web dashboard

**Commits:** backend v2.8.88
**Touched:** `services/telegram/botService.ts` (new `configureBotChatMenu`), `services/telegram/index.ts` (export), `app.ts` (call at startup)
**Why:** Opening the app from the bot loaded the full web dashboard inside Telegram (hamburger sidebar, `/shop`) instead of the Mini App shell — the BotFather menu button URL pointed at the bare web root. Added `configureBotChatMenu()` which calls `setChatMenuButton` with `web_app.url = miniAppUrl()` (`${FRONTEND_URL}/telegram/`) at startup when the Telegram feature is enabled, so the launcher always opens the Mini App route regardless of BotFather config. Fire-and-forget; a Bot API failure never blocks boot.
**Verification:** backend `npx tsc --noEmit` clean. After deploy + bot restart: the bot's menu button («Open Amanat») opens `dev.amn.gg/telegram` (Mini App shell with the bottom tab bar), not the web dashboard. NOTE: the chat-list «OPEN» button (Main Mini App) is BotFather-only — if used, its URL must also be set to `/telegram`.

---

### 2026-06-05 — frontend@v2.8.104 — Mini App: onboarding→in-shell settings, achievements (email gate + buy/request)

**Commits:** frontend only (v2.8.104)
**Touched:** `components/telegram-onboarding-sheet.tsx`, `view/telegram-mini-app-view.tsx`, `view/telegram-points-view.tsx`, `locales/{fa,en,types}.ts`
**Why:** (1) The onboarding «Account Settings» button was an `<a href={paths.dashboard.account.root}>` that left the Mini App for the web dashboard; switched it to a button that opens the in-shell settings overlay (`onOpenSettings` → `setOverlayScreen('settings')`). (2) The «تأیید ایمیل» achievement showed «کسب شد» for Telegram accounts that have `isEmailVerified=true` but no email — gated it on `user?.email && user?.isEmailVerified` (same fix as the header badge). (3) Added three buy/request achievements derived from `useTelegramMyRequests`: «اولین درخواست» (≥1 request, +50), «اولین خرید موفق» (≥1 delivered/seller_paid/completed, +100), «خریدار حرفه‌ای» (≥5 completed, +300).
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: onboarding «Account Settings» opens the in-shell settings; the email achievement only unlocks with a real verified email; the achievements list shows the new request/purchase milestones (8 total).

---

### 2026-06-05 — frontend@v2.8.103 — Mini App account: meaningful row icons

**Commits:** frontend only (v2.8.103)
**Touched:** `sections/telegram/components/telegram-icons.tsx` (new `globe`/`palette`/`settings`/`bell`/`pin`), `sections/telegram/view/telegram-account-view.tsx`
**Why:** Several account-settings rows used placeholder/wrong icons — زبان & پوسته both showed the seal-mark star, points showed a wallet, notifications a chat bubble, addresses a document. Added five solar-linear icons and remapped: زبان→globe, پوسته→palette, تنظیمات عمومی→settings(gear), امتیازات و رفرال→trophy, اعلان‌ها→bell, آدرس‌های تحویل→pin. Wallet/support rows unchanged.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean.

---

### 2026-06-05 — frontend@v2.8.102 — Mini App new-request: preferred-sellers picker (web parity)

**Commits:** frontend only (v2.8.102)
**Touched:** `sections/telegram/view/telegram-new-request-view.tsx`
**Why:** The web new-request form lets a buyer choose preferred sellers to invite, but the Mini App form only had title/description/category/budget/urgency — the `field_seller_*` locale strings existed but nothing rendered them. Added an optional multi-select chip list of sellers (from `useTelegramShops` / `getTemplateSellers`) between budget and urgency; tapping toggles a seller, and `preferredSellerIds` is included in the create payload only when non-empty.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: the «درخواست امانت جدید» form shows a «فروشنده‌های منتخب» section; selected sellers are sent with the request so they're invited to bid.
---

### 2026-06-05 — backend@v2.8.84, frontend@v2.8.101 — Notifications wired end-to-end (id-seam normalize + Mini App joins user room)

**Commits:** backend v2.8.84, frontend v2.8.101
**Touched:** backend `services/notification/NotificationService.ts`; frontend `sections/telegram/hooks/use-telegram-notifications.ts`
**Why:** No notifications reached the Mini App (in-app, real-time, or Telegram push). Two root causes. (1) **Id seam:** notifications were created with whatever id the event carried — usually a Postgres uuid (`sellerId`/`buyerId`) — but everything that *consumes* a notification keys on the user's session legacy ObjectId: the in-app fetch (`req.user.id`), the `user-<id>` socket room, and `TelegramLink.userId`. So a notification stored under a uuid was invisible to fetch, never reached the room, and `sendTelegramNotificationToUser(uuid)` found `no_link`. Fix: `NotificationService.createNotification`/`createNotificationsBulk` now normalise `userId` via `toCanonicalUserId` (uuid → `users.legacy_object_id`) before persist + real-time emit + Telegram forward, so all three line up. (2) **Mini App never joined its room:** the socket connected but `use-telegram-notifications` only registered listeners — it never emitted `join-user-room`, so the backend's targeted `new-notification` emits had no subscriber. Added a `joinUserRoom(userId)` effect that (re)joins on every connect. `selfId` is the session ObjectId, matching the socket's authed id and the now-normalised emit target.
**Verification:** backend `npx tsc --noEmit`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint — clean. After deploy: a marketplace event (offer received/accepted, payment, delivery) creates a notification that (a) shows in the Mini App bell list, (b) bumps the unread badge live, and (c) arrives as a Telegram bot message for linked users.

---

### 2026-06-05 — backend@v2.8.83, frontend@v2.8.100 — Select-offer 403 (id seam) + offer delivery-time `[object Object]`

**Commits:** backend v2.8.83, frontend v2.8.100
**Touched:** backend `services/marketplace/marketplaceController.ts` (`selectOffer`); frontend `sections/telegram/view/telegram-request-detail-view.tsx`
**Why:** Testing the new Mini App «انتخاب و پرداخت» surfaced two bugs. (1) **403 on select-offer**: `selectOffer` gated buyer ownership with `toIdString(purchaseRequest.buyerId) !== buyerId` — the session legacy ObjectId vs the PG-uuid buyerId (the recurring seam) → the real buyer got 403. Switched to `!(await sameUser(buyerId, toIdString(purchaseRequest.buyerId)))`. (2) **Delivery time rendered `[object Object]`**: the offer card assumed `offer.deliveryTime` was a number, but it can be an object (`{amount, unit}`). Now extracts `dt.amount ?? dt.value ?? offer.deliveryTimeAmount` when it isn't a plain number.
**Verification:** backend `npx tsc --noEmit`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint — clean. After deploy: buyer taps «انتخاب و پرداخت» → offer accepted (no 403) → in-shell payment opens; delivery time shows a number of days, not `[object Object]`.

---

### 2026-06-05 — frontend@v2.8.99 — Mini App buyer: see received offers + select & pay in-shell

**Commits:** frontend only (v2.8.99)
**Touched:** `sections/telegram/view/telegram-request-detail-view.tsx`, `sections/telegram/locales/{fa,en,types}.ts` (`offers_title`, `offer_select_pay`, `offer_delivery_days`, `offer_accepting`)
**Why:** When the buyer's stepper reached «انتخاب و پرداخت» (step 3, status `received_offers`) the Mini App had no way to act — the pay CTA only showed for `pending_payment`. Now, for a buyer on an offer-selection status, the detail view fetches and lists the received offers (seller, price, delivery days, description) and each has a «انتخاب و پرداخت» button → `acceptOffer(requestId, offerId)` (sets `selectedOfferId`, moves to `pending_payment`) → `refresh()` → opens the in-shell direct-transfer payment screen via `onPay`. Reused the existing offers SWR (broadened from review-only to also fire on `received_offers`/`offers_received`/`in_negotiation`).
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: buyer opens a request with an offer → sees the offer card → «انتخاب و پرداخت» → in-shell payment opens; paying advances the stepper.

---

### 2026-06-05 — frontend@v2.8.98 — Mini App seller shop: product-type filter + show controls for small shops

**Commits:** frontend only (v2.8.98)
**Touched:** `sections/telegram/view/telegram-seller-shop-view.tsx`
**Why:** The seller shop (per-seller template list) already had search + sort but (1) no filter, and (2) the controls bar was gated on `allTemplates.length > 3`, so a typical 3-template shop showed no controls at all. Added a product-type filter (chips built only for the types the seller actually sells — physical/digital/service/consultation, plus «همه») wired through `TelegramListControls`, and lowered the controls threshold to `> 1` so shops with 2–3 templates still get search/filter/sort.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: opening a seller shop with ≥2 templates shows the controls bar; the filter chips reflect the seller's product types and narrow the list.

---

### 2026-06-04 — backend@v2.8.82, frontend@v2.8.97 — Stepper advances: offer→step, seller offer detection, real-time on code-verify

**Commits:** backend v2.8.82, frontend v2.8.97
**Touched:** backend `services/marketplace/marketplaceController.ts` (`getOffersForRequest` sellerId filter, `verifyDeliveryCode` socket emit); frontend `sections/request/request-config.tsx` (buyer `received_offers`→3), `actions/marketplace.ts` (`getSellerOfferForRequest` fallback)
**Why:** Three related stepper bugs surfaced while testing an offer end-to-end. (1) **Buyer stepper stuck at "awaiting offers" (2)** after an offer arrived: `determineBuyerStep('received_offers')` returned `offersCount>0 ? 3 : 2`, but callers that don't thread `offersCount` (the Mini App) got 2. The status itself means an offer arrived on the buyer's own request, so it now returns 3 unconditionally. (2) **Seller stuck on "send offer" (1)** after submitting: `getSellerOfferForRequest` matched offers by `o.sellerId === user._id`, but `o.sellerId` is a PG uuid and `user._id` is the legacy ObjectId (the seam) → no match → `sellerOfferStatus` undefined → step 1. Backend `getOffersForRequest` now filters to the seller's own offers via `sameUser` when `?sellerId=` is passed, and the client returns `data[0]` as a fallback. (3) **Seller needed a manual refresh** to go from "awaiting buyer" (4) to "receive funds" (5): the seller's code entry (`verifyDeliveryCode`) replaced the buyer's `confirmDelivery` in v2.8.95 but never emitted a socket event, so no live update. Added the `purchase-request-update` / `status-changed` emit on successful verify (matching what `confirmDelivery` did).
**Verification:** backend `npx tsc --noEmit`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint — all clean. After deploy: buyer sees step 3 once an offer arrives; seller sees step 2 right after sending an offer; seller's stepper advances to "receive funds" without a manual refresh after entering the code.

---

### 2026-06-04 — frontend@v2.8.96 — Mini App account: don't show "email verified" when the user has no email

**Commits:** frontend only (v2.8.96)
**Touched:** `sections/telegram/view/telegram-account-view.tsx`, `sections/telegram/locales/{fa,en,types}.ts` (`email_not_set`)
**Why:** A Telegram sign-up has `isEmailVerified=true` but no email address, so the account header showed a green «ایمیل تأیید شده» badge even though the email field was empty. Gated the verified badge on `user?.email && user?.isEmailVerified`. Added a third state for the no-email case: a «ایمیل ثبت نشده» badge that opens the in-shell settings (`onOpenSettings`) to add an email. The not-verified (has-email-but-unverified) branch is unchanged.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: a Telegram user with no email sees «ایمیل ثبت نشده» (→ settings), not a false «ایمیل تأیید شده».

---

### 2026-06-04 — frontend@v2.8.95 — Delivery confirmation: code is entered by the seller, buyer's «تایید دریافت کالا» removed

**Commits:** frontend only (v2.8.95)
**Touched:** `sections/telegram/view/telegram-request-detail-view.tsx` (Mini App buyer: drop confirm button), `sections/request/components/buyer-steps/step-5-receive-goods.tsx` (web buyer: drop confirm button), `sections/request/components/seller-steps/step-4-waiting-for-confirmation.tsx` (web seller: code display → code input)
**Why:** The handover flow was wrong/duplicated: the buyer had a «تایید دریافت کالا» button (`confirmDelivery`) AND the seller was shown the expected code. Per the user, the single correct mechanism is: the **buyer gives the 6-digit code, the seller enters it** to confirm — and that 403'd anyway (`confirmDelivery` compared session ObjectId `!==` PG-uuid buyerId — same id seam). Changes: (1) Mini App buyer (`status delivery`) now only displays the code to hand over — the `confirmDelivery` button/handler/import removed. (2) Web buyer step-5: removed the «تایید دریافت کالا» button (kept the code display + an explanatory note). (3) Web seller step-4: replaced the code **display** (`getDeliveryCode`, which leaked the code to the seller and 403s post-v2.8.79) with a 6-digit **input** + «تایید تحویل کالا» calling `verifyDeliveryCode(id, code)`. Both `confirmDelivery` and `verifyDeliveryCode` reach the same `delivered` state, so no escrow-release regression. (Mini App seller already had the input from v2.8.94.)
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: buyer sees only the code (no confirm button); seller enters the buyer's code → status → delivered; no 403.

---

### 2026-06-04 — backend@v2.8.81 — Public shop settings: resolve uuid→legacy ObjectId (name/avatar were blank publicly)

**Commits:** backend v2.8.81
**Touched:** `services/marketplace/shopSettingsStore.ts` (`getSellerShopSettings` + new `resolveSellerLegacyId`)
**Why:** Found while setting shop name/avatar/cover for the three seed sellers. After PUT-ing settings, the seller's OWN `GET /marketplace/shop/settings` returned them correctly, but the PUBLIC `GET /marketplace/shop/settings/:sellerId` (what the shop page shows buyers) returned `data:null` — blank name, no avatar/cover. Root cause: the recurring Mongo↔PG id seam. `shop_settings` rows are keyed by `seller_legacy_object_id` (the seller's legacy Mongo ObjectId, which is what `req.user.id` is on the own-settings path), but the public sellers list returns each seller by their PG **uuid**, so `getSellerShopSettings(uuid)` queried `where seller_legacy_object_id = <uuid>` and matched nothing. Fix: when the direct lookup misses, resolve the incoming id via `select legacy_object_id from users where id=$1` (a 24-hex id is treated as already-legacy) and retry. Mirrors the existing reverse `resolveSellerUuid` helper.
**Verification:** backend `npx tsc --noEmit` clean. After deploy: `GET /marketplace/shop/settings/<pg-uuid>` returns the saved name/avatar/cover for all three sellers (was null). Seeded: seller/seller1/seller2 @amn.gg each have a shop name, description, avatar, cover image, social links, and 3 active products.

---

### 2026-06-04 — backend@v2.8.80 — Shop sellers list cache: invalidate global `templates:list` on per-seller change

**Commits:** backend v2.8.80
**Touched:** `services/redis/cacheService.ts` (`invalidateTemplatesCache`)
**Why:** Found while seeding shops for three sellers via API: after creating templates for `seller1@amn.gg`/`seller2@amn.gg`, the public shop endpoint (`GET /marketplace/request-templates/sellers`) still returned only the first seller. Root cause — the global sellers list is cached under Redis key `templates:list`, but `RequestTemplateService` create/update/delete call `invalidateTemplatesCache(sellerId)` which only deleted `templates:seller:<id>`, never `templates:list`. So a newly-onboarded seller didn't appear in `/shop` until the 5-minute TTL elapsed. Fix: when invalidating with a `sellerId`, also `del('templates:list')` since the aggregated list changes whenever any seller adds/edits/removes a template.
**Verification:** backend `npx tsc --noEmit` clean. After deploy: a brand-new seller's first template appears in `GET …/request-templates/sellers` (and dev.amn.gg/shop) immediately, not after TTL. (Seeded data: seller/seller1/seller2 @amn.gg each have 3 active templates — two at 0.01 USDT, one at 0.05 — with images, BSC USDT/USDC payment config.)

---

### 2026-06-04 — frontend@v2.8.94 — Telegram Mini App: seller delivery-code entry field

**Commits:** frontend only (v2.8.94); backend stays v2.8.79
**Touched:** `telegram-request-detail-view.tsx` (seller code-entry card), `locales/{fa,en,types}.ts` (`seller_code_*` strings)
**Why:** The escrow handover is two-sided: the buyer generates a 6-digit delivery code (shown in v2.8.92) and the **seller enters it** to confirm delivery. The Mini App seller had no field for this — at status `delivery` the seller only saw «منتظر تایید خریدار» with nowhere to type the code. Added a code-entry card for `role === 'seller' && status === 'delivery'`: a numeric 6-digit input (digit-filtered, monospace, centred) + «تایید تحویل کالا» button calling `verifyDeliveryCode(requestId, code)` then `refresh()` so the status advances to `delivered`. Backend `verifyDeliveryCode` already uses the `sameUser` seller gate (v2.8.77), so this composes with the id-seam fix.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. Admin verifies after deploy: buyer opens `delivery` request → sees code; seller opens same request in Mini App → enters that code → «تایید تحویل کالا» → status → delivered.

---

### 2026-06-04 — backend@v2.8.79 — Delivery code: apply `sameUser` to buyer auth gates (Mini App showed "—")

**Commits:** backend v2.8.79 (frontend stays v2.8.93)
**Touched:** `services/marketplace/marketplaceController.ts` (`generateDeliveryCode`, `getDeliveryCode`)
**Why:** In the Mini App receive-goods step the delivery code rendered as «—»: both `getDeliveryCode` and `generateDeliveryCode` returned 403. Root cause is the recurring Mongo↔PG id seam — these two handlers still used the strict `request.buyerId !== userId` comparison (session legacy ObjectId vs PG-uuid `buyerId`), unlike `verifyDeliveryCode` which was already moved to the async `sameUser` helper in v2.8.77. Replaced both buyer checks with `!(await sameUser(userId, toIdString(request.buyerId)))`. Added an explicit `!userId` unauthorized guard in `getDeliveryCode` to restore the `string` narrowing the removed `!==` previously provided (so `getDeliveryCode(id, userId)` still type-checks). The repo layer (`getDeliveryCodeForUser`) already resolves the seam via `resolveEntityId`, so once the code exists it loads without regenerating.
**Verification:** backend `npx tsc --noEmit` clean. Admin verifies after deploy: buyer opens a `delivery`-status request in the Mini App → a 6-digit delivery code appears (no «—», no 403); seller can verify it.

---

### 2026-06-04 — backend@v2.8.78, frontend@v2.8.93 — Gate shop orders on payment: abandon-cancel + 30-min TTL sweep

**Commits:** backend v2.8.78, frontend v2.8.93
**Touched:** backend `services/admin/dataCleanupService.ts` (new `cancelStaleUnpaidRequests`), `services/admin/ttlCleanupJob.ts` (schedule it every 5 min); frontend `telegram-payment-view.tsx` (abandon-cancel on unmount)
**Why:** Shop checkout creates the purchase request in `pending_payment` *before* the buyer pays — unavoidable because the direct-balance deposit address is derived per-request, so the request must exist for the AMN scanner to credit it (the payment-intent route requires `purchaseRequestId`/`sellerOfferId`/`sellerId`). That left unpaid orders lingering. Now, per the user's choice (both mechanisms, 30-min window, only cancel orders with **no** deposit): (1) **Frontend abandon-cancel** — when the buyer leaves the in-shell checkout payment screen without completing, the just-created order is set to `cancelled` via `updateRequestStatus`; two refs (`paidRef`, `depositRef`) suppress the cancel if the deposit was confirmed *or* a partial deposit was detected, so funds are never orphaned. (2) **Backend TTL sweep** — `cancelStaleUnpaidRequests` runs every 5 min, cancels `pending_payment` requests older than 30 minutes that have **no** active payment (`findActivePaymentForRequest === null`); any active/slow on-chain payment is left untouched. The sweep is the backstop for app-closed cases the frontend unmount can't catch.
**Verification:** backend `npx tsc --noEmit` clean; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. Admin verifies after deploy: shop checkout → reach payment step → press back without paying → order shows `cancelled` (not lingering pending_payment); a paid/underpaid order is NOT cancelled.

---

### 2026-06-04 — frontend@v2.8.92 — Telegram Mini App: buyer receive-goods/confirm + checkout stepper line fix

**Commits:** frontend only (v2.8.92); backend stays at v2.8.77
**Touched:** `telegram-request-detail-view.tsx` (buyer receive-goods flow), `components/telegram-request-stepper.tsx` (row width cap/centre), `locales/{fa,en,types}.ts` (delivery-code + confirm-receipt strings)
**Why:** (1) The buyer had no way to confirm receipt inside the Mini App — once the seller shipped (status `delivery`, fixed in backend v2.8.77's `sameUser` patch), the buyer was stuck at step 5 «دریافت کالا» with no action. Mirrored the web `step-5-receive-goods`: on `status === 'delivery'` for a buyer, auto-load/generate the delivery code (`getDeliveryCode` → fallback `generateDeliveryCode`) shown dashed/monospace for the buyer to hand the seller, plus a «تایید دریافت کالا» button calling `confirmDelivery(requestId)` then `refresh()` so the stepper advances to «تایید نهایی». (2) The 3-step checkout stepper stretched each cell across the full Telegram width, leaving a long connector stub past the last dot — capped the steps row at `steps.length * 96` and centred it (`margin: 0 auto`), so both the 3-step checkout and 6-step request steppers render tight, even connectors.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` clean. Admin verifies after deploy: buyer opens a `delivery`-status request → sees delivery code + «تایید دریافت کالا» → tap → request advances to تایید نهایی; checkout stepper line is clean (no stub).

---

### 2026-06-03 — scanner@ccd96e8, backend@22ae0bd, frontend@10c4292 — direct scanner balance checks and balance watches

**Commits:** scanner `ccd96e8` (tag `v0.1.8`), backend `22ae0bd` (tag `v2.8.60`), frontend `10c4292` (tag `v2.8.60`)
**Touched:** scanner: `balance.go`, `balance_watch.go`, `balance_test.go`, `api.go`, `config.go`, `intent.go`, `main.go`, `README.md`, `VERSION`; backend: `src/services/payment/adapters/amnPayAdapter.ts`, `src/routes/amnScannerWebhookRoutes.ts`, `package.json`, `package-lock.json`; frontend: `package.json` version metadata only.
**Why:** Add scanner primitives for non-smart-contract/direct-address token payments: synchronous ERC-20 balance checks, persisted balance watches with 5/10/20/40-minute cadence decay, 7-day expiry, signed `balance_changed` callbacks, and backend adapter/webhook plumbing. Backend still needs the product decision layer that turns validated balance deltas into funded payments; documented in the new direct-address PRD.
**Verification:** scanner `GOCACHE=/private/tmp/codex-go-cache go test -count=1 ./...`; backend `npm run typecheck`; frontend not built because only version metadata changed.
**Linked docs updated:** [[Scanner API]], [[Scanner Architecture]], [[Payment Flow - Scanner]], [[ScannerBalanceWatch]], [[Scanner Operations]], [[Environment Variables]], [[PRD - Direct Address Token Payments via Scanner Balance Watches]]

---

### 2026-06-03 — frontend@9bafbbb — Telegram Mini App: full in-shell shop, account tab parity, and shopping cart (v2.8.57–v2.8.59)

**Commits:** `a8ae1e3` (v2.8.57), `6dc3918` (v2.8.58), `9bafbbb` (v2.8.59) — frontend only; backend stays at v2.8.56
**Touched:** `telegram-mini-app-view.tsx` (shell nav: `openSellerId`, `overlayScreen='cart'`, BackButton dismissal chain), `telegram-shop-view.tsx` (cart badge header button, `TelegramShopRow` converted from `<a href>` to in-shell `onOpen`), `telegram-seller-shop-view.tsx` (new — seller header, active templates with budget/usage, add/remove-to-cart buttons, floating cart CTA), `telegram-cart-view.tsx` (new — qty stepper, remove, USDT total, "Continue to payment" → web checkout), `telegram-account-view.tsx` (new — profile header, preferences section, help section, sign-out with bottom-sheet confirm), `hooks/use-telegram-seller-shop.ts` (new — SWR over `getSellerWithTemplates`), `hooks/use-telegram-cart.ts` (new — localStorage `app-request-template-checkout`, custom `tg-cart-changed` event), `hooks/use-telegram-shops.ts` (new — SWR over `getTemplateSellers`), telegram locales (fa/en/types)
**Why:** Three sequential buyer-parity milestones shipped in one session. (1) v2.8.57: tapping a seller in فروشگاه previously opened the web dashboard inside the webview; the seller store now renders entirely in-shell with `TelegramSellerShopView` — seller header, active templates, budget/usage count — and template ordering hands off to the web from-template checkout where the wallet stack lives. (2) v2.8.58: the web account menu (تنظیمات عمومی، اعلان‌ها، کیف پول، آدرس‌های تحویل، Passkey) had no Mini App counterpart; all five are now accessible from the account tab — notifications open the existing in-shell overlay, the other four deep-link to web dashboard pages (labeled «در داشبورد وب باز می‌شود») because passkey and wallet require browser-context APIs unavailable in Telegram. (3) v2.8.59: phase 1 of full buyer parity — new `useTelegramCart` hook writes to the same localStorage key (`app-request-template-checkout`) that the web checkout provider reads, so the Mini App cart IS the web cart with no sync step; `TelegramCartView` adds qty controls, remove, and a "Continue to payment" link that hands off seamlessly. Remaining buyer-parity roadmap: offers view/accept on requests, delivery confirmation, payments list, points/referral, addresses CRUD, TON Connect payments (Telegram Wallet is TON-native; backend already has `tonProofService`).
**Verification:** tsc + eslint clean across all three commits. Admin verifies after deploy: (shop) فروشگاه → tap seller → in-shell shop → سفارش این قالب → web checkout; (cart) افزودن به سبد → cart badge → سبد overlay → ادامه و پرداخت → web checkout shows same items; (account) account tab → preferences rows → notifications overlay in-shell, remaining rows open web dashboard.
**Linked docs updated:** `04 - Flows/Telegram Mini App.md` (major update — navigation model, all view files, shop/cart/checkout flow, account tab, SDK surfaces, API call table), `01 - Architecture/Frontend Architecture.md` (updated — Telegram Mini App section)

---

### 2026-06-02 — backend@cf59726, frontend@a2b972b — normalize Postgres repository store modes

**Commits:** backend `cf59726` (version `2.8.37`), frontend `a2b972b` (version `2.8.37`)
**Touched:**
- Backend: `src/db/repositories/factory.ts`, `src/services/health/healthCheckService.ts`, `__tests__/repository-factory-modes.test.ts`, `__tests__/health-check-service.test.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json` version metadata only.
**Why:** Fix migration-control drift found after enabling notification PG mode in dev. Repository factory flags now treat `postgres` as an alias for `pg`, so `NOTIFICATION_STORE=postgres` actually resolves to the Drizzle notification repo. Release-hold mode is now independent from dispute mode, so `DISPUTE_STORE=postgres` no longer makes health or repository selection falsely report release holds as cut over.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/repository-factory-modes.test.ts __tests__/health-check-service.test.ts __tests__/notification-service-repo.test.ts --runInBand`; backend `npm run typecheck`; backend/frontend `git diff --check`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]]

---

### 2026-06-02 — backend@882096f, deployment@8764fdf — enable notification Postgres dev cutover

**Commits:** backend `b64995a` and `882096f` (version `2.8.36`), frontend `28ad8e6` (version `2.8.36`, already on remote), deployment `8764fdf`
**Touched:**
- Backend: `src/services/marketplace/index.ts`, `__tests__/marketplace-runtime-import-surface.test.ts`, `package.json`, `package-lock.json`
- Deployment: `docker-compose.yml`, `gatus/config.yaml`
**Why:** Continue MongoDB removal by detaching the unmounted legacy marketplace router export that still pulled in top-level Mongoose/model imports, and move notifications from PG-capable/operator-ready to PG-backed in the dev runtime. Dev Gatus now requires eight enabled Postgres stores and explicitly asserts `storeModes.notification == "postgres"`.
**Verification:** Backend `npm run typecheck`; backend `npm test -- --runTestsByPath __tests__/marketplace-runtime-import-surface.test.ts __tests__/blog-service-repo.test.ts __tests__/health-check-service.test.ts --runInBand`; backend active-surface static scan for non-type top-level `mongoose`/`models/*` imports returned no matches; backend/frontend/deployment `git diff --check`. Frontend push was not needed because remote frontend was already at `2.8.36`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]]

---

### 2026-06-02 — backend@f1ba14b, frontend@b94d8a9 — add notification Postgres backfill tooling

**Commits:** backend `f1ba14b` (version `2.8.34`), frontend `b94d8a9` (version `2.8.34`)
**Touched:**
- Backend: `src/services/notification/notificationBackfill.ts`, `src/scripts/backfillNotificationPostgres.ts`, `src/db/backfill/backfill-notifications.ts`, `src/db/backfill/run-backfill.ts`, `scripts/smoke/notifications-postgres.sh`, `__tests__/notification-backfill.test.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json` version metadata only.
**Why:** Continue the MongoDB removal by making the already repo-backed notification domain operator-ready for Postgres cutover. Existing notification runtime paths use `getNotificationRepo()`; this adds Mongo→Postgres backfill, ordered-runner inclusion, dry-run support, and a focused PG smoke for notification create/list/read/delete behavior.
**Verification:** Backend `npm run typecheck`; backend `npm test -- --runTestsByPath __tests__/notification-service-repo.test.ts __tests__/notification-backfill.test.ts __tests__/health-check-service.test.ts --runInBand`; backend `bash scripts/smoke/marketplace-core-postgres-backfill.sh` (static checks passed; optional live dry-run skipped because migration DSNs were not set); backend active-surface static scan for non-type top-level `mongoose`/`models/*` imports returned no matches; backend/frontend `git diff --check`. The new `scripts/smoke/notifications-postgres.sh` was added but not run locally because no `PG_URL`/`DATABASE_URL`/`POSTGRES_URL` is configured in the backend env files.
**Linked docs updated:** [[Postgres Runtime Cutover Status]]

---

### 2026-06-02 — backend@10de752, frontend@3dfbac2 — defer legacy Mongo runtime imports

**Commits:** backend `10de752` (version `2.8.33`), frontend `3dfbac2` (version `2.8.33`)
**Touched:**
- Backend: `src/infrastructure/database/connection.ts`, `src/services/health/healthCheckService.ts`, `src/services/admin/dataCleanupService.ts`, `src/services/payment/migration/reportService.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json` version metadata only.
**Why:** Continue removing MongoDB as a runtime dependency by deferring the remaining active startup/health/admin/report Mongoose/model loads. Legacy Mongo connection, health ping, admin cleanup, and SHKeeper migration report paths still work, but now load Mongoose/models only when those legacy actions actually run.
**Verification:** Backend `npm run typecheck`; backend `npm test -- --runTestsByPath __tests__/health-check-service.test.ts --runInBand`; backend `npm test -- --runTestsByPath __tests__/payment-migration.service.test.ts --runInBand`; backend static scan for non-type top-level `mongoose`/`models/*` imports in active app/routes/services/infrastructure returned no matches; backend/frontend `git diff --check`; `BASE_URL=https://dev.amn.gg bash scripts/smoke/backend-health.sh` passed against dev before push.
**Linked docs updated:** [[Postgres Runtime Cutover Status]]

---

### 2026-06-02 — backend@134d155, frontend@18af5dd — lazy-load PG-capable store Mongo fallbacks

**Commits:** backend `134d155` (version `2.8.32`), frontend `18af5dd` (version `2.8.32`)
**Touched:**
- Backend: `src/services/config/configStore.ts`, `src/services/marketplace/reviewStore.ts`, `src/services/marketplace/shopSettingsStore.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json` version metadata only.
**Why:** Continue removing MongoDB as a runtime dependency by making the already PG-capable config, review, and shop-settings stores avoid top-level Mongoose/model imports. Mongo fallback/backfill paths still lazy-load the legacy models when they are actually used, preserving rollback/mirror behavior.
**Verification:** Backend `npm run typecheck`; backend `bash scripts/smoke/marketplace-core-postgres-backfill.sh` (static Jest checks passed; optional live dry-run skipped because `MIGRATION_MONGO_URL`/`MIGRATION_PG_URL` were not set); backend `npx jest __tests__/health-check-service.test.ts __tests__/telegram-service.test.ts __tests__/telegram-auth.test.ts --runInBand` rerun outside the sandbox after `MongoMemoryServer` hit local bind restrictions (16/16 passed); backend/frontend `git diff --check`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]]

---

### 2026-06-01 — backend@2c5c3c7, frontend@775a73b — route funds ledger through payment repo seam

**Commits:** backend `2c5c3c7`, frontend `775a73b` (backend `2.8.20`, frontend `2.8.20`)
**Touched:**
- Backend: `src/services/payment/ledger/fundsLedgerService.ts`, `src/db/repositories/factory.ts`, `src/db/repositories/drizzle/DrizzlePaymentRepo.ts`, `src/db/repositories/mongo/MongoPaymentRepo.ts`, `__tests__/payment-ledger.service.test.ts`, `__tests__/mongo-payment-repo.test.ts`, `scripts/smoke/funds-ledger-repo.sh`, `package.json`, `package-lock.json`
- Frontend: `package.json`, `Dockerfile` version metadata only.
**Why:** Start replacing the remaining PG-capable repository stores with a low-risk money-core slice. Funds ledger appends and balance reads now go through `getPaymentRepo()`, so `REPO_PAYMENT=mongo|dual|pg` can control the ledger path. The repo factory now lazy-loads PG/dual implementations so importing it in Mongo mode no longer requires `PG_URL`. Mongo/Drizzle payment repo stats were aligned with live behavior (`buyerId`, nested `amount.amount`, and `completed` counted as successful), and Drizzle ledger balance reads now match external/string refs as well as UUID refs.
**Verification:** Backend `git pull --rebase --autostash` (already up to date); backend `scripts/smoke/funds-ledger-repo.sh`; backend `npm test -- --runTestsByPath __tests__/payment-release-refund-orchestration.test.ts __tests__/money-safety.test.ts --runInBand` (`money-safety` skipped by its own env guard); backend `npm run typecheck -- --pretty false`; backend `npm run build:server`; frontend `git pull --rebase --autostash` (already up to date); frontend `npx tsc --noEmit --project tsconfig.json`; frontend `npm run build` (passed with the existing non-fatal SSR `getPosts` fetch refusal during static page generation); backend/frontend `git diff --check`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]], [[MongoDB to PostgreSQL Migration Plan (Drizzle)]], [[Payment]]

---

### 2026-06-01 — deployment@38cb75b — default PG-capable stores to Postgres in dev

**Commits:** deployment `38cb75b`
**Touched:**
- Deployment: `docker-compose.yml`, `gatus/config.yaml`
**Why:** Move the existing PG-capable runtime stores from opt-in env settings to the default dev runtime: `AUTH_STORE`, `CONFIG_STORE`, `ADDRESS_STORE`, `CATEGORY_STORE`, `LEVEL_CONFIG_STORE`, `SHOP_SETTINGS_STORE`, and `REVIEW_STORE` now default to `postgres` in compose. Gatus now verifies `checks.postgres.enabledStoreCount >= 7` and asserts each of those store modes is `postgres`, so monitoring catches partial cutover drift.
**Verification:** Deployment YAML parse via `ruby -e 'require "yaml"; YAML.load_file("docker-compose.yml"); YAML.load_file("gatus/config.yaml"); puts "yaml ok"'`; deployment `docker compose config --quiet`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]], [[Monitoring]], [[Gatus Monitoring - Proposed Config]]

---

### 2026-06-01 — backend@c5db471, frontend@f424a03 — add RequestTemplate Postgres backfill surface

**Commits:** backend `c5db471`, frontend `f424a03` (backend `2.8.19`, frontend `2.8.19`)
**Touched:**
- Backend: `src/db/schema/requestTemplate.ts`, `src/db/backfill/backfill-requestTemplates.ts`, `src/db/backfill/run-backfill.ts`, `src/db/backfill/_idMap.ts`, `src/db/schema/purchaseRequest.ts`, `src/db/migrations/0010_request_templates.sql`, `src/db/migrations/meta/_journal.json`, `__tests__/marketplace-core-backfill.test.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json`, `Dockerfile` version metadata only.
**Why:** Start the 2–8 migration pass with the lowest-risk marketplace-core gap. `RequestTemplate` now has a PG schema, id-map collection entry, ordered backfill step, and marketplace-core runner coverage. The same migration also adds the missing unique index for `purchase_request_specifications (purchase_request_id, key)`, matching the existing backfill upsert target.
**Verification:** Backend `./scripts/smoke/marketplace-core-postgres-backfill.sh` (static checks passed; optional live dry-run skipped because `MIGRATION_MONGO_URL`/`MIGRATION_PG_URL` were not set); backend `npm test -- --runTestsByPath __tests__/marketplace-core-backfill.test.ts`; backend `npm run typecheck -- --pretty false`; backend `npm run build:server`; frontend `npx tsc --noEmit --project tsconfig.json`; frontend `npm run build` (passed with the existing non-fatal SSR `getPosts` fetch refusal during static page generation); backend/frontend `git diff --check`.
**Linked docs updated:** [[RequestTemplate]], [[Postgres Runtime Cutover Status]], [[MongoDB to PostgreSQL Migration Plan (Drizzle)]], [[MongoDB to PostgreSQL Migration Guide]]

---

### 2026-06-01 — backend@1543b53, frontend@457de07 — enforce unique active categories

**Commits:** backend `1543b53`, frontend `457de07` (backend `2.8.17`, frontend `2.8.17`)
**Touched:**
- Backend: `src/services/marketplace/categoryStore.ts`, `src/services/marketplace/CategoryService.ts`, `src/db/schema/category.ts`, `src/db/migrations/0009_unique_active_categories.sql`, `src/db/migrations/meta/_journal.json`, `__tests__/category-store.test.ts`, `scripts/smoke/categories-postgres-unique.sh`, `scripts/smoke/reference-stores-postgres.sh`, `package.json`, `package-lock.json`
- Frontend: `package.json` version metadata only.
**Why:** Category seed/backfill reruns could leave multiple active rows with the same visible label, which surfaced in the category dropdown as repeated Persian names. The PG category store now deactivates duplicate active rows before adding a normalized active-name unique index, repoints existing category references to the kept row, catches duplicate inserts as idempotent creates, and dedupes cached/list responses so stale rows cannot leak into the UI.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/category-store.test.ts __tests__/postgres-client.test.ts`; backend `npm run typecheck`; backend `npm run build:server`; backend `PG_URL=postgresql://escrow:throwaway@127.0.0.1:5434/escrow_migration_test ./scripts/smoke/categories-postgres-unique.sh`; frontend `npx tsc --noEmit --project tsconfig.json`; frontend `npm run build` (passed with the existing non-fatal SSR `getPosts` fetch refusal during static page generation).
**Linked docs updated:** [[Category]], [[Postgres Runtime Cutover Status]], [[MongoDB to PostgreSQL Migration Plan (Drizzle)]], [[MongoDB to PostgreSQL Migration Guide]]

---

### 2026-06-01 — backend@6df113d, frontend@0f1db64 — harden marketplace-core Postgres backfill

**Commits:** backend `6df113d`, frontend `0f1db64` (backend `2.8.13`, frontend `2.8.13`)
**Touched:**
- Backend: `src/db/backfill/backfill-purchaseRequests.ts`, `src/db/backfill/run-backfill.ts`, `__tests__/marketplace-core-backfill.test.ts`, `scripts/smoke/marketplace-core-postgres-backfill.sh`, `package.json`, `package-lock.json`
- Frontend: `src/components/hook-form/rhf-select.tsx`, `tsconfig.json`, `yarn.lock`, `package.json`
**Why:** The next Postgres migration slice is marketplace core. The existing PurchaseRequest/SellerOffer backfill path had real cutover blockers: the `purchase_requests` insert omitted `updated_at`, preferred sellers wrote the wrong junction column, and selected offers were remapped before seller offers existed. The runner now exposes a `marketplaceCore` group and a post-offer selected-offer remap step. Frontend typecheck was restored after the pulled request-template changes by installing the declared `rehype-sanitize` lock entry, allowing `displayEmpty` through `RHFSelect`, and silencing TS6's `baseUrl` deprecation gate.
**Verification:** Backend `./scripts/smoke/marketplace-core-postgres-backfill.sh` (static checks passed; optional live dry-run skipped because `MIGRATION_MONGO_URL`/`MIGRATION_PG_URL` were not set); backend `npm run typecheck`; backend `npm run build:server`; frontend `npx tsc --noEmit --project tsconfig.json`; frontend `npm run build` (passed with a non-fatal SSR `getPosts` fetch refusal during static page generation); backend/frontend `git diff --check`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]], [[MongoDB to PostgreSQL Migration Plan (Drizzle)]], [[MongoDB to PostgreSQL Migration Guide]]

---

### 2026-06-01 — backend@ea43862, frontend@b4ea7c9 — expose Postgres store modes in health

**Commits:** backend `ea43862`, frontend `b4ea7c9` (backend `2.8.11`, frontend `2.8.11`)
**Touched:**
- Backend: `src/infrastructure/postgres/client.ts`, `src/services/health/healthCheckService.ts`, `__tests__/postgres-client.test.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json`, `Dockerfile` version metadata only.
**Why:** Phase 0 of the Postgres cutover needed runtime visibility before moving another domain. `/api/health` now reports `checks.postgres.storeModes`, `enabledStores`, and `enabledStoreCount` while preserving the existing `configured`/`required`/status semantics. This lets Gatus and operators verify which opt-in stores are actually PG-backed in dev. Backend was rebased on Mojtaba's `2be91d2` authStore fix before push.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/postgres-client.test.ts`; backend `npm run typecheck -- --pretty false`; backend `npm run build`; backend focused health smoke against local Mongo/Redis containers and `escrow-pgmig-test` Postgres asserted all seven enabled PG stores in `checks.postgres`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; backend/frontend `git diff --check`.
**Linked docs updated:** [[Monitoring]], [[Postgres Runtime Cutover Status]]

---

### 2026-06-01 — backend@1757f1e, frontend@600dd0d, deployment@6db02b0 — Postgres runtime cutover stores and health monitoring

**Commits:** backend `1757f1e`, frontend `600dd0d`, deployment `6db02b0` (backend `2.8.9`, frontend `2.8.9`)
**Touched:**
- Backend: `src/infrastructure/postgres/client.ts`, `src/infrastructure/database/connection.ts`, auth/config/address/category/level/shop/review store facades, backfill scripts, smoke scripts, `/api/health`, `package.json`, `package-lock.json`
- Frontend: `package.json`, `Dockerfile` version metadata only.
- Deployment: `docker-compose.yml` Postgres service/store env wiring, `gatus/config.yaml` Postgres health assertions.
**Why:** Continue the MongoDB-to-Postgres runtime cutover by moving auth-owned users and Telegram auth records, confirmation thresholds, user addresses, and the first reference/marketplace domains behind opt-in Postgres store flags while keeping Mongo as the default and rollback mirror. Monitoring now treats Postgres as a required health dependency when any PG-backed store is enabled.
**Verification:** Backend `npm run typecheck -- --pretty false`; backend `npm run build`; backend `PG_URL=postgres://escrow:throwaway@127.0.0.1:5434/escrow_migration_test MONGODB_URI=mongodb://127.0.0.1:27018/reference-smoke DB_NAME=reference-smoke scripts/smoke/reference-stores-postgres.sh`; backend focused health smoke asserted `checks.postgres.ok/configured/required`; local backend `BASE_URL=http://127.0.0.1:5011 scripts/smoke/auth-basic.sh`; local backend `BASE_URL=http://127.0.0.1:5011 scripts/smoke/confirmation-thresholds.sh`; local backend `BASE_URL=http://127.0.0.1:5011 JWT_SECRET=test-secret scripts/smoke/addresses-basic.sh`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; deployment `ruby -e 'require "yaml"; YAML.load_file("gatus/config.yaml")'`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]], [[MongoDB to PostgreSQL Migration Plan (Drizzle)]], [[API Overview]], [[Monitoring]], [[Gatus Monitoring - Proposed Config]]

---

### 2026-05-31 — backend@8e03360, frontend@228eed2 — keep auth and health checks resilient under load

**Commits:** backend `8e03360`, frontend `228eed2` (backend `2.6.84`, frontend `2.7.24`)
**Touched:**
- Backend: `src/app.ts`, `src/services/health/healthCheckService.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json`, `package-lock.json` version bump only.
**Why:** A dev performance run consumed the global 100/15m limiter and blocked `/api/auth/login`; repeated `/api/health` calls also drove the external Request Network reachability probe into `429`, making Gatus report `status: degraded` even though Mongo/Redis/app were healthy. Auth routes now bypass the global limiter and rely on the auth-specific limiter, and the RN health subcheck is cached and treats non-5xx HTTP responses as upstream reachable.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/health-check.test.ts`; backend `npm run typecheck`; backend `git diff --check`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; frontend `git diff --check`. Dev login was manually verified after resetting the backend limiter state.
**Linked docs updated:** [[Monitoring]], [[Gatus Monitoring - Proposed Config]]

---

### 2026-05-31 — backend@cbc32dc, frontend@08e8da9 — seller-owned template delivery and payment rails

**Commits:** backend `cbc32dc`, frontend `08e8da9` (backend `2.6.83`, frontend `2.7.23`)
**Touched:**
- Backend: `src/models/RequestTemplate.ts`, `src/services/marketplace/RequestTemplateService.ts`, `src/services/marketplace/requestTemplateRoutes.ts`, `src/services/marketplace/requestTemplateController.ts`, `__tests__/marketplace-request-budget-validation.test.ts`, `scripts/smoke/marketplace-request-budget.sh`, `package.json`, `package-lock.json`
- Frontend: `src/sections/request-template/request-template-checkout-billing-address.tsx`, `src/sections/request-template/request-template-checkout-payment.tsx`, `src/sections/request-template/request-template-new-edit-form.tsx`, `src/sections/request-template/request-template-details-summary.tsx`, `src/sections/request-template/view/seller-shop-view.tsx`, `src/sections/request-template/view/public-seller-shop-view.tsx`, `src/web3/components/multi-seller-provider-payment.tsx`, `src/actions/request-template.ts`, `src/types/request-template.ts`, `src/sections/request-template/context/types.ts`, `package.json`, `package-lock.json`
**Why:** Template checkout let buyers choose delivery even though fulfillment is a seller decision, and payment creation could fail when a seller/template had no usable network/token allowlist. Sellers now choose physical vs online delivery on the template, new templates require at least one chain/token rail, checkout asks buyers only for the needed address/email details, and template payment intents resolve rails with the real `templateId`.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/marketplace-request-budget-validation.test.ts`; backend `npm run typecheck`; backend `git diff --check`; backend `BASE_URL=http://localhost:5001 ./scripts/smoke/marketplace-request-budget.sh` skipped with exit 77 because `ACCESS_TOKEN` and `CATEGORY_ID` were not set; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; frontend `git diff --check`. No browser verification was possible because the Browser tool was unavailable in this session.
**Linked docs updated:** [[RequestTemplate]], [[PurchaseRequest]], [[ShopSettings]], [[Marketplace API]], [[Payment API]], [[Purchase Request Flow]], [[Seller Guide]]

---

### 2026-05-31 — backend@a4d72df, frontend@07db9b0, scanner@ca62e7a — cap confirmations at per-chain acceptance floors

**Commits:** backend `a4d72df`, frontend `07db9b0`, scanner `ca62e7a` (backend `2.6.82`, frontend `2.7.22`, scanner `0.1.7`)
**Touched:**
- Backend: `src/services/payment/safety/confirmationThresholdService.ts`, `src/routes/amnScannerWebhookRoutes.ts`, `src/services/payment/requestNetwork/requestNetworkRoutes.ts`, `src/services/payment/adapters/amnPayAdapter.ts`, `src/services/admin/confirmationThresholdRoutes.ts`, `src/services/payment/requestNetwork/supportedChains.json`, `__tests__/confirmation-threshold-service.test.ts`, `package.json`, `package-lock.json`
- Frontend: `src/sections/payment/payment-table-row.tsx`, `src/sections/payment/view/payment-details-view.tsx`, `package.json`, `package-lock.json`
- Scanner: `supported-chains.json`, `webhook.go`, `chain.go`, `tron_chain.go`, `ton_chain.go`, `main.go`, `README.md`, `VERSION`, `webhook_test.go`
**Why:** Confirmation depth should be a chain-specific acceptance floor, roughly a ten-minute safety window, not an endlessly increasing block counter. The backend now clamps runtime settings below the chain floor, caps stored settled confirmations at the effective threshold, sends the same threshold to AMN scanner intents, and the scanner includes the capped accepted count in webhooks. Frontend payment views show settled confirmation counts with a `+` suffix.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/confirmation-threshold-service.test.ts __tests__/transaction-safety-provider.test.ts`; backend `npm run typecheck`; backend `BASE_URL=https://dev.amn.gg ./scripts/smoke/rn-webhook.sh`; backend `git diff --check`; scanner `go test ./...`; scanner `git diff --check`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; frontend `git diff --check`.
**Linked docs updated:** [[Payment]], [[Payment API]], [[Scanner API]], [[Payment Flow - Scanner]], [[ScannerIntent]], [[Scanner Architecture]], [[Scanner Operations]], [[Environment Variables]]

---

### 2026-05-31 — backend@896f17f, frontend@fd8e797 — persist webhook confirmations for scanner payments

**Commits:** backend `896f17f`, frontend `fd8e797` (backend `2.6.81`, frontend `2.7.21`)
**Touched:**
- Backend: `src/routes/amnScannerWebhookRoutes.ts`, `src/services/payment/requestNetwork/requestNetworkRoutes.ts`, `src/services/payment/safety/transactionSafetyProvider.ts`, `__tests__/transaction-safety-provider.test.ts`, `package.json`, `package-lock.json`
- Frontend: `package.json`, `package-lock.json` version bump only.
**Why:** `dev.amn.gg` showed a confirmed AMN scanner payment with `blockchain.confirmations = 0`. Scanner webhooks report `status: "confirmed"`, but transaction safety only evaluated `"completed"` statuses, so no verifier evidence was produced. The webhook routes now persist confirmations from safety evidence, scanner/RN payloads, or the chain confirmation threshold fallback when the provider already says confirmed/completed.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/transaction-safety-provider.test.ts`; backend `npm run typecheck`; backend `git diff --check`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; frontend `git diff --check`. Could not inspect the actual `dev.amn.gg` payment row because SSH to `193.180.213.68` rejected the available key.
**Linked docs updated:** [[Payment]], [[Payment API]], [[Payment Flow - Scanner]]

---

### 2026-05-31 — backend@cab0719, frontend@ec2f765 — align request budget validation after Postgres migration

**Commits:** backend `cab0719`, frontend `ec2f765` (backend `2.6.80`, frontend `2.7.20`)
**Touched:**
- Backend: `src/shared/constants/marketplace.ts`, `src/models/PurchaseRequest.ts`, `src/models/RequestTemplate.ts`, `src/services/marketplace/requestTemplateRoutes.ts`, `src/services/marketplace/PurchaseRequestService.ts`, `src/db/schema/purchaseRequest.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `__tests__/marketplace-request-budget-validation.test.ts`, `scripts/smoke/marketplace-request-budget.sh`
- Frontend: `package.json`, `package-lock.json` version bump only.
**Why:** Product/template creation could return `400` when the UI sent `urgency: "urgent"`, and template-to-purchase conversion could later fail when a template budget used `USD` / `EUR` / `IRR` while `PurchaseRequest` only accepted `USDT` / `USDC`. Runtime Mongoose validation, request-template route validation, and the PG `budget_currency` enum now share `USD`, `EUR`, `IRR`, `USDT`, `USDC`; urgency validation includes `urgent`.
**Verification:** Backend `npm test -- --runTestsByPath __tests__/marketplace-request-budget-validation.test.ts`; backend `npm run typecheck`; backend `git diff --check`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0`; smoke helper added at `scripts/smoke/marketplace-request-budget.sh` but not run against dev because no `ACCESS_TOKEN` / `CATEGORY_ID` were available in this session.
**Linked docs updated:** [[PurchaseRequest]], [[RequestTemplate]], [[Marketplace API]], [[MongoDB to PostgreSQL Migration Guide]], [[MongoDB to PostgreSQL Migration Plan (Drizzle)]], [[Postgres Runtime Cutover Status]], [[Data Model Overview]], [[Payment]]

---

### 2026-05-31 — nick-doc@local — clarify Postgres runtime cutover status

**Commits:** docs-only sync after backend `3a50dc4` (`integrate-main-into-development`, backend `2.6.79`)
**Touched:**
- Added [[Postgres Runtime Cutover Status]].
- Updated overview, architecture, data-model, payment API, env, database-ops, migration-plan, migration-guide, and oracle checkout docs.
**Why:** Correct the overbroad assumption that the promoted Postgres branch means normal runtime traffic is already PG-backed. The code has Postgres infrastructure, migrations, repository classes, and conditional `payment_quotes`, but live services still call Mongoose directly and Mongo remains authoritative until repository wiring/backfill/shadow-read cutover is completed.
**Verification:** Code audit via `rg` on backend `origin/integrate-main-into-development@3a50dc4`: no runtime `createRepositories()` / `get*Repo()` use outside `src/db/repositories/factory.ts`; only normal service import of `src/db/client.ts` is `services/payment/priceOracle/quoteRepo.ts`.
**Linked docs updated:** [[Postgres Runtime Cutover Status]], [[System Overview]], [[Tech Stack]], [[System Architecture]], [[Backend Architecture]], [[Database Strategy - Mongo vs Postgres Assessment]], [[Data Model Overview]], [[Payment]], [[Payment API]], [[Environment Variables]], [[Database Operations]], [[MongoDB to PostgreSQL Migration Guide]], [[MongoDB to PostgreSQL Migration Plan (Drizzle)]], [[Oracle Pricing & Stablecoin Depeg Protection]], [[Oracle Depeg Checkout — UI Implementation Guide]]

---

### 2026-05-31 — backend@3a50dc4 — promote Postgres integration branch with oracle/depeg + gasless backports

**Commits:** backend `11bfd02` `74d73c5` `1730c4d` `148c803` `8aa4473` `a5e4da2` `3a50dc4` (backend `2.6.76` → `2.6.79`)
**Touched:**
- Branches: preserved old `integrate-main-into-development` as `integrate-main-into-development-old`; promoted `feat/pg-money-core-migration` to `integrate-main-into-development`.
- Payment routing: `requestNetworkRoutes.ts`, `requestNetworkPayInService.ts`, `amnScannerPayInService.ts`, `permitRelay.ts`, `amnPayAdapter.ts`
- Oracle/depeg: `priceOracle/*`, `paymentQuote.ts`, migration `0008_giant_winter_soldier.sql`, `Payment.ts`, `SellerOffer.ts`
- Tests/config: `oracle-depeg-protection.test.ts`, `request-network-payin.test.ts`, `.env.example`, `package.json`, `package-lock.json`
**Why:** Combine the old integration branch's AMN scanner rail-switch fix and partial gasless permit work with the Postgres money-core branch and oracle/depeg quote engine. The final fix resolves the PG payment id through `payments.legacy_object_id` / `id_map` before writing `payment_quotes`, records `pg_dualwrite_gaps` if PG is behind, and keeps the Mongo quote mirror coherent during dual-write.
**Verification:** `npm run typecheck -- --pretty false`; `npm test -- --runInBand __tests__/oracle-depeg-protection.test.ts`; `npm test -- --runInBand __tests__/request-network-payin.test.ts __tests__/request-network-adapter.test.ts __tests__/request-network-webhook.test.ts __tests__/sweep-service.test.ts`. The PG decimal integration cases in the oracle suite skipped because no local `PG_URL`/`MIGRATION_PG_URL` was configured.
**Linked docs updated:** [[Payment]], [[SellerOffer]], [[Payment API]], [[Environment Variables]], [[Oracle Pricing & Stablecoin Depeg Protection]], [[PRD - Gasless Buyer Payments (Roadmap)]]

---

### 2026-05-30 — frontend@9013b70, c77cf82, 8add494 — staged node-package upgrade + TS6 test fix + lint sweep

**Commits:** `8add494` `c77cf82` `9013b70`
**Touched:**
- Deps (`package.json`, `yarn.lock`): TypeScript 5→6, Jest 29→30, Tiptap 2→3 (all 11 sub-packages), i18next 25→26, react-i18next 15→17, @types/node 22→25, @types/jest 29→30, react-dropzone 14→15, react-apexcharts 1→2, mui-one-time-password-input 5→7, React 19.1→19.2, MUI 7.1→7.3 (in-range), zod 4.0→4.4. Constraints bumped to tested floors (`@mui/material ^7.3.11`, `wagmi ^2.19.5`, etc.). Version bumped 2.7.9 → 2.7.10.
- Code fixes for new types: `src/theme/with-settings/update-core.ts` (cast `currentScheme` via `Record<string,unknown>` after MUI 7.3 tightened `ColorSystemOptions`), `src/components/editor/components/code-highlight-block.tsx` (cast `NodeViewContent as='code'` → `'code' as 'div'` for Tiptap 3 stricter prop typing).
- Test infra: `jest.config.js` (point ts-jest at `tsconfig.test.json` explicitly, ignore TS5101/TS5011), `tsconfig.test.json` (add `rootDir: "."` and `ignoreDeprecations: "6.0"`).
- Security hygiene: `.env.local` + `.env.production` removed from tracking; added to `.gitignore`. Existing values still in git history — rotate any leaked credentials.
- Lint sweep: `yarn lint:fix` applied across 64 files in `src/` — mostly `perfectionist/sort-imports` reorders and unused-imports removals.
- Docs: `AGENTS.md` gained an "Enforced project conventions" section covering Prettier, ESLint, TypeScript, and the centralized `src/theme/` structure. `CLAUDE.md` is now a symlink → `AGENTS.md` so Claude Code reads the same rules.
- Tooling: `scripts/upgrade-packages.sh` (reusable staged-upgrade runner with snapshot + auto-rollback) and `scripts/UPGRADE-PLAN.md` (strategy + per-stage rationale) added. `.upgrade-backups/` added to `.gitignore`.

**Why:** Many runtime / dev dependencies were 3–7 minors behind; the audit was triggered by a request to "update all node packages without breaking the build." Did it as eight staged groups (in-range → @types → ESLint → Jest → Tiptap → i18next → misc → TypeScript), each gated by `yarn build`. Three stages were pulled back: ESLint 10 (eslint-plugin-react@7 incompatible with new context API), wagmi 3 (@coinbase/wallet-sdk declares `window.ethereum: unknown`, breaks type union with viem), MUI 7→9 (AGENTS.md pins to v7).

**Verification:** `yarn build` passes after every stage (34–44s, all 57 routes). `yarn test` recovered from "45 suites fail, 0 tests run" (TS6 blocker) to 530 tests pass, 18 unrelated mock failures. `yarn lint` went 204 → 21 problems (the remaining 5 errors are pre-existing: 2× `@ts-nocheck`, 3× `no-bitwise`). Dev server (`/`, `/auth/jwt/sign-in`, `/post`, `/shop`, `/dashboard`, `/telegram`) all return 200. Manual smoke test of the Tiptap editor + wagmi connect flow is still recommended before promoting to prod.

**Linked docs updated:** none yet — `07 - Development/` should grow a "Node dependency upgrade runbook" pointing at `frontend/scripts/UPGRADE-PLAN.md` and the staged-rollback pattern. Also worth promoting the new AGENTS.md conventions section to `07 - Development/Coding Standards.md`.

---

### 2026-05-29 — backend@cdc8df1 — AMN Pay Scanner integration (retire Request Network)

**Commits:** backend `cdc8df1`, scanner `8fee27e`
**Touched:**
- Backend: `src/services/payment/adapters/amnPayAdapter.ts`, `src/routes/amnScannerWebhookRoutes.ts`, `src/services/payment/adapters/types.ts`, `src/services/payment/providerConfig.ts`, `src/app.ts`, `.env.example`, `docker-compose.dev.yml`, `docker-compose.production.yml`
- Scanner (new repo): `scanner/*.go`, `Dockerfile`, `supported-chains.json`
- Frontend: `src/actions/network-registry.ts`, `src/sections/admin/networks/networks-list-view.tsx`
**Why:** Implement AMN Pay Scanner per `PRD - Retire Request Network — In-House Payment Scanner.md`. Standalone Go microservice scans `ERC20FeeProxy` `TransferWithReferenceAndFee` events directly, eliminating RN API dependency. Supports any destination address (derived HD wallets enabled). Parallel run: RN stays active for existing payments; new payments route to scanner when `AMN_SCANNER_URL` is configured.
**Verification:** `tsc --noEmit` clean. Scanner binary builds (`go build`). Go tests pass (3/3). Frontend networks page renders scanner lag column.
**Linked docs updated:** [[07 - Development/Environment Variables]], [[PRD - Retire Request Network — In-House Payment Scanner]]

---

### 2026-05-29 — backend@7688f57 — Sweep gas strategy: PermitPull + GasTopUp signers

**Commits:** backend `7688f57`
**Touched:**
- Backend: `src/services/payment/wallets/sweepService.ts`, `__tests__/sweep-service.test.ts`, `.env.example`
**Why:** Implement hybrid two-signer sweep strategy per `PRD - Sweep Gas Strategy - Permit Pull vs Gas Top-Up.md`. `PermitPullSweepSigner` uses EIP-2612 permit for non-BSC chains (ETH, Arbitrum, Polygon, Base) so derived addresses never need native gas. `GasTopUpSweepSigner` handles BSC by topping up BNB from a master wallet before the derived address calls `transfer()`. `getSweepSigner(chainId, tokenSymbol)` auto-selects the correct signer. Static `PERMIT_CAPABLE_TOKENS` map seeded from on-chain audit 2026-05-29.
**Verification:** `tsc --noEmit` clean. `npx jest __tests__/sweep-service.test.ts` — 31/31 pass (including 16 new tests for auto-selection and permit capability matrix).
**Linked docs updated:** [[07 - Development/Environment Variables]], [[PRD - Sweep Gas Strategy - Permit Pull vs Gas Top-Up]]

---

### 2026-05-28 — deployment@4e8658d — Gatus monitoring: Docker service + config

**Commits:** deployment `1ac2e74` → `4e8658d`
**Touched:** `deployment/gatus/config.yaml`, `deployment/docker-compose.yml`, `deployment/.env`
**Why:** Add Gatus monitoring service to the deployment stack. Config covers backend-dev, backend-prod, frontend-dev, frontend-prod, and external deps (RN API, Chainalysis, BSC RPC). Telegram alerting configured. Service exposed via Traefik at `gatus.ch.manko.yoga`.
**Verification:** Config file validated against Gatus schema. Awaiting `docker-compose up -d gatus` on server.
**Linked docs updated:** [[08 - Operations/Gatus Monitoring - Proposed Config]]

---

### 2026-05-28 — backend@6c01a30 — Gatus monitoring: GET /api/health endpoint

**Commits:** backend `19f7eb9` → `44579d6` → `6c01a30` (2.6.48 → 2.6.49)
**Touched:**
- Backend: `src/services/health/healthCheckService.ts`, `src/services/health/index.ts`, `src/app.ts`, `__tests__/health-check.test.ts`
**Why:** Implement `GET /api/health` for Gatus monitoring. Exposes 5 checks (db, redis, rnChainRegistry, rnTokenRegistry, rnApi) in a single public endpoint. Status semantics: `ok` | `degraded` | `down` (503 when DB fails). Each check includes `latencyMs`; registry checks include counts. Rate limiter and request logging skip `/api/health`. 5 route-level unit tests cover ok/degraded/down transitions.
**Verification:** `tsc --noEmit` clean. `npx jest __tests__/health-check.test.ts` — 5/5 pass.
**Linked docs updated:** [[08 - Operations/Gatus Monitoring - Proposed Config]]

---

### 2026-05-28 — backend@19f7eb9, frontend@60ee6fb — Task #10: AML screening (Chainalysis, seller-paid, seller opt-in)

**Commits:** backend `441c8be` → `80ba046` → `19f7eb9` (2.6.46 → 2.6.47), frontend `717d5c8` → `b7540f5` → `60ee6fb` (2.6.46 → 2.6.47)
**Touched:**
- Backend: `src/services/payment/safety/amlProvider.ts`, `src/services/payment/safety/chainalysisProvider.ts`, `src/services/payment/safety/amlScreeningService.ts`, `src/services/payment/safety/transactionSafetyProvider.ts`, `src/services/payment/paymentCoordinator.ts`, `src/services/admin/amlConfigRoutes.ts`, `src/models/SellerOffer.ts`, `src/app.ts`, `.env.example`
- Frontend: `src/sections/request/components/seller-steps/step-1-send-proposal.tsx`, `src/types/marketplace.ts`
**Why:** Task #10 implementation. Chainalysis Public Sanctions API integration for seller-paid AML screening. Seller can opt-in per-offer via `requireAmlCheck` + `amlBlockOnFailure` toggles. `TransactionSafetyProvider` screens buyer source address after on-chain transfer verification. `paymentCoordinator` deducts `AML_CHECK_COST_USD` (default 0, API is free) from seller escrow on payment completion. Admin routes for AML config.
**Verification:** Frontend `tsc --noEmit` clean. Backend relevant tests pass (module resolution issues in unrelated test files).
**Linked docs updated:** [[02 - Data Models/SellerOffer]], [[03 - API Reference/Admin API]], [[04 - Flows/Escrow Flow]]

---

### 2026-05-28 — backend@441c8be, frontend@717d5c8 — Task #9: Per-chain confirmation thresholds + admin UI

**Commits:** backend `4a85737` → `441c8be` (2.6.47 → 2.6.48), frontend `0ebb2f1` → `717d5c8` (2.6.46 → 2.6.48)
**Touched:**
- Backend: `src/models/ConfigSetting.ts`, `src/services/payment/safety/confirmationThresholdService.ts`, `src/services/payment/safety/transactionSafetyProvider.ts`, `src/services/admin/confirmationThresholdRoutes.ts`, `src/services/admin/awaitingConfirmationRoutes.ts`, `src/app.ts`
- Frontend: `src/sections/admin/confirmation-thresholds/`, `src/sections/admin/payments-awaiting-confirmation/`, `src/actions/confirmation-thresholds.ts`, `src/routes/paths.ts`, `src/layouts/nav-config-dashboard.tsx`
**Why:** PRD §3 — Task #9 implementation. Runtime per-chain confirmation thresholds via `ConfigSetting` Mongo model with 30s in-memory cache. `TransactionSafetyProvider` now reads `getConfirmationThreshold(chainId)` instead of static env. Admin endpoints: `GET/PATCH /api/admin/settings/confirmation-thresholds`, `GET /api/admin/payments/awaiting-confirmation`. Frontend admin pages for threshold editing and awaiting-confirmation payment monitoring.
**Verification:** All 56 relevant backend tests green. Frontend `tsc --noEmit` clean.
**Linked docs updated:** [[03 - API Reference/Payment API]]

---

### 2026-05-28 — backend@4a85737, frontend@0ebb2f1 — Task #8: Multichain RN proxy registry + USDC/USDT support + Base fix + USDT fork test

**Commits:** backend `01b9ea0` → `ae17b18` → `4a85737` (2.6.45 → 2.6.47), frontend `0ebb2f1` (2.6.44 → 2.6.46)
**Touched:**
- Backend: `src/services/payment/requestNetwork/supportedChains.json`, `src/services/payment/requestNetwork/tokens.json`, `src/services/payment/requestNetwork/tokens.ts`, `src/services/payment/requestNetwork/proxyAddresses.ts`, `src/services/payment/requestNetwork/inHouseCheckout.ts`, `src/services/payment/requestNetwork/networkRegistryRoutes.ts`, `src/services/payment/wallets/sweepService.ts`, `src/app.ts`, `scripts/probe-rn-chains.ts`
- Frontend: `src/web3/config.ts`, `src/sections/payment/checkout/rn-in-house-checkout-view.tsx`, `src/sections/admin/networks/`, `src/app/dashboard/admin/networks/page.tsx`, `src/actions/network-registry.ts`, `src/routes/paths.ts`, `src/layouts/nav-config-dashboard.tsx`
**Why:** PRD §2 — Task #8 implementation. 5-chain registry (BSC, Arbitrum, Ethereum, Polygon, Base) with canonical RN ERC20FeeProxy addresses and per-chain USDC/USDT entries including Base. `tokens.ts` and `proxyAddresses.ts` now load from JSON files with admin reload capability. `buildInHouseCheckoutBlock` returns `unsupported_chain:<id>` for unknown chains. Frontend wagmi config expanded to include arbitrum + base. Per-chain explorer URLs in checkout view. USDT-mainnet `approve(0)` reset quirk handled in approve flow. New admin page `/dashboard/admin/networks` renders registry with reload button. New probe script `scripts/probe-rn-chains.ts` verifies proxy deployment on-chain.
**Verification:** All 58 relevant backend tests green (`rn-in-house-checkout`, `derived-destinations`, `sweep-service`, `request-template-orphan-cleanup`). Frontend `tsc --noEmit` clean.
**Linked docs updated:** [[03 - API Reference/Payment API]] (new `GET /api/admin/rn/networks` and `POST /api/admin/rn/networks/reload` endpoints)

---

### 2026-05-28 — backend@34f542e — Task #7 B: unit tests for derived-destinations + sweep-service + orphan-cleanup regression

**Commits:** backend `34f542e` (2.6.44 → 2.6.45)
**Touched:** `__tests__/derived-destinations.test.ts` (26 tests), `__tests__/sweep-service.test.ts` (18 tests), `__tests__/request-template-orphan-cleanup.test.ts` (2 tests)
**Why:** PRD item B — regression lock-in test suite for Task #7. Covers: `getDestinationFor` idempotency, E11000 race fallback, `validateXpub` rejection of xpriv/tprv/garbage, `deriveAddressAtIndex` determinism, `recordSweep` `$inc` accumulation (regression lock-in for item E), and orphan-payment cleanup provider filtering (regression lock-in for Gap 2 fix in 2.6.44).
**Verification:** All 46 tests green (`npx jest derived-destinations.test.ts sweep-service.test.ts request-template-orphan-cleanup.test.ts`).
**Linked docs updated:** [[08 - Operations/Handoff - Request Network In-House Checkout - 2026-05-28]]

---

### 2026-05-28 — backend@1889169, frontend@c44ed64 — Task #7 A verification fix: multi-checkout conversion + orphan-payment guard

**Commits:** backend `1889169` (2.6.43 → 2.6.44), frontend `c44ed64` (2.6.43 → 2.6.44)
**Touched:**
- Backend: `src/services/marketplace/RequestTemplateService.ts`
- Frontend: `src/sections/payment/checkout/rn-multi-checkout-view.tsx`
**Why:** A verification revealed two gaps: (1) `RnMultiCheckoutView.handleFinish` only navigated to payment list and never called `convertTemplatesToRequests`, so multi-seller carts never created PurchaseRequests; fixed by calling conversion with stashed cart items and navigating to the first created request. (2) Backend orphan-payment cleanup found ALL pending payments for the buyer and hard-deleted all but the first — fatal for multi-seller carts; fixed by restricting orphan query to `provider: 'shkeeper'` only so request.network payments retain their independent lifecycle.
**Verification:** Pushed to `integrate-main-into-development` on both repos — Woodpecker builds pending.
**Linked docs updated:** [[03 - API Reference/Payment API]]

---

### 2026-05-28 — backend@faf2221, frontend@022ecb6 — Task #7 derived destinations: sweep autostart, recordSweep fix, multi-seller checkout UX

**Commits:** backend `faf2221` (2.6.42 → 2.6.43), frontend `022ecb6` (2.6.42 → 2.6.43)
**Touched:**
- Backend: `src/app.ts`, `src/models/DerivedDestination.ts`, `src/models/Payment.ts`, `src/services/payment/requestNetwork/requestNetworkPayInService.ts`, `src/services/payment/wallets/derivedDestinations.ts`, `.env.example`
- Frontend: `src/sections/payment/checkout/rn-in-house-checkout-view.tsx`, `src/sections/request-template/request-template-checkout-payment.tsx`, `src/web3/components/multi-seller-provider-payment.tsx`, `src/sections/payment/checkout/rn-multi-checkout-view.tsx`, `src/app/checkout/request-network/multi/page.tsx`
**Why:** PRD items D/E/F + frontend cart-aware checkout (A). Auto-start sweep cron on boot; fix `recordSweep` to `$inc` totalSwept instead of `$setOnInsert`; widen Payment unique index to include `sellerOfferId` for multi-seller carts; add multi-seller checkout wrapper and wire into template + request flows.
**Verification:** Pushed to `integrate-main-into-development` on both repos — Woodpecker builds pending.
**Linked docs updated:** [[03 - API Reference/Payment API]] (derived-destination endpoints)

---

### 2026-05-28 — backend@e46be98, frontend@af77b3c — add nick-doc sync rule + version bumps

**Commits:** backend `e46be98` (2.6.24 → 2.6.25), frontend `af77b3c` (2.6.25 → 2.6.26)
**Touched:** `backend/AGENTS.md`, `frontend/AGENTS.md` (new), both `package.json` +
`package-lock.json`
**Why:** Establish a mandatory rule that every code push must be followed by a
nick-doc Activity Log entry (and relevant section updates) so the vault never
falls behind the code. Frontend AGENTS.md created from scratch (was missing).
**Verification:** Pushed to `integrate-main-into-development` on both repos —
Woodpecker builds pending.
**Linked docs updated:** This vault's `AGENTS.md` updated with the same rule.
**Note:** Backend (2.6.25) and frontend (2.6.26) are intentionally one patch
apart — backend was a version behind before this session. Should be re-aligned
on the next paired bump.

---

### 2026-05-28 — frontend@9d4aa37 — fix 429 request storm on template SWR hooks

**Commits:** `9d4aa37`
**Touched:** `src/actions/request-template.ts`
**Why:** Production browser showed repeated 429 (Too Many Requests) on
`/api/marketplace/request-templates/sellers`. Default SWR config was
revalidating on focus/reconnect and retrying on errors, making backend
rate-limit recover impossible without a restart.
**Verification:** Pushed, awaiting Woodpecker build. Visual confirmation on
dev.amn.gg after deploy.
**Linked docs updated:** none yet — SWR pattern should be promoted to
`07 - Development/Coding Standards.md` in a follow-up.

---

### 2026-05-28 — frontend@6c89444 — improve request template form debug feedback

**Commits:** `6c89444`
**Touched:** `src/sections/request-template/request-template-new-edit-form.tsx`
**Why:** Users could not tell why "ایجاد قالب" failed — validation errors
silently blocked submission, API errors collapsed to generic "خطایی رخ داده
است!", and the "انتشار" Switch in renderActions was visual-only.
**Verification:** Type-check passes via Docker build in prior session; manual
browser test pending.
**Linked docs updated:** none.

---

### 2026-05-27 — frontend@8c0f14d, ad498f4, f3a3c9d, bb72a66 — unblock 2.6.19 Docker build

**Commits:** `bb72a66` `f3a3c9d` `ad498f4` `8c0f14d`
**Touched:** `src/sections/request-template/request-template-checkout-payment.tsx`,
`src/web3/components/wallet-selector.tsx`, `tsconfig.json`, `src/types/payment.ts`
**Why:** Docker build was failing on TypeScript compilation after the
wallet-support + test-payment feature merge. Four distinct errors fixed:
User type uses `_id` not `id`; wallet-selector imported non-existent
`@/components/ui/dialog`; `@/*` path alias missing from tsconfig; IPayment
metadata type didn't allow test-payment fields.
**Verification:** Local `docker build` succeeded — image
`escrow-frontend:2.6.19` created.
**Linked docs updated:** none — should add SWR + UI library notes to
`07 - Development/Coding Standards.md`.

---

### 2026-06-02 — backend@4949988, frontend@integrate-main-into-development — route admin user counts through postgres-capable stores

**Commits:** `4949988` (backend), version bump (frontend)
**Touched:** `src/services/user/userController.ts`, `src/services/auth/authStore.ts`,
`src/db/repositories/interfaces/IMarketplaceRepo.ts`, `src/db/repositories/interfaces/IPaymentRepo.ts`,
`src/db/repositories/mongo/MongoMarketplaceRepo.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`,
`src/db/repositories/dual/DualWriteMarketplaceRepo.ts`,
`src/db/repositories/mongo/MongoPaymentRepo.ts`, `src/db/repositories/drizzle/DrizzlePaymentRepo.ts`,
`src/db/repositories/dual/DualWritePaymentRepo.ts`,
`__tests__/auth-store-pg-query.test.ts`, `__tests__/user-dependencies-repo.test.ts`,
`scripts/smoke/user-admin-postgres.sh`, `scripts/smoke/user-dependencies.sh`
**Why:** `GET /api/user/admin/:userId/dependencies` was directly importing Mongoose models
(`RequestTemplate`, `PurchaseRequest`, `Payment`, `Chat`) bypassing the repository layer and
blocking `MONGO_CONNECT_MODE=never`. Routed through `getUserDependencyCounts` / `countByParticipant`
on all three repo tiers (Mongo, Drizzle, DualWrite). Also hardened `AuthUser` PgQuery facade with
generic multi-field sorting, nested path support, and `isActive`/`isVerified` filter aliases so
admin list/stats routes work correctly in `AUTH_STORE=postgres` mode.
**Verification:** 5 test suites, 11 tests pass. `typecheck` clean. Smoke scripts added but not
run end-to-end (require live `ADMIN_TOKEN`).
**Linked docs updated:** none — Mongo removal remaining work tracked in `MONGODB_REMOVAL_HANDOFF_2026-06-02.md`.

---

### 2026-06-02 — backend@515bea3 — guard dataCleanupService against MONGO_CONNECT_MODE=never
**Commits:** `515bea3`
**Touched:** `src/services/admin/dataCleanupService.ts`
**Why:** dataCleanupService dynamically imported 10 Mongoose models unconditionally; under MONGO_CONNECT_MODE=never this throws at runtime. Routed user counts through AuthUser facade (Postgres-gated). Wrapped all other model access in isMongoAvailable() guard — cleanup ops return a safe error result when MongoDB is disconnected.
**Verification:** 5 test suites 11 tests pass. typecheck clean. Mongo scan: all remaining hits confirmed inside Postgres-gated fallback branches.
**Linked docs updated:** none.

---

### 2026-06-02 — backend@14c231e, frontend@d7a2a86 — make admin user management work end-to-end (v2.8.50)

**Commits:** `14c231e` (backend), `d7a2a86` (frontend)
**Touched (backend):** `src/services/auth/authStore.ts`, `src/services/user/userRoutes.ts`,
`__tests__/auth-store-pg-query.test.ts`
**Touched (frontend):** `src/actions/user.ts`, `src/types/user.ts`,
`src/sections/user/view/user-list-view.tsx`, `src/sections/user/user-table-row.tsx`,
`src/sections/user/user-quick-edit-form.tsx`, `src/sections/user/user-new-edit-form.tsx`,
`src/sections/request/view/admin/admin-request-list-view.tsx`,
`src/sections/overview/app/view/overview-admin-view.tsx`, `Dockerfile`
**Why:** Admin could not delete users on dev (`DELETE /api/users/admin/:id` → 404). Root cause:
PG rows seeded outside the auth store have NULL `legacy_object_id`; the auth store fabricated a
fresh random `_id` on every read, so no admin action keyed on that id could ever resolve.
Audit of the admin dashboard surfaced further breakage which is also fixed in this pair of
commits:
- Backend: legacy-id self-heal on read; `findById` accepts PG uuids; missing
  `/api/users/admin/:userId/toggle-status` and `/dependencies` routes added (frontend called
  them under the plural prefix where they did not exist); soft-deleted users excluded from
  admin list and all stats; list/detail return full profile address fields.
- Frontend: `updateUserStatus` sent `{status}` but backend reads `{isActive}` — activating a
  user actually suspended them; user quick-edit form was a mock (no API call); "حذف کاربر"
  button in edit form had no handler; bulk deletes used `Promise.all` (partial failures
  hidden); list query params (`query`/`status`) were ignored by the backend (now
  `search`/`isActive`); profile payloads now match the backend schema (dotted paths, no
  whole-profile overwrite); blocked `aria-hidden` console warnings fixed.
**Verification:** Backend: 10 tests pass (`auth-store-pg-query`, `user-dependencies-repo`,
`repository-factory-modes`), `tsc --noEmit` clean. Frontend: `tsc --noEmit` + eslint clean
(jest suite is broken repo-wide — pre-existing ESM transform issue, unrelated). Smoke test
against dev pending post-deploy (admin will verify delete in UI).
**Linked docs updated:** `03 - API Reference/Admin API.md`, `03 - API Reference/User API.md`
(stale KNOWN BUG notes removed; route availability updated).

---

### 2026-06-03 — backend@378f8f6, frontend@6fe1328 — unblock user creation & purchase requests for native-PG users (v2.8.51)

**Commits:** `378f8f6` (backend), `6fe1328` (frontend)
**Touched (backend):** `src/services/auth/authStore.ts`, `src/services/auth/authController.ts`,
`src/services/user/userRoutes.ts`, `src/services/user/userController.ts`,
`src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `DrizzleUserRepo.ts`,
`DrizzleTrezorAccountRepo.ts`, `DrizzleDerivedDestinationRepo.ts`,
`__tests__/auth-store-pg-query.test.ts`
**Touched (frontend):** `src/actions/user.ts`, user section forms/views (error toasts)
**Why:** Two follow-up dev blockers after v2.8.50:
1. Admin create returned 409 for emails held by soft-deleted accounts (users.email is unique;
   deleted rows kept the address and are hidden from the list). Fix: delete releases the email
   (`deleted_<id>_<email>`); create/register lazily free emails held by previously deleted rows.
2. Purchase request creation threw "Buyer <id> not found in Postgres id map" — users created
   natively in PG (auth store / Telegram / Google / admin create) never get id_map rows. Fix:
   resolveId in 4 Drizzle repos falls back to the entity's own legacy_object_id column and
   self-heals id_map.
**Verification:** 47 backend tests green across 6 suites, tsc clean (both repos), eslint clean.
Deployed to dev via CI; admin verifies create-user and create-request flows in UI.
**Linked docs updated:** `03 - API Reference/Admin API.md`, `03 - API Reference/User API.md`.

---

### 2026-06-03 — backend@804bb99, frontend@714dfbd — fix PG response serialization & id resolution (v2.8.52)

**Commits:** `804bb99` (backend), `714dfbd` (frontend)
**Touched (backend):** `src/services/marketplace/PurchaseRequestService.ts`,
`src/services/chat/ChatService.ts`, `src/db/repositories/drizzle/DrizzleChatRepo.ts`,
`src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`
**Touched (frontend):** request list views (buyer/seller/admin), `src/socket/hooks/use-purchase-requests.ts`,
`src/types/marketplace.ts`
**Why:** After v2.8.51, request detail pages opened `/purchase-requests/[object Object]` and the
seller request list returned 500. Root causes (all PG-path):
1. `mongoCompatId` returned `{toString: () => id}` — JSON.stringify drops functions, so every
   response carried `_id: {}`. Now a plain string; chat compat ObjectIds get `toJSON()` instead.
2. `findPurchaseRequests` passed unresolved legacy 24-hex user ids into uuid column comparisons
   (PG cast error → 500); the route's Mongo-style `$or` filter was silently ignored (privacy:
   non-admin users saw all requests). Both fixed — ids resolved up front, `$or` honored.
3. 16 `resolveId(...) ?? originalId` fallbacks replaced with a nil-UUID fallback (500 → empty/404).
4. Frontend reads `request.id || request._id` everywhere (tolerates both API shapes).
**Verification:** 47 backend tests green, tsc clean both repos, eslint clean. Deployed to dev via
CI; admin verifies: request detail page, seller request list, chat.
**Linked docs updated:** none beyond this log (API id semantics documented in Marketplace API as
needed later).

---

### 2026-06-03 — frontend@7b949bf — Mini App live socket updates (v2.8.53)

**Commits:** `7b949bf` (frontend only; backend stays at 2.8.52)
**Touched:** `src/sections/telegram/hooks/use-telegram-realtime.ts` (new),
`telegram-request-detail-view.tsx`, `telegram-requests-view.tsx`, `telegram-mini-app-view.tsx`
**Why:** Mini App request views were SWR-only — status changes required a manual reload.
New `useTelegramRealtime` hook bridges marketplace socket events to SWR refresh (plus a 30s
periodic fallback for WebView throttling). Detail view joins the request room; list view joins
the buyer/seller room.
**Verification:** tsc + eslint clean. Admin verifies live stepper advance in Telegram after deploy.
**Linked docs updated:** none.

---

### 2026-06-03 — backend@8b8c1ae, frontend@583d55a — 'guard' role + Mini App tab fix (v2.8.54)

**Commits:** `8b8c1ae` (backend), `583d55a` (frontend)
**Touched (backend):** User model, drizzle users schema + migration `0017_user_role_guard.sql`,
postgresAuthSchema (enum + ALTER TYPE for existing DBs), shared types, IUserRepo,
authStore/authService role types, userRoutes role validation
**Touched (frontend):** role types/permissions/display-names, user forms, useRole hook,
_roles mock, user-table-row (central role labels), telegram-mini-app-view (tab dispatch)
**Why:**
1. New admin-assignable role «نگهبان» (guard), registered across every role definition site —
   modeled on resolver. No special endpoint permissions yet; grant via authorizeRoles() later.
   Side fix: the admin role-change endpoint previously rejected 'resolver' (validation list only
   had buyer/seller/admin) — both resolver and guard now pass.
2. Mini App: tapping a bottom tab while a request-detail/chat/new-request overlay was open did
   nothing (overlay kept rendering over the new tab). Tab selection now dismisses overlays.
**Verification:** Backend tsc clean, 12 tests green. Frontend tsc + eslint clean. Admin verifies
after deploy: role dropdown shows نگهبان; Mini App tabs always respond.
**Linked docs updated:** `03 - API Reference/User API.md` (role enum), pending.

---

### 2026-06-03 — backend@9424395, frontend@a18e870 — chat/notification fixes, role dashboards, live Mini App chat (v2.8.55)

**Commits:** `9424395` (backend), `a18e870` (frontend)
**Why (from chat + notifications + Mini App audits):**
1. **Chat (backend):** 5 endpoints were dead — routes pass `:id` but controllers destructured
   `req.params.chatId` (undefined): edit/delete message, archive chat, add/remove participant.
2. **Notifications → Telegram (backend):** `sendTelegramNotification` existed but was never
   called. Every in-app notification is now also delivered as a Telegram bot message when the
   user has an active telegram_link (new `sendTelegramNotificationToUser`, lazy-required,
   fire-and-forget).
3. **Role dashboards (frontend):** resolver landed on the buyer dashboard. New
   `OverviewResolverView` (dispute stats) and `OverviewGuardView` (oversight stats); dispatch
   updated; «حل اختلاف» nav item now visible to resolver.
4. **Mini App chat (frontend):** was SWR-only. Thread view joins its conversation socket room,
   list joins the user room — messages now appear in real-time.
**Touched (backend):** `src/services/chat/chatController.ts`, `src/services/telegram/botService.ts`,
`src/services/notification/NotificationService.ts`
**Touched (frontend):** `src/sections/overview/app/view/*` (2 new views + dispatch),
`src/layouts/nav-config-dashboard.tsx`, `src/sections/telegram/hooks/use-telegram-chat-thread.ts`,
`use-telegram-conversations.ts`, `telegram-chat-view.tsx`
**Verification:** backend tsc clean + 28 tests green; frontend tsc clean, eslint clean (pre-existing
no-img-element rule-config errors in untouched files). Admin verifies after deploy: resolver
dashboard, Mini App live chat, Telegram bot messages on new offers, chat edit/delete.
**Known remaining (documented, not yet built):** Mini App notifications tab; web chat
"new conversation" button uses a placeholder id; Drizzle chat participant population.

---

### 2026-06-03 — backend@14d164c, frontend@6adb2e0 — Mini App account tab, pinned support chat, shop fix (v2.8.56)

**Commits:** `14d164c` (backend), `6adb2e0` (frontend)
**Why:**
1. **Mini App account tab:** «به‌زودی» rows are now functional — notifications (new in-shell list
   view + hook, socket-live, mark-all-read), support (creates/opens support chat via new
   `createSupportChat` action → POST /chat/support), wallet (shows linked address), guard role label.
2. **Mini App chat tab:** pinned «پشتیبانی AMN» row always at top (per user request); support-type
   conversations filtered from the regular list.
3. **Shop bug «خطا در بارگذاری اطلاعات فروشنده»:** PG sellers list returns uuid ids but
   seller-shop-view only accepted legacy 24-hex ids → uuid sellers always errored. Both formats
   accepted now (frontend validation + backend lookup comparison).
**Touched (backend):** `DrizzleMarketplaceRepo.findSellerWithActiveRequestTemplates`
**Touched (frontend):** telegram account/chat/notifications views + hooks, locales (fa/en/types),
`src/actions/chat.ts` (createSupportChat), `src/lib/axios.ts`, `seller-shop-view.tsx`
**Verification:** tsc clean both repos, 16 backend tests green, eslint clean (pre-existing
no-img-element config errors aside). Admin verifies after deploy: account tab rows, pinned support
chat, opening a seller shop from فروشگاه.
**Known remaining:** in-shell seller shop detail inside the Mini App (currently opens the web view —
now working after the id fix); Mini App notifications row could get an unread badge.

---

### 2026-06-03 — frontend@a8ae1e3 — full in-shell shop inside the Mini App (v2.8.57)

**Commits:** `a8ae1e3` (frontend only; backend stays at 2.8.56)
**Why:** Tapping a seller in فروشگاه opened the web dashboard inside the Telegram webview. The
seller's shop now renders in-shell: new `TelegramSellerShopView` (seller header + active templates
with budget/usage), `useTelegramSellerShop` hook, `TelegramShopRow` converted from `<a href>` to
in-shell `onOpen`, and an `openSellerId` overlay in the shell (BackButton + tab dismissal).
Template ordering hands off to the web from-template checkout (wallet stack lives there).
Cleanup: removed eslint-disable comments for the unregistered @next/next/no-img-element rule.
**Verification:** tsc clean, eslint clean (0 errors). Admin verifies after deploy: فروشگاه → tap
seller → in-shell shop → سفارش این قالب → web checkout.

---

### 2026-06-03 — frontend@HEAD — Mini App account tab parity with web sections (v2.8.58)

**Why:** The web account menu (تنظیمات عمومی، اعلان‌ها، کیف پول، آدرس‌های تحویل، مدیریت Passkey)
had no counterpart in the Mini App. All five are now reachable from the Mini App account tab —
notifications stay in-shell; settings/wallet/addresses/passkey deep-link to the web dashboard pages
(passkey/wallet need browser-context APIs). Labeled «در داشبورد وب باز می‌شود».
**Touched:** telegram-account-view.tsx, telegram locales (fa/en/types)
**Verification:** tsc + eslint clean. Pending: in-shell shop cart/checkout (user request — payment
hand-off design needed since the crypto wallet stack lives in web).

---

### 2026-06-03 — frontend@HEAD — Mini App in-shell shopping cart (v2.8.59) — buyer-parity phase 1

**Why:** Goal: full buyer-role parity in the Mini App (then other roles). Phase 1 = cart.
New `useTelegramCart` hook shares the web checkout's localStorage key
(`app-request-template-checkout`) so the Mini App cart IS the web cart; new `TelegramCartView`
(qty stepper, remove, total, checkout → web); add/remove-to-cart buttons in the in-shell seller
shop; cart badge + overlay in the shell.
**Buyer-parity roadmap (remaining):** offers view/accept on requests → delivery confirmation →
payments list → points/referral → addresses CRUD → TON Connect payments (Telegram Wallet is
TON-only, no EVM; backend already has tonProofService).
**Verification:** tsc + eslint clean. Admin verifies: فروشگاه → افزودن به سبد → سبد → ادامه و
پرداخت → web checkout shows the same items.

---

### 2026-06-03 — frontend@HEAD — Mini App floating cart FAB (v2.8.60)

**Why:** User wants the web shop's small edge-docked basket (count badge) in the Mini App too.
New `TelegramCartFab` component pinned above the tab bar, shown on shop tab + in-shell seller
shop, opens the in-shell cart. Replaced the header cart chip + bottom cart bar.
**Pending next (user requests):** (1) Mini App «تنظیمات» — dark/light mode for the Telegram shell
(needs a dark palette variant of telegram-shell-css + toggle; should follow webApp.colorScheme);
(2) «درخواست جدید» button font — it is Telegram's NATIVE MainButton, font cannot be customized;
option is replacing it with an in-shell button using the project font.
**Verification:** tsc + eslint clean.

---

### 2026-06-02 — backend@7c4dedf — complete dual-write repos, migrations pipeline, TTL scheduler, address reconciliation

**Commits:** `7c4dedf` (backend v2.8.44), frontend v2.8.44
**Touched:** `src/db/repositories/dual/DualWriteDisputeRepo.ts` (new), `DualWriteTrezorAccountRepo.ts` (new), `DualWriteDerivedDestinationRepo.ts` (new), `src/db/repositories/factory.ts`, `src/db/schema/dispute.ts`, `src/db/schema/address.ts`, `src/db/schema/fundsLedgerEntry.ts`, `src/services/address/addressStore.ts`, `src/services/admin/dataCleanupService.ts`, `src/services/admin/ttlCleanupJob.ts` (new), `src/app.ts`, `src/shared/types/address.ts`, `drizzle.config.ts`, `migrations/` (new), `__tests__/dispute-dual-write.test.ts` (new)
**Why:** Complete all remaining Phase 1 migration prerequisites: the three missing DualWrite repos (Dispute, TrezorAccount, DerivedDestination) unblock PG cutover for those domains; migrations pipeline enables drizzle-kit schema management; FundsLedgerEntry immutability trigger (DDL in migrations/) protects money records from mutation; Dispute composite indexes match Mongo query patterns; TTL scheduler replaces Mongo TTL indexes for Notification/TempVerification/TelegramSession; Address schema reconciled so Drizzle is authoritative.
**Verification:** typecheck clean. 5 core suites (11 tests) pass. DualWriteDisputeRepo 21 tests pass.
**Linked docs updated:** `MIGRATION_TODO.md` — all 9 tasks marked done.

---

### 2026-06-03 — frontend@HEAD — Mini App floating support bubble (v2.8.61)

**Why:** Parity with the web dashboard's green floating chat widget. New `TelegramSupportFab`
(edge-docked, green bubble) on home/shop/requests tabs + in-shell seller shop; opens/creates the
support chat. Stacks above the cart FAB in shop contexts.
**Verification:** tsc + eslint clean.
**In progress / queued:** Points & Referral audit (background agent — bugs observed: values render
as "·", Bronze level claims "highest level"); Mini App dark mode (تنظیمات); buyer-parity phase 2
(offers view/accept).

---

### 2026-06-03 — backend@c5d6490 — Points & Referral audit fixes (v2.8.62)

**Why (from points audit):** /dashboard/points showed "·" instead of numbers and told Bronze users
they are at "the highest level". Root causes: (1) level boundary off-by-one — seeds define
inclusive maxPoints (Bronze 0–999) but PG used `>` and Mongo `<`, so exact-boundary users resolved
to no level; (2) `level_configs` table in Postgres was created but NEVER seeded → zero levels →
null next-level → "highest level" message. Fixes: inclusive comparisons in both repos +
self-seeding of the 5 default levels (برنز/نقره/طلا/پلاتین/الماس) in ensurePostgresLevelConfigSchema.
**Remaining from audit:** frontend points page swallows API errors (shows zeros instead of an
error state); integration ideas: award points on completed escrow, show points in Mini App.
**Verification:** tsc clean, 8 tests green. Admin verifies after deploy: /dashboard/points shows
real numbers and Bronze shows progress toward نقره.

---

### 2026-06-03 — frontend@131700b — compact floating cart/support FABs (v2.8.63)

**Why:** User feedback — the two edge-docked FABs were too large. Compacted paddings, icons,
badge, and stack positions.
**Verification:** tsc clean.

---

### 2026-06-03 — frontend@d64e194 — Mini App points view + tap-to-verify email (v2.8.64)

**Why:** Buyer parity — امتیازات و رفرال now in-shell (TelegramPointsView: points cards, level
progress, referral code copy + stats; account-tab row). Email verification from the Mini App:
unverified badge is tappable → sends code → code-entry sheet → verifyProfileEmail → session
refresh. Reuses existing account.ts actions.
**Verification:** tsc + eslint clean. Test after deploy: account tab → امتیازات row; unverified
user → tap red badge → enter emailed code → badge turns green.

---

### 2026-06-03 — backend@91877ae — points lookups accept legacy user ids (v2.8.63)

**Why:** Mini App points view (v2.8.64) failed: every user lookup in DrizzlePointsRepo compared
users.id (uuid) against the JWT legacy 24-hex id → uuid cast error → 500. All lookups now match
either format (id::text = $n OR legacy_object_id = $n).
**Verification:** tsc clean, tests green. Test after deploy: Mini App → حساب → امتیازات shows data.

---

### 2026-06-03 — frontend@HEAD — remove MainButton, in-tab CTA, sign-in retry (v2.8.65)

**Why (user requests):** (1) native Telegram MainButton removed — wrong font, duplicated CTAs;
«+ درخواست جدید» button added inside the requests tab header instead. (2) Intermittent 503 on
Telegram sign-in (backend redeploy windows) — sign-in now retries transient 502/503/504 3x with
backoff and shows the Persian message.
**Queued (user, with screenshots):** cart −=remove at qty 1; seller-shop card redesign (+ icon,
template detail view, search/filter/sort); requests search + status-chip filters + sort; shop
search; seller chat button; referral signup points; dark mode.
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — frontend v2.8.66–67 — seller chat, header bell, cart UX + review system audit

**v2.8.66:** seller rows in the Mini App shop get a chat icon (direct conversation with the
seller); header gets a notification bell opening the in-shell notifications list.
**v2.8.67 fixes:** seller chat gave "Validation failed" (backend expects ONE participant for
direct chats — current user auto-added; we sent two). Cart «−» at qty 1 now removes the item
(separate remove button dropped). Bell: borderless + red dot instead of count.
**Review/rating system audit completed (agent):** backend schema/API/store fully working
(reviews table, /api/marketplace/reviews). Broken: seller rating HARDCODED 4.5 in
DrizzleMarketplaceRepo.findSellersWithActiveRequestTemplates (never reads real reviews);
review form never sends purchaseRequestId (verified-buyer badge never set); Step 6
post-delivery has NO review prompt; Mini App has no reviews. Full phased plan in agent output —
next work items.

---

### 2026-06-03 — backend v2.8.64–65 — real seller ratings + chat participant names

**v2.8.64:** seller list ratings now aggregated from published reviews (was hardcoded 4.5);
exposes rating/ratingCount per seller.
**v2.8.65:** PG chats showed "Unknown User" — participants (JSONB ids) are now populated with
firstName/lastName/email/avatar from the users table (uuid OR legacy id matched), in
findByIdWithParticipants and findForUser. Parity with Mongo .populate().
**In progress:** background agent fixing seeds for the PG database + creating complete mock shops
(categories in PG → fixes template-creation 500) — will be reviewed and shipped when it reports.
**Queued (user):** in-shell تنظیمات عمومی (profile editing) in Mini App; post-delivery review
prompt; template detail view + search/filter/sort in shop and requests; dark mode.

---

### 2026-06-03 — backend v2.8.65–66 — PG seeds, mock shops, template-creation fix

**Template-creation 500 root cause (agent):** categories table column conflict (Drizzle "order"
vs categoryStore order_num) + random legacy ids per seed → category ids served by the API were
unresolvable by createRequestTemplate. Fixed: categoryStore aligned to canonical "order" column
(with one-time order_num migration), deterministic category legacy ids, hardened categories
fallback (id::text match). Also fixed: the v2.8.64 rating aggregation queried Drizzle reviews
columns but the physical table (written by reviewStore) has different columns — rewritten
against the real columns (seller_user_id).
**New seed:** src/seeds/seedMockShops.ts — Persian categories, 5 sellers + shop settings,
templates with picsum product images, published reviews. npm scripts fixed (paths) + added
seed:levels / seed:templates / seed:mock-shops.
**⚠️ DEV ENV REQUIREMENT:** CATEGORY_STORE=postgres (and ideally REVIEW_STORE / 
SHOP_SETTINGS_STORE = postgres) must be set in dev .env.local when REPO_MARKETPLACE=postgres —
docker-compose.dev.yml still defaults these to mongo. Without this, template creation keeps
failing.
**Run on dev after deploy:** docker exec <backend-container> npm run seed:categories && npm run
seed:mock-shops
**Verification:** tsc clean, 14 tests green (3 suites).

---

### 2026-06-03 — frontend v2.8.68 — product-style template cards + in-shell template detail

**Why (user reference design):** seller-shop template cards redesigned image-first (photo +
circular add-to-cart overlay + title/price); tapping opens a new in-shell template detail view
(image gallery with thumbnails/counter, product-type chip, price, description, specs, tags,
sticky add-to-cart/order). New view: telegram-template-detail-view.tsx; shell openTemplate
overlay.
**Dev env (user applied):** CATEGORY_STORE/REVIEW_STORE/SHOP_SETTINGS_STORE=postgres set;
backend v2.8.66 restarted healthy; 24 categories auto-seeded into PG. seed:mock-shops still to
be run by the user for demo data.
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — backend v2.8.67 — auto-seed mock shops on startup

**Feature:** new env flag `SEED_MOCK_SHOPS_ON_START=true` runs seedMockShops() during backend
boot (after migrations/levels). Idempotent — existing rows are skipped — so the flag can stay
on permanently in dev.
**Action for dev env:** add `SEED_MOCK_SHOPS_ON_START=true` next to the other store flags and
restart the backend container; demo shops/templates/reviews appear automatically.
**Verification:** tsc clean.

---

### 2026-06-03 — frontend v2.8.69 — web-app-parity shop cards + template detail gallery

**Per user screenshots:** (1) shop cards — circular «+» add-to-cart moved OFF the photo, now
sits beside the title/price row; (2) template detail — web-style carousel: rounded image,
‹ n/m › arrow/counter pill, touch-swipe navigation, centered thumbnails; (3) removed «سفارش این
قالب» everywhere (product logic: buyer either creates a request or buys via cart) — add-to-cart
is the single full-width CTA in the detail sticky bar.
Files: telegram-seller-shop-view.tsx, telegram-template-detail-view.tsx.
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — frontend v2.8.70 — in-shell settings + addresses, central theme + dark mode

**In-shell account screens (were opening the web dashboard):**
- تنظیمات عمومی → telegram-settings-view.tsx: name/email/bio edit form via
  updateUserProfile; session refresh on save.
- آدرس‌های تحویل → telegram-addresses-view.tsx: full address CRUD (list / add /
  edit / delete / set-primary) on the addresses API + use-telegram-addresses SWR hook.
- Account rows switched from href→onClick; new overlay states 'settings'/'addresses'
  in the shell. (Passkey stays web — WebAuthn limitation.)

**Theme overhaul — Mini App now uses the CENTRAL web-app theme:**
- constants.ts: TG_PALETTE_LIGHT / TG_PALETTE_DARK derived from
  src/theme/theme-config.ts (primary green, grey ramp, status colors) instead of
  the ad-hoc cream/saffron palette; font stack aligned with AMANEH_FONT
  (IBM Plex Sans / Vazirmatn).
- telegram-shell-css.ts: emits both palettes; `.tg-shell--dark` flips every token —
  components keep the same CSS-var names.
- use-telegram-theme hook: auto (follow Telegram colorScheme) | light | dark,
  persisted in localStorage; syncs Telegram native header/background colors.
- Theme toggle row (auto/light/dark) added to the account tab.
- Hero headers (account / seller-shop / welcome banner / unlinked) use new
  --header-bg/--header-fg/--header-fg-muted vars so they stay legible in both modes.
- Removed the inline themeParams background/text override in getTelegramShellStyle
  that forced a navy shell under light components on dark-mode Telegram clients
  (the bad bg/font the user reported).
**Verification:** tsc + eslint clean (one pre-existing no-constant-condition
warning in telegram-webapp.ts, untouched).

---

### 2026-06-03 — frontend v2.8.71 — Mini App: solar icons, avatar upload, achievements, theme-toggle fix

**Icons:** telegram-icons.tsx redrawn to mirror the dashboard's Solar "linear"
set (home-2 / document-add / chat-round-dots / user / shop-2 / wallet-money /
rounded arrows). Added sun, moon, themeAuto, camera, trophy, lock.
**Theme toggle now icon-based** (sun=light, moon=dark, half-disc=auto) instead
of text labels — per user request.
**BUGFIX — theme toggle didn't respond:** TelegramListRow rendered a *disabled*
<button> wrapper whenever it had no onClick; a disabled button swallows clicks
from nested children, so the language AND theme toggles (rendered inside the
row's `value` slot) never received taps. Non-interactive rows (no onClick, no
href) now render a <div>.
**Avatar upload in settings:** telegram-settings-view gained a tappable avatar
with a camera badge → file picker → uploadUserAvatar + updateUserProfile
(persists immediately + session refresh); 3 MB / JPG-PNG-GIF validation; relative
/uploads paths resolved to full backend URL.
**Achievements in points view:** milestone badges (joined / verify email / first
referral / 5 active referrals / 1000 points) derived client-side from the user's
points + referral + isEmailVerified data; unlocked (trophy + "earned") vs locked
(lock + "+N" reward) with an N/total counter. Display-layer gamification over the
existing server-side point grants.
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — frontend v2.8.72 — Mini App fixes (avatar/email/web-links) + search/sort

**Bug fixes (user testing on v2.8.71):**
- **Avatar broken in account header** — it rendered the raw relative `/uploads`
  path against the frontend origin (404 inside the Telegram WebView). New shared
  `resolveTelegramAvatar` (sections/telegram/avatar-url.ts) rebases avatars onto
  the backend origin; used in both the account header and the settings uploader.
- **Inline email verification in settings** — changing the email now surfaces a
  code field + verify/resend buttons (resendProfileEmailVerification +
  verifyProfileEmail) whenever the saved email is unverified, instead of forcing
  the user back to the account-tab badge.
- **Passkey/Wallet rows broke the Mini App** — `href` replaced the whole WebView
  with the web dashboard and the Telegram back button couldn't return. They now
  open in Telegram's in-app browser via `webApp.openLink`
  (openTelegramExternalLink in telegram-webapp.ts), keeping the Mini App mounted.

**Feature — search / filter / sort (queued item):**
- Requests: search + status filter chips (all / active / completed / cancelled)
  + sort (newest / highest budget).
- Shop sellers: search + sort (top rated / most templates / newest).
- Seller-shop templates: search + sort (newest / cheapest / priciest), shown
  when the shop has > 3 templates.
- New reusable `TelegramListControls` component + `controls` locale section.
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — backend v2.8.68 + frontend v2.8.73 — pending-email flow, referral signup bonus, Mini App cleanups

**Backend v2.8.68 — email change now requires verification before it applies:**
- New `pendingEmail` field. Changing email parks the new address in pendingEmail
  (NOT email) and sends a code to the NEW address; the account email + its
  verified status stay intact until the code is confirmed, then pendingEmail is
  promoted to email (with a re-check nobody else claimed it). Resend targets the
  pending address. Implemented in the Mongoose User model AND the Postgres
  authStore path (PgAuthUser field + constructor + toObject + savePgUser
  INSERT/ON CONFLICT params + mirrorUserToMongo + `pending_email` column via
  idempotent ALTER in postgresAuthSchema). userController.updateUserProfile /
  resendCurrentUserEmailVerification / verifyCurrentUserEmail updated.
- **Referral signup bonus:** referrer gets +50 points the moment someone signs
  up with their code (email + Google paths) via PointsService.addPoints (source
  'referral'), referralStats.totalEarned kept in step, 'referral-reward' socket
  event. Best-effort, never blocks signup. Separate from the 2% purchase commission.

**Frontend v2.8.73 — Mini App:**
- Removed the «وضعیت‌های امانت» escrow-states legend from Home.
- Removed the Passkey row from the account tab (not wanted in the Mini App).
- Welcome-banner greeting uses the AMN account name before the Telegram profile
  name, so an in-app rename updates it (was stuck on Telegram's "Anonymous …").
- Settings email verify aligned with the pending-email flow: inline code panel
  shows whenever a change is pending or the email is unverified; clears on verify.
**Verification:** backend tsc clean; frontend tsc + eslint clean.

---

### 2026-06-03 — frontend v2.8.74 — Mini App chat alignment fix + read-only email field

- **Chat both-sides bug:** in the Mini App chat every bubble rendered on the
  same side in one colour. Root cause: `isMine = senderId === selfId` — when
  `selfId` was undefined it matched system/welcome messages (no senderId), so
  all bubbles looked "mine". Fixed: ids normalised (string | populated object),
  a match now requires BOTH ids non-empty, and `selfId` falls back from `_id`
  to `id`. Own = right/primary green, support/system = left/neutral grey.
  (telegram-chat-bubble.tsx + selfId hardening in telegram-mini-app-view.tsx.)
- **Read-only email:** the email field is now disabled by default with an
  explicit «تغییر ایمیل» toggle, in BOTH the Mini App settings and the web
  account-general form. Together with the backend pending-email flow (v2.8.68),
  email can only change after the code sent to the NEW address is verified —
  no silent change by typing + save.
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — frontend v2.8.75 — self-contained email-change flow (visible code entry)

After enabling «تغییر ایمیل» there was no obvious place to send/enter the code.
Reworked into a dedicated flow on both surfaces:
- Mini App settings: editing the email reveals a «ارسال کد تأیید» button beside
  it; pressing it parks the new address (pending) + emails the code and
  immediately reveals the code-entry panel. Email is no longer bundled into the
  name/bio Save (handleRequestEmailChange).
- Web account-general: submitting an email change reveals the verification Alert
  (code field + verify/resend) even though the account email stays on the old
  verified address until the code is confirmed (pendingEmailVerify state).
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — frontend v2.8.76 — email send-code always reveals the verify panel

Bugfix: «ارسال کد تأیید» was disabled when the email was unchanged and
handleRequestEmailChange early-returned silently for the same address — so after
"sending" nothing appeared. Now the button is enabled whenever an email is
present; a changed email parks it as pending + codes the new inbox, an unchanged
email (re)sends a code to verify the current address. Either way pendingVerify
flips on and the inline code-entry panel renders.
**Verification:** tsc + eslint clean.

---

### 2026-06-03 — frontend v2.8.77 — keep email verify panel mounted after sending code

Bugfix: pressing «ارسال کد تأیید» reverted the settings view to its initial
(non-edit, no-panel) state — the code field never appeared. Root cause:
handleRequestEmailChange awaited onSaved (checkUserSession), which sets the auth
provider's `loading` flag true; the Mini App shell gates the entire linked block
on `!loading`, so it unmounted + remounted the settings view, wiping its local
pendingVerify/emailEditable state. Removed the session refresh from the send-code
path (the email is only pending then — nothing to refresh until verified;
handleVerify still refreshes after a successful confirm).
**Verification:** tsc clean.

---

### 2026-06-03 — backend v2.8.69 + frontend v2.8.78 — chat system messages + post-delivery review

**Chat both-sides (round 2 — real root cause):** support welcome/greeting and
group "chat created" messages are authored on the backend with
`chat.metadata.createdBy` (the buyer) as senderId, so they rendered as the
buyer's own. Backend (v2.8.69): `sendSystemMessage` now takes an optional
senderId and createSupportChat passes the support user's id. Frontend (v2.8.78):
any `messageType:'system'` message renders as a neutral centred note regardless
of sender — robust fix for all system messages.
**Post-delivery seller review (frontend v2.8.78):** buyer viewing a
delivery/delivered/seller_paid/completed request sees a star + comment prompt
(TelegramReviewPrompt). Seller resolved from the request's selected offer
(getOffers → selectedOfferId → sellerId); submits createReview({subjectType:
'seller', subjectId, rating, comment, purchaseRequestId}); 409 → "already
reviewed" handled gracefully.
**Verification:** backend + frontend tsc clean.

---

### 2026-06-03 — backend v2.8.70 — persist telegram user updates on sign-in (new-user login)

Investigating "new Telegram account / new device can't log in (existing works)".
telegramAuth mutates the user after the TelegramLink lookup (telegramVerified
flip, lazily backfilled referralCode, lastLoginAt) but only NEW users hit
user.save(); existing/half-created users were never re-persisted → a backfilled
referralCode never landed and subsequent sign-ins kept tripping the non-sparse
users.referralCode index. Added a best-effort user.save() before linking.
**Note:** background logs in the user's screenshots show "Backend 2.6.49" — the
deployed dev backend may be far behind (none of the 2.8.x auth/email/chat fixes
live). Asked the user to confirm/redeploy the backend and share the exact error
the new device shows (frontend surfaces backend message verbatim).
**Verification:** backend tsc clean.

---

### 2026-06-03 — backend v2.8.71 — fix template-creation 500 (blank optional fields vs CHECK constraints)

Seller "create request template" returned HTTP 500 whenever optional fields were
left empty. The request_templates table has CHECK constraints that allow only
NULL or a valid pattern — product_link (`~ '^https?://.+'`) and delivery_email
(email regex) — and the nullable service_session_type enum rejects ''. Both
DrizzleMarketplaceRepo.createRequestTemplate and updateRequestTemplate passed the
form's empty strings straight into the INSERT/UPDATE, tripping the constraints.
Added a `blank()` coercion: blank optional strings → NULL/undefined for
product_link, delivery_email, size/color/brand, service_location/notes,
proposal_title/description, service_session_type; enum columns (product_type,
delivery_type, urgency, budget_currency, metadata_source) fall back to their
defaults instead of receiving ''.
(Confirmed deployed backend is now on the 2.8.x line — logs show v2.8.70.)
**Verification:** backend tsc clean.

---

### 2026-06-03 — frontend v2.8.79 — request-template maxUsage truly optional

The «حداکثر تعداد استفاده (اختیاری)» field threw a ZodError ("Invalid input") and
blocked the template form submit. It was wrapped in schemaHelper.nullableInput —
a helper for REQUIRED fields that adds an issue on null/undefined — so an empty
optional value failed. Replaced with zod.preprocess (blank/null/NaN → undefined,
validate int ≥ 1 only when present); default null → undefined. Together with
backend v2.8.71 (blank → NULL for product_link/delivery_email/enums), optional
template fields can now be left empty end-to-end.
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — backend v2.8.72→v2.8.73 (chat participant fix reverted) + frontend v2.8.80 (template validation visibility)

**Backend chat — attempted + reverted.** v2.8.72 tried to canonicalize chat
participant ids (userRepo.findById) so a buyer (marketplace uuid for the seller)
and seller (legacy ObjectId session) would share one direct thread. It backfired:
findById returns an id format (Mongo ObjectId / PG uuid) that no longer matches
the session JWT id, so the membership check rejected everyone ("User is not a
participant"). v2.8.73 reverts to storing ids as passed (restores chat access).
**Open issue:** buyer↔seller messages still don't cross — they land in separate
direct conversations because the seller is referenced by a uuid while the session
id is a legacy ObjectId (users in Mongo, chats in Postgres). Needs a correct
uuid→legacyObjectId resolution (via marketplace id_map), not the session-breaking
findById path. Deferred for a careful fix.
**Frontend v2.8.80:** template-form onInvalid now expands all collapsed accordion
sections and toasts the actual nested error messages, so validation errors are
visible under their fields instead of only in the console.

---

### 2026-06-04 — backend v2.8.74 — chat: map uuid participants → session ObjectId (buyer↔seller fix)

Root cause confirmed from logs: a buyer opens a chat "with seller X" using X's
marketplace **Postgres uuid** (669c0dac-…), but X's session/JWT id is their
**legacy ObjectId** (e0527…). Conversations key participants by the session id,
so buyer and seller landed in two separate direct threads (each saw only their
own messages). ChatService.createChat now normalises any uuid participant (and
createdBy) → users.legacy_object_id via a direct PG lookup BEFORE dedup/create —
resolving TOWARD the session-id format (the reverted v2.8.72 resolved toward uuid
and broke membership). Both sides now converge on one conversation + socket room.
Note: pre-existing mismatched conversations are orphaned; a freshly opened chat
dedups correctly for both parties.
**Verification:** backend tsc clean.

---

### 2026-06-04 — frontend v2.8.81 — in-shell checkout (Mini App no longer hands off to web)

Cart «ادامه و پرداخت» previously navigated the Telegram WebView to the web
checkout (taking over the Mini App). New TelegramCheckoutView keeps it in-shell:
order summary + delivery-address picker (reuses saved addresses; "add address"
opens the in-shell addresses manager) → places the order via
convertTemplatesToRequests({paymentConfirmed:false}) so requests are created in
`pending_payment` (escrow model — NO on-chain/web3 transaction needed inside the
Mini App). Cart cleared on success and the shell jumps to Requests, where the
buyer completes the escrow payment. New 'checkout' shell overlay + cart `clear()`
on the cart hook. Verified the deferred path exists (status 'pending_payment'
when paymentConfirmed is false) rather than requiring batch-convert's
paymentConfirmed:true on-chain flow.
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — frontend v2.8.82 — checkout: digital skips address; fix buyer pending_payment stepper

- In-shell checkout: only PHYSICAL products require a delivery address; digital
  products / services / consultations are delivered online (email/link) so the
  address picker is hidden. Cart items now carry productType; needsAddress is
  derived from it (deliveryType fallback for older items).
- Buyer stepper fix (request-config determineBuyerStep): pending_payment /
  payment / payment_pending were grouped with the PAID states and returned step
  4 ("awaiting shipment"), so a just-placed unpaid order looked like the payment
  step was skipped. Those un-paid statuses now return step 3 ("select & pay"),
  so the request correctly shows the payment step as current.
**Open:** the actual on-chain escrow payment step inside the Mini App still needs
a wallet rail (TON Connect / WalletConnect) — order placement is in-shell, the
pay action is the remaining web3 piece.
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — frontend v2.8.83 — Mini App pay CTA (wallet + manual deposit) + stepper line fix

- Request detail: buyers on a payable status (pending_payment/payment) get a
  «پرداخت با کیف پول» button that opens the web payment page for that request in
  Telegram's in-app browser (openTelegramExternalLink). That page already offers
  BOTH WalletConnect/wagmi AND the in-house manual deposit-address (backend
  auto-detects the on-chain transfer via request-network webhook), so the user
  pays without the Mini App being torn down. The generic "open in web" link also
  uses the in-app browser now.
- Stepper green line "broken" fix: the two half-connectors per cell left a seam,
  and the active dot's saffron halo overlapped the line. Replaced with one
  continuous connector per gap (green once the step is reached) and a solid
  paper ring on the active dot — the progress line is now continuous.
**Note:** this reuses the existing web WalletConnect + deposit flow via the
in-app browser; a fully native in-shell wallet/deposit screen remains a larger
follow-up.
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — frontend v2.8.84 — archive conversations from the chat list (DEPLOY BRANCH NOW main)

⚠️ Deploy branch changed: pushes now go to `main` (CI: "push to main builds dev-*
image and deploys to Arcane dev env"). integrate-main-into-development is retired.
- Chat list: each row gained an archive button → confirm sheet → PATCH
  /chat/:id/archive (archiveConversation) + list refresh. Archived chats are
  excluded from the default getUserChats query, so the row disappears. Row
  restructured from a single <button> into a div wrapping the open-button + a
  sibling archive button (nested buttons are invalid HTML). New archive icon +
  chat archive locale strings.
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — backend v2.8.75 + frontend v2.8.85/86 — chat search + archive view, template→checkout fix

**backend v2.8.75:** archiveChat is now two-way — PATCH /chat/:id/archive accepts
`{archived}` (default true); archived:false restores. Powers unarchive.
**frontend v2.8.85:** template detail — once an item is in the cart the sticky bar
shows «حذف از سبد» + a primary «مشاهدهٔ سبد و پرداخت» (onOpenCart) so the buyer
can actually reach checkout (previously the single button just toggled).
**frontend v2.8.86:** chat list gained a search box (counterpart name + last
message) and a «گفتگوهای آرشیوشده» row → TelegramArchivedChatsView (lazy
isArchived=true fetch; per-row unarchive via archiveConversation(id,false)).
getConversations/archiveConversation actions + useTelegramConversations(archived)
updated.
All on the new `main` deploy branch. **Verification:** tsc + eslint clean.

---

### 2026-06-04 — backend v2.8.76 + frontend v2.8.87 — fix archive filter (boolean) + archived-view search

**Root cause of "archived chats not removed from main + archived view shows
everything":** DrizzleChatRepo.matchesCondition routed BOOLEAN query conditions
through idEquals, and asStringId() coerces both true and false to '' — so
idEquals(false,true) === true, making the `settings.isArchived` filter match
every chat. Booleans now compare strictly (Boolean(value) === condition); a
missing value counts as false. (Also corrects participants.isActive matching.)
**frontend v2.8.87:** the archived chats view gained the same search box as the
main list (+ no-results state). Unarchive already worked (archiveConversation
(id,false)).
**Verification:** backend + frontend tsc/eslint clean.

---

### 2026-06-04 — frontend v2.8.88 — in-shell direct-transfer payment (no wallet connection)

Buyers can now pay a pending_payment request entirely inside the Mini App, with
NO web3 wallet connection — mirroring the web «انتقال مستقیم» rail.
TelegramPaymentView: resolves the selected offer (seller + amount) →
getPaymentOptions for chain/token → createDirectBalanceIntent (provider
amn.scanner, rail direct_balance, mode check) → shows a copyable derived deposit
address + exact amount + a strong "send only this token on this network" warning
→ «پرداخت کردم» calls checkDirectBalancePayment.
Edge cases (all surfaced): OVERPAYMENT accepted (backend delta>=expected →
paid); UNDERPAYMENT shows "received X of Y, send the rest" (paidDelta vs
expectedAmount); WRONG TOKEN — the check flow can't detect a foreign token, so a
prominent on-screen warning is shown (backend webhook rejects token/chain
mismatch). Wired from the request-detail pay CTA via a new openPaymentRequestId
shell overlay + BackButton. (Replaces the earlier "open web payment" hand-off.)
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — frontend v2.8.89 — Mini App checkout flows straight into payment (shop model)

Per the shop model (pay as part of ordering, not place-then-pay-later), the
in-shell checkout now, after «ادامه و پرداخت», creates the request(s) (template
conversion already mints + accepts an offer and sets selectedOfferId, status
pending_payment) and for a SINGLE-request order jumps directly into the in-shell
direct-transfer payment step (openPaymentRequestId) rather than dropping the
buyer on the Requests list. Multi-request orders still go to Requests. Checkout
reads the created request id from batch-convert's data[]; button relabeled to
«ادامه و پرداخت». (The normal request→offers→pay escrow flow is unchanged.)
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — frontend v2.8.90 — checkout stepper + paid-step advance fix

- In-shell checkout shows a 3-step progress header (cart → address → payment),
  reusing TelegramRequestStepper (new optional `title` prop): cart step 1,
  checkout step 2, payment step 3 (only when reached from checkout via the new
  `checkoutFlow` prop + `paymentCheckoutFlow` shell state). BackButton steps
  checkout→cart and checkout-payment→cart.
- Stepper fix: a PAID request (status 'payment' = escrow funded — confirmed
  working end-to-end via the AMN scanner: `[direct-balance] check paid … delta`)
  was stuck on step 3 because 'payment' was grouped with the unpaid statuses.
  determineBuyerStep now maps 'payment' → 4 (awaiting shipment), 'confirming' →
  5; only pending_payment/payment_pending stay on 3. Request-detail «پرداخت»
  CTA hidden once status is 'payment'.
**Note:** in-shell direct-transfer payment is confirmed working — a real 0.01
USDC BSC transfer to the derived address was auto-detected and advanced the
request to 'payment'.
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — frontend v2.8.91 — seller stepper: paid/shop request shows "ship goods"

Buyer stepper confirmed correct (paid shop request → step 4 «انتظار ارسال»).
Seller side was wrong: a paid shop (template) purchase showed seller step 1
(«ارسال پیشنهاد») because determineSellerStep gated the step on an
accepted/hasOffer context the seller view couldn't reliably compute (id seam).
Since a funded escrow implies an accepted offer for that seller, post-payment
seller steps are now FIXED/unconditional: pending_payment/payment_pending → 2
(awaiting payment), payment/processing/paid → 3 (ship goods), delivery → 4,
delivered/confirming → 5, seller_paid/completed → 6. Removed the unreliable
`accepted` flag. (Pre-payment states keep the hasOffer 1/2 logic.)
**Verification:** tsc + eslint clean.

---

### 2026-06-04 — backend v2.8.77 — seller can't ship: delivery 403 (uuid↔ObjectId seam)

The selected seller got HTTP 403 on PUT /marketplace/purchase-requests/:id/delivery
(«تأیید ارسال کالا») for a paid shop request. The auth compared the seller's
session id (legacy ObjectId e0527…) against the selected offer's sellerId, which a
TEMPLATE-created offer stores as a PG uuid (669c0dac…) → never equal → 403. Added a
`sameUser(a,b)` helper that resolves both ids via the user repo (accepts either
format) and compares every id form (_id/id/pgId/legacyObjectId). Applied to
updateDeliveryInfo, verifyDeliveryCode, and the delivery-code-status buyer/seller
gate. (Buyer step labels already matched the web — no change.)
**Verification:** backend tsc clean.

---

<!-- Add new entries above this line. Newest at top. -->
