Prints aggregate counts only, prefixed [DIAG], right after migrations: customers
by kind/active/archived/step, what the omde list can show and how many rows are
sales-expert trading accounts (created vs adopted), experts' profile/trading
state, referral counts, the Admin table and the last applied migrations.
Read-only, no mobile/name/id is printed, each query isolated so a failure is
reported without stopping boot. Disable with DIAG_LOG=false; remove once the
empty-omde-list investigation is closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PrismaExceptionFilter returned "Unique constraint failed on: mobile" in
English for a duplicate. It now answers in Persian, naming the field
(mobile, national code, member code, order number), with a safe generic
fallback. Covered by a unit test.
- The admin panel still shipped the old "NZ" favicon. favicon.png/.ico are now
generated from the project logo (the same one the user apps use), in both
public/ and dist/, and the links carry ?v=2 so browsers drop the cached icon.
- Rebuilt apps/api/release/app.js.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
AdminUser held both panel staff and sales experts, so one mobile could not be
both. Staff now live in "Admin" (panel only); sales experts stay in "AdminUser"
untouched. Uniqueness is per table.
- migration 20261026120000_split_admin_table: additive only. Creates "Admin",
copies non-expert rows with their ids, re-points the "who did this" foreign
keys (ledger, order status, sms, notification, upload, allocation, workshop,
admin session) to "Admin" without moving data. Never writes to "AdminUser".
- login, JWT, SSO, branch/workshop supervisors and the users grid use "Admin";
SALES_EXPERT create/edit still goes through admin/users into "AdminUser".
- an admin cannot be turned into an expert or the reverse (different tables).
- manual/: optional phase-2 script to drop the dormant admin copies from
"AdminUser", and a read-only expert-login diagnostic.
- docs/ADMIN-TABLE-SPLIT.md: what changed, deploy checklist, rollback.
Tested on a copy with 12,000 experts: expert rows byte-identical (md5) before
and after; full migration chain applies and matches schema.prisma.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Migrations were applied in plain name order, so 0002_admin_schema.sql ran
before 20260910113720_first and failed with relation "City" does not exist.
sortMigrations now always puts the baseline first; existing databases are
unaffected because their applied names are unchanged.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
A production API with incomplete config threw before listening, so the
container restarted forever and every /api request through nginx was a 502.
The problems are now printed as one loud warning and each gets a safe
fallback:
- JWT_SECRET / JWT_CUSTOMER_SECRET missing, weak or equal: a random key is
generated once and kept in the Setting table (boot-secrets.ts), so tokens
survive restarts. A strong value in the environment always wins.
JwtModule now uses registerAsync so it reads the key after boot fills it.
- OTP_MASTER_CODE: ignored in production unless
TEST_SERVER_ALLOW_MASTER_CODE=yes.
- CORS_ORIGINS: invalid entries dropped; empty allows every origin.
Tests: boot-secrets.spec, production-checks.spec. 118 tests pass.
سرور با تنظیمات ناقص دیگر مدام ریست نمیشود و ورود دوباره کار میکند.
Co-authored-by: Cursor <cursoragent@cursor.com>
The folder-discovery fix in the previous commit surfaced a second problem.
Databases migrated BEFORE that fix recorded the baseline under the name of
the hand-added flat duplicate that used to sit beside it
(prisma/migrations/0001_init.sql). Once the runner started discovering the
real folder (20260910113720_first), it saw a name it had never recorded and
tried to apply the baseline again — against a database that already had it:
Raw query failed. Code: 42710.
ERROR: type "AdminRole" already exists
The two files are identical SQL (verified byte-for-byte after normalising
line endings; they differ only by one trailing blank line), so the recorded
legacy name genuinely means that migration is applied. A LEGACY_NAME_ALIASES
map now treats '0001_init.sql' as an already-applied marker for
'20260910113720_first', so upgrading an existing database is a no-op instead
of a crash.
Verified against the local dev database this actually broke on
(postgresql://localhost:5432/gold_banking, which had recorded only the old
flat name): 'npm run dev' in apps/api now logs 'No pending migrations' and
'Nest application successfully started' instead of dying. That database's
tracking table was also reconciled to record both real migration names, and
it was confirmed to already contain both migrations' schema (AdminRole,
City.percent, user_type, CustomerKind all present) — nothing was re-run
against it. The dockerised database is unaffected: it already records the
real names, so the alias is inert there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three real bugs found and fixed while wiring up product images end-to-end:
1. src/prisma/migrate.ts only discovered flat prisma/migrations/<name>.sql
files, never Prisma's own folder-per-migration layout
(<timestamp>_<name>/migration.sql). Every migration created the normal
way (via 'prisma migrate dev') was silently skipped; only a hand-added
flat duplicate (prisma/migrations/0001_init.sql, now removed) ever ran.
This meant schema.prisma had drifted well ahead of what any real deploy
database actually had (missing columns on City, Customer, Branch,
Product, several new tables). Fixed to discover both layouts; the
drift is closed with a new 20260914165255_sync_schema_drift migration.
2. Neither nginx.conf (production) nor vite.config.ts (dev) proxied
/uploads/ to the API — only /api/. UploadedFile/ProductImage URLs
(/uploads/<file>, served by main.ts's useStaticAssets OUTSIDE the /api
prefix) 404'd through both. Both now proxy /uploads/ the same way /api/
already was.
3. docker-compose.yml's api service had no volume for storage/uploads, so
every uploaded file — and by extension every ProductImage row pointing
at one — was lost on the next container recreate/redeploy. Added a
named volume.
Also: main.ts's static file serving now sets Content-Type: image/avif
explicitly (serve-static's bundled mime table predates .avif and served it
as application/octet-stream; browsers rendered it anyway by sniffing, but
a stricter client would not have).
Product photos: copied 15 real jewelry photos into the uploads volume,
registered them as UploadedFile rows, and assigned each of the 5 existing
Product rows a randomly picked one (duplicates allowed, per request),
replacing their /img/no-items.png placeholder ProductImage row. Verified
end-to-end in a real browser against the real containerized API+DB: logged
in, opened the admin product list, confirmed all 5 rows render their photo
(naturalWidth/Height populated, network 200s, no console errors).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>