feat: per-task on/off switches on /sync page for staged rollout

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>
This commit is contained in:
mguschin
2026-05-12 23:32:02 +03:00
parent 5c2b501749
commit 75b3872170
6 changed files with 94 additions and 19 deletions

View File

@@ -0,0 +1,18 @@
"""Add evo_mirror_enabled and vk_mirror_enabled to sync_configs."""
revision = "0009"
down_revision = "0008"
branch_labels = None
depends_on = None
import sqlalchemy as sa
from alembic import op
def upgrade():
op.add_column("sync_configs", sa.Column("evo_mirror_enabled", sa.Boolean, nullable=False, server_default="0"))
op.add_column("sync_configs", sa.Column("vk_mirror_enabled", sa.Boolean, nullable=False, server_default="0"))
def downgrade():
op.drop_column("sync_configs", "vk_mirror_enabled")
op.drop_column("sync_configs", "evo_mirror_enabled")

View File

@@ -57,6 +57,8 @@ class SyncConfig(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
is_enabled = Column(Boolean, nullable=False, default=False) is_enabled = Column(Boolean, nullable=False, default=False)
evo_mirror_enabled = Column(Boolean, nullable=False, default=False)
vk_mirror_enabled = Column(Boolean, nullable=False, default=False)
confirmed_at = Column(DateTime, nullable=True) confirmed_at = Column(DateTime, nullable=True)
price_multiplier = Column(Numeric(10, 4), nullable=False, default=1.0) price_multiplier = Column(Numeric(10, 4), nullable=False, default=1.0)
created_at = Column(DateTime, nullable=False, server_default=func.now()) created_at = Column(DateTime, nullable=False, server_default=func.now())

View File

@@ -3,7 +3,6 @@ from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from web.auth.session import get_current_user from web.auth.session import get_current_user
@@ -21,10 +20,6 @@ def _render(request: Request, ctx: dict):
return templates.TemplateResponse(ctx.pop("request"), "sync.html", ctx) return templates.TemplateResponse(ctx.pop("request"), "sync.html", ctx)
def _now():
return datetime.now(timezone.utc).replace(tzinfo=None)
@router.get("/sync") @router.get("/sync")
async def sync_get(request: Request, db: Session = Depends(get_db)): async def sync_get(request: Request, db: Session = Depends(get_db)):
try: try:
@@ -48,8 +43,12 @@ async def sync_settings_post(request: Request, db: Session = Depends(get_db)):
return RedirectResponse("/login", 303) return RedirectResponse("/login", 303)
form = await request.form() form = await request.form()
raw_multiplier = str(form.get("price_multiplier", "1")).strip()
evo_mirror_enabled = form.get("evo_mirror_enabled") == "1"
vk_mirror_enabled = form.get("vk_mirror_enabled") == "1"
sync_enabled = form.get("is_enabled") == "1"
raw_multiplier = str(form.get("price_multiplier", "1")).strip()
try: try:
multiplier = float(raw_multiplier) multiplier = float(raw_multiplier)
if multiplier <= 0: if multiplier <= 0:
@@ -59,11 +58,16 @@ async def sync_settings_post(request: Request, db: Session = Depends(get_db)):
config = db.query(SyncConfig).filter_by(user_id=user.id).first() config = db.query(SyncConfig).filter_by(user_id=user.id).first()
if config: if config:
config.evo_mirror_enabled = evo_mirror_enabled
config.vk_mirror_enabled = vk_mirror_enabled
config.is_enabled = sync_enabled
config.price_multiplier = multiplier config.price_multiplier = multiplier
else: else:
config = SyncConfig( config = SyncConfig(
user_id=user.id, user_id=user.id,
is_enabled=False, evo_mirror_enabled=evo_mirror_enabled,
vk_mirror_enabled=vk_mirror_enabled,
is_enabled=sync_enabled,
price_multiplier=multiplier, price_multiplier=multiplier,
) )
db.add(config) db.add(config)

View File

@@ -215,6 +215,9 @@ def refresh_catalog(self) -> dict:
.all() .all()
) )
for conn in connections: for conn in connections:
cfg = db.query(SyncConfig).filter_by(user_id=conn.user_id).first()
if not cfg or not cfg.evo_mirror_enabled:
continue
try: try:
_sync_user(db, conn.user_id, conn.access_token) _sync_user(db, conn.user_id, conn.access_token)
results["ok"] += 1 results["ok"] += 1

View File

@@ -10,7 +10,7 @@ from celery import shared_task
import web.lib.api_logger as api_logger import web.lib.api_logger as api_logger
from web.config import settings from web.config import settings
from web.database import SessionLocal from web.database import SessionLocal
from web.models.connections import VkCachedAlbum, VkCachedProduct, VkConnection from web.models.connections import SyncConfig, VkCachedAlbum, VkCachedProduct, VkConnection
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -172,6 +172,9 @@ def refresh_vk_catalog(self) -> dict:
.all() .all()
) )
for conn in connections: for conn in connections:
cfg = db.query(SyncConfig).filter_by(user_id=conn.user_id).first()
if not cfg or not cfg.vk_mirror_enabled:
continue
try: try:
_sync_user(db, conn.user_id, conn.access_token, conn.vk_user_id) _sync_user(db, conn.user_id, conn.access_token, conn.vk_user_id)
results["ok"] += 1 results["ok"] += 1

View File

@@ -10,9 +10,52 @@
<div role="alert" class="alert alert-success"><p>Настройки сохранены.</p></div> <div role="alert" class="alert alert-success"><p>Настройки сохранены.</p></div>
{% endif %} {% endif %}
<article class="card">
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Настройки цены</h2>
<form method="post" action="/sync/settings"> <form method="post" action="/sync/settings">
<article class="card mb-3">
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Фоновые задачи</h2>
<p class="text-muted small" style="margin-bottom:1.25rem;">Включайте поочерёдно: сначала проверьте зеркало Эвотор, затем ВК, затем синхронизацию.</p>
<div style="display:flex; flex-direction:column; gap:1rem;">
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
<input type="hidden" name="evo_mirror_enabled" value="0">
<input type="checkbox" name="evo_mirror_enabled" value="1" role="switch"
{% if config and config.evo_mirror_enabled %}checked{% endif %}
style="margin-top:0.2rem; flex-shrink:0;">
<span>
<strong>Зеркало Эвотор</strong><br>
<span class="text-muted small">Периодически импортирует товары, группы и магазины из Эвотор в локальную базу.</span>
</span>
</label>
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
<input type="hidden" name="vk_mirror_enabled" value="0">
<input type="checkbox" name="vk_mirror_enabled" value="1" role="switch"
{% if config and config.vk_mirror_enabled %}checked{% endif %}
style="margin-top:0.2rem; flex-shrink:0;">
<span>
<strong>Зеркало ВК</strong><br>
<span class="text-muted small">Периодически импортирует альбомы и товары из VK Market в локальный кэш.</span>
</span>
</label>
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
<input type="hidden" name="is_enabled" value="0">
<input type="checkbox" name="is_enabled" value="1" role="switch"
{% if config and config.is_enabled %}checked{% endif %}
style="margin-top:0.2rem; flex-shrink:0;">
<span>
<strong>Синхронизация</strong><br>
<span class="text-muted small">Зеркалит каталог Эвотор в VK Market: создаёт, обновляет и удаляет товары.</span>
</span>
</label>
</div>
</article>
<article class="card mb-3">
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Настройки цены</h2>
<label style="max-width:320px;"> <label style="max-width:320px;">
Множитель цены Множитель цены
<input type="number" name="price_multiplier" step="0.0001" min="0.0001" <input type="number" name="price_multiplier" step="0.0001" min="0.0001"
@@ -20,7 +63,9 @@
placeholder="1"> placeholder="1">
<small class="text-muted">Цена из Эвотор умножается на это значение перед отправкой в ВК. По умолчанию: 1.</small> <small class="text-muted">Цена из Эвотор умножается на это значение перед отправкой в ВК. По умолчанию: 1.</small>
</label> </label>
<button type="submit" style="margin-top:1rem;">Сохранить</button>
</form>
</article> </article>
<button type="submit">Сохранить</button>
</form>
{% endblock %} {% endblock %}