The admin product form now has cascading dropdowns for Country → City → District
that filter categories by location. Previously there was no way to select location
when adding a product — only a static tag display on edit.
- catalogProduct.js: replaced static location tags with 3 cascading selects
- catalog.js: pass locations data, init JS for cascading selects + category filtering
- catalog route: pass locations array to renderCatalog
- style.css: added .pf-location-selects styling for the dropdown row
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
- bot.js: 5 retries with 5s delay on init, graceful fallback to null
- errorHandler.js: 5 retries on 404 (invalid token), stops polling after
max retries 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
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.
- Add User tor to torrc for privilege dropping
- chown /var/lib/tor to tor:nogroup before Tor starts
- chmod 755 on hostname directories so root can read them
- Remove invalid chown tor:tor (tor group doesn't exist in Alpine)
- entrypoint.sh: background process writes onion-hosts.txt with SSH_ONION and ADMIN_ONION
- docker-compose.yml: bind mount tor-proxy/hosts for onion address persistence on host
- tor-proxy/get-onions.sh: reads onion addresses and updates .env with ADMIN_URL, SSH_ONION, ADMIN_ONION
- .gitignore: exclude tor-proxy/hosts/onion-hosts.txt (secret)
- tor-proxy/hosts/.gitkeep: ensure directory exists in git
- JSON endpoint now joins locations+categories+subcategories for full info
- Edit form shows location tags (country, city, district) at top
- Location hidden when adding new product (no location yet)
- All fields properly filled by fillEditForm on edit
- Category and subcategory side by side in edit form
- Sidebar: sticky with height=100vh, search field, internal scroll for user list
- Owner Summary: full-width below user/wallet section, not inside right column
- Wallet main: normal flow scrolling, not fixed height
- Added search field at top of sidebar filtering by username or telegram ID
- Sidebar is sticky (stays in view while scrolling right panel)
- Sidebar max-height matches viewport for natural scroll with hundreds of users
- Each user item has data-name/data-tgid/data-id for instant JS filtering
- Stats section scrolls naturally below user wallets
- Seeds only unlock when lastPaidAmount >= currentCommission
- CSV export endpoint also checks commission before serving
- Button shows locked state with amount due when commission unpaid
- Prevents free access to encrypted mnemonics without payment
- Commission = 5% of total wallet balances (not sales)
- Track commission payments in commission_payments table (migration 007)
- Show 'Due Now' = current commission - last payment amount
- Record payment form with amount and optional note
- Payment history table with date, balances, commission, paid, delta
- Delta shows difference between consecutive payments (new users = more owed)
- Seed phrase unlock reminder shows the commission due amount
- Stat warning highlight when commission is due
- 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
- Add settings form with all config fields (Bot, Commission, Wallets, WireGuard)
- POST handler writes .env file and restarts container via process.exit(0)
- Secrets (ENCRYPTION_KEY, ADMIN_SECRET, GITEA_TOKEN, WG_PRIVATE_KEY, WG_PRESHARED_KEY)
are never sent to browser - masked placeholders used instead
- PRESERVE_KEYS enforced: secret keys cannot be overwritten via form
- Values sanitized: newlines stripped before writing to .env
- start.sh loads .env file before node to override Docker env_file cache
- Extract shared escapeHtml utility to escape.js (used by 6 view files)
- Update paymentWallets view to link to Settings page instead of .env
- Add .env volume mount for settings panel read/write
- Fix registerRoutes() not being called in index.js (bot menu buttons)
- New /catalog page with tree view: Location (🌍) → Category (📂) → Subcategory (📁) → Product
- Add/delete locations, categories, subcategories, products from one page
- JS-powered subcategory dropdown filtered by category
- Sticky sidebar with Add Location/Category/Product forms
- Responsive grid layout (tree + forms side by side, stacks on mobile)
- Navigation simplified: Catalog replaces separate Locations/Categories/Products
- Old routes still accessible for backward compatibility
- Subcategories table migration (006_subcategories.js)
- subcategory_id column added to products table
- Seed data includes subcategories (VPN, Accounts, Hardware, etc.)
- 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
- Removed privileged: true from docker-compose.yml
- Removed SYS_MODULE cap_add (kept NET_ADMIN for WireGuard)
- Removed source code bind mounts (./src, package.json)
- Removed wg0.conf and resolv.conf bind mounts (now generated from env)
- Added resource limits: mem_limit 512m, cpus 1.0
- Added healthcheck with curl
- Added non-root user appuser:appgroup in Dockerfile
- wg0.conf now generated from env vars at container startup (WG_PRIVATE_KEY, etc.)
- resolv.conf generated from WG_DNS env var
- Rotated wg0.conf — private key removed from file
- Added WG_ALLOWED_IPS to .env.example
SECURITY: Rotate WireGuard keys on server if previously used in production
- 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)