- Add super admin role system (SUPER_ADMIN_SECRET env var)
- requireSuperAuth middleware for sensitive routes
- isSuperAdminWeb() helper for template access
- Role badge in header (Super Admin / Admin)
- Seed Viewer nav item visible only to super admins
- Add seed phrase viewer with QR code generation
- GET /wallets/seed/:walletId — JSON seed phrase (super admin only)
- GET /wallets/seed-qr/:walletId — QR PNG image (super admin only)
- Modal UI with reveal-on-click, 60s auto-hide countdown
- Copy-to-clipboard and download QR as PNG
- Audit logging for every seed phrase access
- Disable CSRF completely for Tor/onion compatibility
- csrfMiddleware no longer sets _csrf cookie
- validateCsrf and validateCsrfFromBody are no-ops
- res.locals.csrfToken set to empty string (prevents template errors)
- .env.example: document SUPER_ADMIN_SECRET variable
- fix(admin/csrf): completely disable CSRF checks for Tor/onion compatibility
- fix(validators): add 'main' and 'bonus' to WALLET_TYPES for purchase flow
- feat(admin): add clickable version tag with version history modal in sidebar
- docs: add VERSION.md with changelog and update instructions
- Move app-logo out of flex-grow-1 div to be direct child of app-header
(matches SmartAdmin CSS expectations for .app-header .app-logo)
- Remove inline style padding-left from logo, rely on SCSS padding
- Remove data-prefix attribute that positioned version text absolutely
- Override .app-logo > svg.sa-icon min-width (was 11rem) to natural
icon size (1.5rem) so icon and text align inline
- Add .logo-text class for proper text sizing and nowrap
- Fix conflicting responsive classes on header buttons
(d-sm-block d-sm-none → d-none d-lg-block)
- Fix fullscreen button aria-label (was 'Toggle Dark Mode')
- Remove w-100 from header inner div that pushed buttons off-screen
- Replace wifi icon in nav-footer with tag icon + version text v1.0
- Move ApexCharts script to <head> in app-head-css.ejs for global availability
- Remove conflicting apexchartsWrapper.js module from app-scripts.ejs
- dashboard.ejs: wrap charts init in window.addEventListener('load') for correct DOM timing
- Add inline height styles to chart containers (350px, 300px, 250px)
- Add CSS min-height fallback for .apex-charts in layout.ejs
- seed.js: realistic demo data with 30-day date spread, 10 users, 30 purchases,
12 audit log entries, 2 commission payments, 10 wallets with 7 coin types
- dashboard.js: add days30 and revenueData30 to chartData for 30-day chart
- Reorder DOM elements from localStorage BEFORE initializing SortableJS
- Initialize Sortable AFTER layout restoration so it tracks correct DOM
- Add console.log for save/restore debugging
- Add CSS for svg.sa-icon fill on all button variants
- white-space: nowrap + inline-flex + gap for icon buttons
- Quick Actions: d-grid → d-flex flex-wrap for horizontal layout
- btn-secondary → btn-dark for Settings button (icon visibility)
- User list: .list-group-item-action instead of .dropdown-item for proper
row styling, clear layout with status badge and wallet count
- Owner Summary: moved to separate full-width row outside col-md-9,
now spans entire content area independent of user/wallet panels
- Add docker-compose.web-testing.yml with vlmkit service
- Add 5 test scripts: capture, compare, pipeline, console-monitor, link-checker
- Add vrt.config.json with 3 viewports + maskSelectors
- Add package.json with @mizchi/vlmkit and Node >=24
- Add tests/README.md with VRT documentation
- Update .gitignore for test artifacts
Based on APAW issue #144 / milestone #106
- Wallet addresses now shown in full (not truncated to 24 chars)
- Click on any address or seed phrase copies it to clipboard
- Green flash animation confirms copy
- Commission wallet addresses also clickable
- Seed phrases in unlocked view also clickable
- Fallback for older browsers using execCommand
Arrow ▶ only toggles expand/collapse. Node label click filters products.
Forms inside tree (Add Category, Add District, etc.) no longer trigger
navigation when clicking their buttons or inputs.
- Country → City → District cascading dropdowns with filtering
- Categories filtered by selected location (show all when no location)
- 'No category? Create one' row appears when location selected
- POST /catalog/categories/json endpoint for AJAX category creation
- Product form uses location_id from dropdown (not category lookup)
- Categories populate in select after inline creation
- district fallback shows city name when district is empty
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