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

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, UniqueConstraint, Numeric, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
@@ -23,6 +23,10 @@ class User(Base):
evotor_connection = relationship("EvotorConnection", back_populates="user", uselist=False)
vk_connection = relationship("VkConnection", back_populates="user", uselist=False)
sync_config = relationship("SyncConfig", back_populates="user", uselist=False)
cached_stores = relationship("CachedStore", back_populates="user", cascade="all, delete-orphan")
cached_groups = relationship("CachedGroup", back_populates="user", cascade="all, delete-orphan")
cached_products = relationship("CachedProduct", back_populates="user", cascade="all, delete-orphan")
class EvotorConnection(Base):
@@ -56,3 +60,96 @@ class VkConnection(Base):
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
user = relationship("User", back_populates="vk_connection")
class SyncConfig(Base):
__tablename__ = "sync_configs"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False)
is_enabled = Column(Boolean, default=False, nullable=False)
confirmed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
user = relationship("User", back_populates="sync_config")
filters = relationship("SyncFilter", back_populates="sync_config", cascade="all, delete-orphan")
class SyncFilter(Base):
__tablename__ = "sync_filters"
id = Column(Integer, primary_key=True, autoincrement=True)
sync_config_id = Column(Integer, ForeignKey("sync_configs.id", ondelete="CASCADE"), nullable=False)
entity_type = Column(String(20), nullable=False) # "store", "group", "product"
entity_id = Column(String(255), nullable=False)
entity_name = Column(String(255), nullable=True)
filter_mode = Column(String(10), nullable=False) # "include", "exclude"
parent_entity_id = Column(String(255), nullable=True)
created_at = Column(DateTime, server_default=func.now(), nullable=False)
__table_args__ = (
UniqueConstraint("sync_config_id", "entity_type", "entity_id"),
)
sync_config = relationship("SyncConfig", back_populates="filters")
class CachedStore(Base):
__tablename__ = "cached_stores"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
evotor_id = Column(String(255), nullable=False)
name = Column(String(255), nullable=False)
address = Column(String(500), nullable=True)
fetched_at = Column(DateTime, nullable=False)
__table_args__ = (
UniqueConstraint("user_id", "evotor_id"),
Index("ix_cached_stores_user_id", "user_id"),
)
user = relationship("User", back_populates="cached_stores")
class CachedGroup(Base):
__tablename__ = "cached_groups"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
evotor_id = Column(String(255), nullable=False)
store_evotor_id = Column(String(255), nullable=False)
name = Column(String(255), nullable=False)
fetched_at = Column(DateTime, nullable=False)
__table_args__ = (
UniqueConstraint("user_id", "evotor_id"),
Index("ix_cached_groups_user_store", "user_id", "store_evotor_id"),
)
user = relationship("User", back_populates="cached_groups")
class CachedProduct(Base):
__tablename__ = "cached_products"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
evotor_id = Column(String(255), nullable=False)
store_evotor_id = Column(String(255), nullable=False)
group_evotor_id = Column(String(255), nullable=True)
name = Column(String(255), nullable=False)
price = Column(Numeric(12, 2), nullable=True)
quantity = Column(Numeric(12, 3), nullable=True)
measure_name = Column(String(20), nullable=True)
article_number = Column(String(100), nullable=True)
allow_to_sell = Column(Boolean, nullable=True)
fetched_at = Column(DateTime, nullable=False)
__table_args__ = (
UniqueConstraint("user_id", "evotor_id"),
Index("ix_cached_products_user_store_group", "user_id", "store_evotor_id", "group_evotor_id"),
)
user = relationship("User", back_populates="cached_products")