Critical fix for product management location selection:
- Country/city callback_data now uses pipe | as separator with
encodeURIComponent/decodeURIComponent for special chars
- District selection uses location ID (prod_loc_{id}, shop_loc_{id})
instead of underscore-delimited country_city_district text
- Empty district names now show city name as fallback
- LocationService.getLocationsByCountryAndCity() returns id+district
for building callback_data with location IDs
- All error handlers in admin product navigation use editOrSendCallback
to avoid chat clutter
- Routes updated: prod_district_ → prod_loc_, shop_district_ → shop_loc_
This fixes the bug where selecting country/city/district in admin panel
or shop failed because split('_') broke on multi-word names or empty
district values.
All callback handlers now use editOrSendCallback() to edit the existing
message in-place instead of bot.sendMessage() which creates new messages
and clutters the chat. If edit fails (message too old), the old message
is deleted and a new one sent.
Added src/utils/messageUtils.js with:
- editOrSendCallback(callbackQuery, text, options) — edit or fallback
- editOrSend(chatId, messageId, text, options) — edit or fallback
- deleteAndSend(chatId, messageId, text, options) — delete then send
Fixed handlers:
- userProductHandler: handleBuyProduct errors, handlePay validation/stock errors
- userPurchaseHandler: viewPurchase errors, handleConfirmReceived errors, handlePurchaseListPage errors
- userLocationHandler: all error paths now edit in-place
- userDeletionHandler: both error paths now edit in-place
- wallet/balanceHandler: showBalance error (text command, acceptable)
- wallet/refreshHandler: user not found and refresh errors
- wallet/topUpHandler: wallet loading error
- wallet/createHandler: invalid wallet type error
- wallet/historyHandler: both transaction history error paths
- wallet/archiveHandler: archived wallets error
sendPhoto now sends local files from /app/uploads/ instead of requiring
a publicly accessible URL. This fixes the issue where onion addresses
and private IPs are unreachable by Telegram API servers.
- resolvePhotoSource(): http URLs pass through, relative paths resolved
to local file path in uploads dir
- sendProductPhoto(): sends file directly, falls back to corrupt-photo.jpg
- Removed all ADMIN_URL prefix logic for photo URLs
- Works without any public IP or domain
- productHandler, purchaseHandler, viewHandler: prefix relative photo_url
with ADMIN_URL so Telegram can fetch images via public URL
- bot.js: 5 retries with 5s delay on init, graceful fallback to null
- errorHandler.js: 5 retries on 404 (invalid token), stops polling
but keeps process alive for admin panel
- config.js: BOT_TOKEN missing logs warning instead of process.exit
- index.js: bot handlers only registered when bot is available,
admin panel always starts regardless of bot status
- adminWalletsHandler.js: replace throw with logger.warn for missing
commission wallets (prevents container crash on startup)
- docker-compose.yml: bind admin port to all interfaces (0.0.0.0)
- README.md: updated with Tor proxy architecture, resilience docs
- install.sh: added Tor proxy status check and onion address display
App crashed on startup if COMMISSION_ENABLED=true but wallet addresses
were missing. This prevented the admin panel from starting at all.
Now logs a warning instead of crashing.
- Left sidebar: user list with ID, username, status icon, wallet count
- Right panel: selected user's balances + crypto wallet table
- Fix inverted status logic (0=Active, 1=Deleted, 2=Blocked)
- Admin bot: block/unblock toggle based on current user status
- Seed data: set active users to status=0 instead of status=1
- Toggle-status route: 0↔2 instead of 1↔0
- Dockerfile: multi-stage build (builder with python3+g++ for native addons)
- Dockerfile: wireguard-tools from edge/community repo
- Dockerfile: removed USER appuser (start.sh needs root for wg-quick)
- Dockerfile: health check on port 3000
- Added /health HTTP endpoint in index.js for Docker healthcheck
- Fixed productValidator.js: added named exports (validateProductName, validateProductPrice)
- Added better-sqlite3 as fallback dependency
- AdminHandler.isAdmin() static method delegates to middleware/auth.js
(index.js calls adminHandler.isAdmin() which needs a class method)
- adminWalletsHandler: this.exportCSV() → this.handleExportCSV(callbackQuery)
(exportCSV doesn't exist, handleExportCSV is the correct method)