feat: release v1.8.0 — connections dashboard, VK OAuth, sync config, catalog browser

- Connections dashboard with add/remove flow and background health checks
- VK OAuth connection with profile info and health monitoring
- Sync configuration page with master toggle and filter summary
- Catalog browser with store/group/product tables, filter management, CSV export
- Alembic migrations for all new tables
- run/read_config.py for shell sync script DB integration
- CHANGELOG.md updated for v1.8.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mguschin
2026-03-06 16:08:19 +03:00
parent cfc7229daf
commit 9aeef73b10
20 changed files with 2010 additions and 85 deletions

102
web/routes/sync.py Normal file
View File

@@ -0,0 +1,102 @@
from datetime import datetime
from fastapi import APIRouter, Request, Depends
from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from web.auth import get_current_user
from web.database import get_db
from web.models import User, EvotorConnection, VkConnection, SyncConfig, SyncFilter
router = APIRouter(prefix="/sync")
templates = Jinja2Templates(directory="web/templates")
def _get_or_create_sync_config(db: Session, user_id: int) -> SyncConfig:
config = db.query(SyncConfig).filter(SyncConfig.user_id == user_id).first()
if not config:
config = SyncConfig(user_id=user_id, is_enabled=False)
db.add(config)
db.commit()
db.refresh(config)
return config
def _filter_summary(config: SyncConfig) -> dict:
stores = [f for f in config.filters if f.entity_type == "store"]
groups = [f for f in config.filters if f.entity_type == "group"]
products = [f for f in config.filters if f.entity_type == "product"]
return {
"stores": len(stores),
"groups": len(groups),
"products": len(products),
"total": len(config.filters),
}
@router.get("")
def sync_page(
request: Request,
db: Session = Depends(get_db),
user: User | None = Depends(get_current_user),
):
if not user:
return RedirectResponse("/login", 303)
evotor = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first()
vk = db.query(VkConnection).filter(VkConnection.user_id == user.id).first()
config = _get_or_create_sync_config(db, user.id)
summary = _filter_summary(config)
if config.confirmed_at and config.is_enabled:
status = "active"
elif config.confirmed_at and not config.is_enabled:
status = "paused"
elif summary["total"] > 0:
status = "pending"
else:
status = "unconfigured"
return templates.TemplateResponse("sync.html", {
"request": request,
"user": user,
"evotor": evotor,
"vk": vk,
"config": config,
"summary": summary,
"status": status,
})
@router.post("/toggle")
def sync_toggle(
request: Request,
db: Session = Depends(get_db),
user: User | None = Depends(get_current_user),
):
if not user:
return RedirectResponse("/login", 303)
config = _get_or_create_sync_config(db, user.id)
config.is_enabled = not config.is_enabled
db.commit()
return RedirectResponse("/sync", 303)
@router.post("/confirm")
def sync_confirm(
request: Request,
db: Session = Depends(get_db),
user: User | None = Depends(get_current_user),
):
if not user:
return RedirectResponse("/login", 303)
config = _get_or_create_sync_config(db, user.id)
if config.is_enabled and len(config.filters) > 0:
config.confirmed_at = datetime.utcnow()
db.commit()
return RedirectResponse("/sync", 303)