All POST/action routes in connections.py were using get_current_user,
which returns the real logged-in admin instead of the impersonated user.
Disconnect, test, save and manual token routes now all operate on the
viewed user so admin impersonation works correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a connection row exists for the user but with a different/null
evotor_user_id, the lookup by evotor_user_id alone missed it and tried
to INSERT, hitting the unique constraint on user_id. Now looks up by
either evotor_user_id or user_id, and always syncs both fields on update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Accept phone as an alternative to username for user lookup
- On first auth when user has no password set, save the provided
password and activate the account (same logic as /user/create)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Evotor sends a password in the payload, hash and store it
immediately and set the user to active — skipping the invite flow.
Without a password, behaviour is unchanged (pending status + invite email).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
userId is nested inside body.data, and event types are
ApplicationInstalled / ApplicationUninstalled (not install/uninstall).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Evotor sends user fields (email, phone_number, first_name, last_name)
at the top level of the webhook body, not inside customField. Now we
check both locations with top-level taking precedence. Also store the
full body in evotor_meta instead of just the customField subset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add POST /user/install endpoint handling Evotor app install/uninstall
events: uninstall suspends the user and marks connection offline;
reinstall reactivates a suspended account
- Exclude admin and system role users from refresh_catalog,
refresh_vk_catalog, and mirror_to_vk periodic tasks by joining users
table and filtering role = 'user'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Catalog, VK catalog, and VK sync tasks were querying all connections
regardless of user role. Admin and system accounts with stored tokens
were generating unnecessary Evotor and VK API calls. Now all three
tasks join to the users table and filter role = 'user' only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Orange #FF5500 rounded-square with shopping-bag icon matching the
sidebar logo. Includes SVG (modern), ICO (16+32px, legacy), and
apple-touch-icon PNG.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Used novalidate + server-side validation so errors appear in the dialog
rather than as browser-native popups. Form fields retain submitted values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a dialog form on the users list. Validates email uniqueness,
password length. Creates user as active with confirmed email.
System role can assign admin role; admin role can only create users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Connections/Catalog/VK/Sync nav links only shown for regular users or when
admin is viewing as a user. Admin/system users land on /admin/users after login.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove /register route and nav links (users created via Evotor webhook)
- Fix evotor_webhooks.py: use phone=None instead of phone="" to avoid unique constraint
- Add admin "view as user" feature: POST /admin/users/{id}/view-as sets viewed_user_id
in session; POST /admin/view-as/stop clears it
- catalog, vk_catalog, sync, connections GET routes use get_viewed_user() so admins
see another user's data while browsing
- Orange banner shown at top when admin is viewing as another user
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added store_filters_seeded / group_filters_seeded flags to SyncConfig.
_enabled_*_ids now returns None (all enabled) only before first toggle,
not when the filter table is empty due to all being disabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phone is optional — admin users created via script don't have one.
Added migration 0010 to alter the column, updated create_admin.py to
pass None instead of empty string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nginx/nginx.conf is symlinked directly into system nginx config.
No need for a per-domain generate script or template — edit the file,
run nginx -t && systemctl reload nginx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Simpler than auto-converting: just pass xn----8sbfwtmcso8g.xn--p1ai directly.
Updated usage comments in both scripts to reflect this.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xn--e1afmapc4af.xn--p1af was wrong; correct punycode is xn----8sbfwtmcso8g.xn--p1ai.
generate-nginx-conf.sh now converts IDN domains to punycode before expanding the
template, so cert paths and server_name directives are always ASCII-safe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MariaDB: limit innodb buffer pool to 128M, max 20 connections
- Celery worker: concurrency 2→1
- Flower: moved to 'flower' profile (opt-in, not started by default)
Start with: docker compose --profile flower up -d flower
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- nginx/nginx.conf: pre-generated config for both domains (IDN punycode for .рф)
- scripts/generate-nginx-conf.sh: generates sites-available config from template per domain
- scripts/init-letsencrypt.sh: accepts domain as arg (falls back to .env)
- README.md: updated deploy section, removed stale VK_WEIGHT_PRICE_MULTIPLIER, added sync/logs routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds evo_mirror_enabled and vk_mirror_enabled flags to SyncConfig.
Each of the three background tasks (Зеркало Эвотор / Зеркало ВК /
Синхронизация) can now be enabled independently from the /sync page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only the create path checked allow_to_sell. The update path
kept syncing disabled products indefinitely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each product's description is now built as "Name (цена за M.)" using its
own measure_name. The global description_postfix setting is removed —
it couldn't handle per-product units.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds price_multiplier and description_postfix to SyncConfig. The sync
page at /sync lets users configure them. vk_sync reads these per-user
settings and applies the multiplier to price and appends the postfix
as "(postfix)" to the VK product description.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VK Market API expects the price field in rubles (float), not kopecks.
Removing the *100 conversion that was inflating all prices by 100x.
Comparison with cached VK prices now also uses rubles consistently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add api_logs table (migration 0007) and ApiLog model
- Add web/lib/api_logger.py — httpx wrapper that records every outbound call
- Wire api_logger into vk_sync, vk_catalog, and connections test endpoints
- Add /admin/logs page with filters (service, method, status, time range, URL search) and expandable request/response detail
- Add "Логи" nav link for admin users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After refresh_vk_catalog syncs the album list from VK, remove any
VkCachedAlbum rows whose album_id was not returned by the API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VK API v5.199 requires item_id not item_ids for market.removeFromAlbum.
The wrong parameter name caused silent failures — products were not
actually removed from their old album when moved to a new one.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- catalog.py: include parent_id as fallback for group_evotor_id (Evotor API returns parent_id instead of group/parentUuid)
- vk_sync.py: on product update, detect album change and call market.removeFromAlbum + market.addToAlbum to move product to the correct album
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add VK OAuth implicit flow: /vk-auth redirect, /vk-callback JS page,
/vk-callback/save endpoint with state validation
- Add VK_CLIENT_ID/VK_CLIENT_SECRET to config
- Add refresh_token/token_expires_at columns to vk_connections (migration 0006)
- Fix vk_catalog task: handle price/thumb_photo as string or dict (VK API v5.199)
- Fix connections/vk/test: use groups.getById instead of market.getAlbums
(works with both user and group tokens)
- Add orphan deletion to mirror_to_vk: VK products not in Evotor are removed
- Handle ungrouped Evotor products: push to "Без категории" VK album
- Respect SyncConfig.is_enabled in mirror_to_vk
- Add product count column to catalog groups page
- Add group name column to catalog products page
- Expand test suite: 73 new tests covering connections routes, catalog routes,
vk_sync task logic, and catalog task helpers (138 total, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded evosync.ru with a DOMAIN variable read from .env.
nginx.conf is now generated from nginx.conf.template via envsubst;
init-letsencrypt.sh reads DOMAIN from .env and fails loudly if unset.
README documents the new variable and first-deploy TLS workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Evotor catalog: background Celery task syncing stores/groups/products
from Evotor API; UI pages with per-store and per-group sync toggles
- VK connection: manual token + group ID entry with inline test button
- Evotor connection: inline test button (calls /stores)
- VK catalog: background task syncing VK Market albums and products;
separate catalog UI at /vk-catalog/albums
- SyncFilter extended to support entity_type=group with parent_entity_id
- Migration 0004: vk_cached_albums + vk_cached_products tables
- Beat schedule updated to run both refresh_catalog and refresh_vk_catalog
- README updated with new schema, routes, tasks, and config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also add itsdangerous to requirements.txt (missing implicit dep of
starlette SessionMiddleware) and a create_admin.py script for
bootstrapping a system-role user with all permissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>