WEB-APP/ Optimization/ Build fix/ Hotkey edit mode/ Log rotation/ Form a11y/ E2E non-blocking

This commit is contained in:
Vladless
2026-04-13 00:38:29 +00:00
parent c1b9b20ea3
commit c693c28ee7
270 changed files with 20933 additions and 6365 deletions
+61
View File
@@ -0,0 +1,61 @@
from ._base import Base, DictLikeMixin
from .admin import Admin, Setting
from .audit import AuditEvent
from .coupons import Coupon, CouponUsage
from .gifts import Gift, GiftUsage
from .identity import Identity
from .keys import Key
from .notifications import Notification, ScheduledBroadcast
from .payments import Payment
from .referrals import Referral
from .servers import Server, ServerSpecialgroup, ServerSubgroup
from .tariffs import Tariff
from .users import BlockedUser, ManualBan, TemporaryData, TrackingSource, User
from .web import (
WebBlock,
WebCustomElementBuild,
WebFlow,
WebFlowEvent,
WebNotification,
WebPage,
WebPageVariant,
WebPageVariantBlock,
WebPushSubscription,
WebTheme,
)
__all__ = [
"Base",
"DictLikeMixin",
"Identity",
"User",
"ManualBan",
"TemporaryData",
"BlockedUser",
"TrackingSource",
"Key",
"Tariff",
"Server",
"ServerSubgroup",
"ServerSpecialgroup",
"Payment",
"Coupon",
"CouponUsage",
"Referral",
"Notification",
"ScheduledBroadcast",
"Gift",
"GiftUsage",
"AuditEvent",
"Admin",
"Setting",
"WebPage",
"WebTheme",
"WebBlock",
"WebPageVariant",
"WebPageVariantBlock",
"WebPushSubscription",
"WebNotification",
"WebFlow",
]
+21
View File
@@ -0,0 +1,21 @@
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class DictLikeMixin:
"""Позволяет обращаться к ORM-объектам как к словарю.
Используется legacy-кодом, который мигрировал с dict-результатов asyncpg
на ORM и не хочет переписывать все `row["field"]` / `row.get("field")`.
"""
def __getitem__(self, key):
return getattr(self, key)
def get(self, key, default=None):
return getattr(self, key, default)
def to_dict(self):
return {column.name: getattr(self, column.name) for column in self.__table__.columns}
+32
View File
@@ -0,0 +1,32 @@
import secrets
from datetime import datetime
from sqlalchemy import BigInteger, Column, DateTime, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from ._base import Base, DictLikeMixin
class Admin(Base):
__tablename__ = "admins"
tg_id = Column(BigInteger, primary_key=True)
token = Column(String, unique=True, nullable=True)
description = Column(String, nullable=True)
role = Column(String, nullable=False, default="admin")
added_at = Column(DateTime, default=datetime.utcnow)
@staticmethod
def generate_token() -> str:
return secrets.token_urlsafe(32)
class Setting(DictLikeMixin, Base):
__tablename__ = "settings"
key = Column(String, primary_key=True)
value = Column(JSONB, nullable=True)
description = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+44
View File
@@ -0,0 +1,44 @@
from datetime import datetime
from sqlalchemy import (
BigInteger,
Column,
DateTime,
ForeignKey,
Index,
Integer,
String,
Text,
text as sql_text,
)
from sqlalchemy.dialects.postgresql import JSONB
from ._base import Base, DictLikeMixin
class AuditEvent(DictLikeMixin, Base):
"""События аудита (флоу пользователя)."""
__tablename__ = "audit_events"
__table_args__ = (
Index("ix_audit_events_tg_created", "actor_tg_id", "created_at"),
Index("ix_audit_events_identity_created", "actor_identity_id", "created_at"),
)
id = Column(Integer, primary_key=True, autoincrement=True)
event_type = Column(String(64), nullable=False, index=True)
channel = Column(String(32), nullable=False, index=True)
actor_identity_id = Column(
String(36),
ForeignKey("identities.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
)
actor_tg_id = Column(BigInteger, nullable=True, index=True)
path_or_handler = Column(String(255), nullable=False)
entity_type = Column(String(64), nullable=True, index=True)
entity_id = Column(String(255), nullable=True, index=True)
result = Column(String(32), nullable=False, server_default=sql_text("'success'"))
reason = Column(Text, nullable=True)
metadata_ = Column("metadata", JSONB, nullable=True)
request_id = Column(String(64), nullable=True, index=True)
created_at = Column(DateTime, default=datetime.utcnow, index=True)
+40
View File
@@ -0,0 +1,40 @@
from datetime import datetime
from sqlalchemy import (
BigInteger,
Boolean,
Column,
DateTime,
ForeignKey,
Integer,
String,
text as sql_text,
)
from ._base import Base, DictLikeMixin
class Coupon(DictLikeMixin, Base):
__tablename__ = "coupons"
id = Column(Integer, primary_key=True)
code = Column(String, unique=True)
amount = Column(Integer)
usage_limit = Column(Integer)
usage_count = Column(Integer, default=0)
is_used = Column(Boolean, default=False)
days = Column(Integer, nullable=True)
new_users_only = Column(Boolean, nullable=False, server_default=sql_text("false"))
percent = Column(Integer, nullable=True)
max_discount_amount = Column(Integer, nullable=True)
min_order_amount = Column(Integer, nullable=True)
class CouponUsage(DictLikeMixin, Base):
__tablename__ = "coupon_usages"
coupon_id = Column(Integer, ForeignKey("coupons.id", ondelete="CASCADE"), primary_key=True)
user_id = Column(BigInteger, primary_key=True)
tg_id = Column(BigInteger, nullable=True, index=True)
used_at = Column(DateTime, default=datetime.utcnow)
+39
View File
@@ -0,0 +1,39 @@
import uuid
from datetime import datetime
from sqlalchemy import BigInteger, Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from ._base import Base, DictLikeMixin
class Gift(DictLikeMixin, Base):
__tablename__ = "gifts"
gift_id = Column(String, primary_key=True, default=lambda: uuid.uuid4().hex)
sender_user_id = Column(BigInteger, nullable=True)
recipient_user_id = Column(BigInteger, nullable=True)
sender_tg_id = Column(BigInteger, ForeignKey("users.tg_id"), nullable=True, index=True)
recipient_tg_id = Column(BigInteger, ForeignKey("users.tg_id"), nullable=True, index=True)
selected_months = Column(Integer)
expiry_time = Column(DateTime)
gift_link = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
is_used = Column(Boolean, default=False)
is_unlimited = Column(Boolean, default=False)
max_usages = Column(Integer, nullable=True)
tariff_id: Mapped[int | None] = mapped_column(ForeignKey("tariffs.id"))
selected_device_limit = Column(Integer, nullable=True)
selected_traffic_gb = Column(Integer, nullable=True)
selected_price_rub = Column(Integer, nullable=True)
class GiftUsage(DictLikeMixin, Base):
__tablename__ = "gift_usages"
gift_id = Column(String, ForeignKey("gifts.gift_id"), primary_key=True)
user_id = Column(BigInteger, nullable=False, primary_key=True)
tg_id = Column(BigInteger, nullable=True, index=True)
used_at = Column(DateTime, default=datetime.utcnow)
+31
View File
@@ -0,0 +1,31 @@
import uuid
from datetime import datetime
from sqlalchemy import (
BigInteger,
Boolean,
Column,
DateTime,
String,
text as sql_text,
)
from ._base import Base, DictLikeMixin
class Identity(DictLikeMixin, Base):
"""Слой идентификации: к одному identity можно привязать email и/или Telegram (tg_id)."""
__tablename__ = "identities"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
email = Column(String(255), unique=True, nullable=True, index=True)
tg_id = Column(BigInteger, unique=True, nullable=True, index=True)
api_token_hash = Column(String(64), nullable=True, index=True)
token_issued_at = Column(DateTime, nullable=True)
password_hash = Column(String(64), nullable=True)
email_verified = Column(Boolean, nullable=False, server_default=sql_text("false"))
is_admin = Column(Boolean, nullable=False, server_default=sql_text("false"))
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+29
View File
@@ -0,0 +1,29 @@
from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, Integer, String
from ._base import Base, DictLikeMixin
class Key(DictLikeMixin, Base):
__tablename__ = "keys"
tg_id = Column(BigInteger, ForeignKey("users.tg_id"), primary_key=True, nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=True, index=True)
client_id = Column(String, primary_key=True)
email = Column(String, unique=True)
created_at = Column(BigInteger)
expiry_time = Column(BigInteger)
key = Column(String)
server_id = Column(String)
remnawave_link = Column(String)
tariff_id = Column(Integer, ForeignKey("tariffs.id", ondelete="SET NULL"))
is_frozen = Column(Boolean, default=False)
alias = Column(String)
notified = Column(Boolean, default=False)
notified_24h = Column(Boolean, default=False)
selected_device_limit = Column(Integer, nullable=True)
selected_traffic_limit = Column(BigInteger, nullable=True)
selected_price_rub = Column(Integer, nullable=True)
current_device_limit = Column(Integer, nullable=True)
current_traffic_limit = Column(BigInteger, nullable=True)
+55
View File
@@ -0,0 +1,55 @@
import uuid
from datetime import UTC, datetime
from sqlalchemy import (
BigInteger,
Column,
DateTime,
ForeignKey,
Index,
Integer,
String,
Text,
text as sql_text,
)
from sqlalchemy.dialects.postgresql import JSONB
from ._base import Base, DictLikeMixin
class Notification(DictLikeMixin, Base):
__tablename__ = "notifications"
tg_id = Column(BigInteger, ForeignKey("users.tg_id", ondelete="CASCADE"), nullable=True, index=True)
user_id = Column(BigInteger, nullable=False, primary_key=True)
notification_type = Column(String, primary_key=True)
last_notification_time = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class ScheduledBroadcast(DictLikeMixin, Base):
__tablename__ = "scheduled_broadcasts"
__table_args__ = (
Index("ix_scheduled_broadcasts_status_time", "status", "scheduled_for"),
Index("ix_scheduled_broadcasts_creator_time", "created_by_tg_id", "created_at"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
created_by_user_id = Column(BigInteger, nullable=True, index=True)
created_by_tg_id = Column(BigInteger, ForeignKey("users.tg_id", ondelete="SET NULL"), nullable=True, index=True)
status = Column(String(32), nullable=False, server_default=sql_text("'scheduled'"), index=True)
send_to = Column(String(32), nullable=False, index=True)
cluster_name = Column(String, nullable=True)
text = Column(Text, nullable=False)
photo = Column(String, nullable=True)
keyboard_json = Column(JSONB, nullable=True)
scheduled_for = Column(DateTime(timezone=True), nullable=False, index=True)
workers = Column(Integer, nullable=False, server_default=sql_text("5"))
messages_per_second = Column(Integer, nullable=False, server_default=sql_text("35"))
stats_json = Column(JSONB, nullable=True)
error_text = Column(Text, nullable=True)
started_at = Column(DateTime(timezone=True), nullable=True)
sent_at = Column(DateTime(timezone=True), nullable=True)
cancelled_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+22
View File
@@ -0,0 +1,22 @@
from datetime import datetime
from sqlalchemy import BigInteger, Column, DateTime, Float, ForeignKey, Integer, Numeric, String
from sqlalchemy.dialects.postgresql import JSONB
from ._base import Base, DictLikeMixin
class Payment(DictLikeMixin, Base):
__tablename__ = "payments"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=True, index=True)
tg_id = Column(BigInteger, ForeignKey("users.tg_id"), nullable=True, index=True)
amount = Column(Float)
payment_system = Column(String)
status = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
original_amount = Column(Numeric(18, 8), nullable=True)
currency = Column(String(10), nullable=False, server_default="RUB")
payment_id = Column(String(128), nullable=True, index=True)
metadata_ = Column("metadata", JSONB, nullable=True)
+13
View File
@@ -0,0 +1,13 @@
from sqlalchemy import BigInteger, Boolean, Column, ForeignKey
from ._base import Base, DictLikeMixin
class Referral(DictLikeMixin, Base):
__tablename__ = "referrals"
referred_user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
referrer_user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
referred_tg_id = Column(BigInteger, nullable=True, index=True)
referrer_tg_id = Column(BigInteger, nullable=True, index=True)
reward_issued = Column(Boolean, default=False)
+47
View File
@@ -0,0 +1,47 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import relationship
from ._base import Base, DictLikeMixin
class Server(DictLikeMixin, Base):
__tablename__ = "servers"
id = Column(Integer, primary_key=True, autoincrement=True)
cluster_name = Column(String)
server_name = Column(String, unique=True)
api_url = Column(String)
subscription_url = Column(String)
inbound_id = Column(String)
panel_type = Column(String)
max_keys = Column(Integer)
tariff_group = Column(String)
enabled = Column(Boolean, default=True)
subgroups = relationship("ServerSubgroup", back_populates="server", cascade="all, delete-orphan")
groups = relationship("ServerSpecialgroup", back_populates="server", cascade="all, delete-orphan")
class ServerSubgroup(DictLikeMixin, Base):
__tablename__ = "server_subgroups"
id = Column(Integer, primary_key=True, autoincrement=True)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), index=True, nullable=False)
group_code = Column(String, nullable=False)
subgroup_title = Column(String, nullable=False)
server = relationship("Server", back_populates="subgroups")
__table_args__ = (UniqueConstraint("server_id", "subgroup_title", name="uq_server_subgroup"),)
class ServerSpecialgroup(DictLikeMixin, Base):
__tablename__ = "server_specialgroups"
id = Column(Integer, primary_key=True, autoincrement=True)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), index=True, nullable=False)
group_code = Column(String, nullable=False)
server = relationship("Server")
__table_args__ = (UniqueConstraint("server_id", "group_code", name="uq_server_group"),)
+37
View File
@@ -0,0 +1,37 @@
from datetime import datetime
from sqlalchemy import BigInteger, Boolean, Column, DateTime, Integer, String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from ._base import Base, DictLikeMixin
class Tariff(DictLikeMixin, Base):
__tablename__ = "tariffs"
id = Column(Integer, primary_key=True)
name = Column(String)
group_code = Column(String)
duration_days = Column(Integer)
price_rub = Column(Integer)
traffic_limit = Column(BigInteger, nullable=True)
device_limit = Column(Integer, nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
subgroup_title = Column(String, nullable=True)
sort_order = Column(Integer, nullable=True)
vless = Column(Boolean, default=False)
external_squad: Mapped[str | None] = mapped_column(String(64), nullable=True)
configurable = Column(Boolean, nullable=False, server_default="false")
device_options = Column(JSONB, nullable=True)
traffic_options_gb = Column(JSONB, nullable=True)
device_step_rub = Column(Integer, nullable=True)
device_overrides = Column(JSONB, nullable=True)
traffic_step_rub = Column(Integer, nullable=True)
traffic_overrides = Column(JSONB, nullable=True)
+88
View File
@@ -0,0 +1,88 @@
from datetime import datetime
from sqlalchemy import (
JSON,
BigInteger,
Boolean,
Column,
DateTime,
Float,
ForeignKey,
Identity as SAIdentity,
Integer,
String,
Text,
)
from ._base import Base, DictLikeMixin
class User(DictLikeMixin, Base):
__tablename__ = "users"
id = Column(BigInteger, SAIdentity(always=False), primary_key=True)
tg_id = Column(BigInteger, nullable=True, unique=True, index=True)
identity_id = Column(
String(36),
ForeignKey("identities.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
)
username = Column(String)
first_name = Column(String)
last_name = Column(String)
language_code = Column(String)
is_bot = Column(Boolean, default=False)
balance = Column(Float, default=0.0)
trial = Column(Integer, default=0)
preferred_currency = Column(String(10), nullable=False, server_default="RUB", index=True)
source_code = Column(
String,
ForeignKey(
"tracking_sources.code",
ondelete="SET NULL",
onupdate="CASCADE",
),
nullable=True,
)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
class ManualBan(DictLikeMixin, Base):
__tablename__ = "manual_bans"
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, primary_key=True)
tg_id = Column(BigInteger, nullable=True, index=True)
banned_at = Column(DateTime(timezone=True), default=datetime.utcnow)
reason = Column(Text)
banned_by = Column(BigInteger)
until = Column(DateTime(timezone=True), nullable=True)
class TemporaryData(DictLikeMixin, Base):
__tablename__ = "temporary_data"
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, primary_key=True)
tg_id = Column(BigInteger, nullable=True, index=True)
state = Column(String)
data = Column(JSON)
updated_at = Column(DateTime, default=datetime.utcnow)
class BlockedUser(DictLikeMixin, Base):
__tablename__ = "blocked_users"
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, primary_key=True)
tg_id = Column(BigInteger, nullable=True, index=True)
class TrackingSource(DictLikeMixin, Base):
__tablename__ = "tracking_sources"
id = Column(Integer, primary_key=True)
name = Column(String)
code = Column(String, unique=True)
type = Column(String)
created_by = Column(BigInteger)
created_at = Column(DateTime, default=datetime.utcnow)
+166
View File
@@ -0,0 +1,166 @@
import uuid
from datetime import UTC, datetime
from sqlalchemy import (
BigInteger,
Boolean,
Column,
DateTime,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
text as sql_text,
)
from sqlalchemy.dialects.postgresql import JSONB
from ._base import Base, DictLikeMixin
class WebPage(DictLikeMixin, Base):
__tablename__ = "web_pages"
slug = Column(String(64), primary_key=True)
title = Column(String(255), nullable=True)
class WebTheme(DictLikeMixin, Base):
__tablename__ = "web_themes"
page_slug = Column(String(64), ForeignKey("web_pages.slug", ondelete="CASCADE"), primary_key=True)
tokens = Column(JSONB, nullable=False, default=dict)
class WebBlock(DictLikeMixin, Base):
__tablename__ = "web_blocks"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
page_slug = Column(String(64), ForeignKey("web_pages.slug", ondelete="CASCADE"), index=True, nullable=False)
order = Column(Integer, nullable=False, default=0)
type = Column(String(64), nullable=False)
data = Column(JSONB, nullable=False, default=dict)
class WebPageVariant(DictLikeMixin, Base):
__tablename__ = "web_page_variants"
__table_args__ = (
UniqueConstraint("page_slug", "variant_key", name="uq_web_page_variants_page_slug_variant_key"),
Index("ix_web_page_variants_page_slug_is_active", "page_slug", "is_active"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
page_slug = Column(String(64), ForeignKey("web_pages.slug", ondelete="CASCADE"), index=True, nullable=False)
variant_key = Column(String(64), nullable=False)
name = Column(String(255), nullable=False, default="Default")
is_active = Column(Boolean, nullable=False, server_default=sql_text("false"))
theme_tokens = Column(JSONB, nullable=False, default=dict)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class WebPageVariantBlock(DictLikeMixin, Base):
__tablename__ = "web_page_variant_blocks"
__table_args__ = (Index("ix_web_page_variant_blocks_variant_id_order", "variant_id", "order"),)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
variant_id = Column(String(36), ForeignKey("web_page_variants.id", ondelete="CASCADE"), index=True, nullable=False)
order = Column(Integer, nullable=False, default=0)
type = Column(String(64), nullable=False)
data = Column(JSONB, nullable=False, default=dict)
class WebPushSubscription(DictLikeMixin, Base):
__tablename__ = "web_push_subscriptions"
__table_args__ = (
Index("ix_web_push_subscriptions_user_id", "user_id"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(BigInteger, nullable=False)
identity_id = Column(String(36), nullable=True, index=True)
endpoint = Column(Text, nullable=False, unique=True)
keys_json = Column(JSONB, nullable=False)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class WebNotification(DictLikeMixin, Base):
__tablename__ = "web_notifications"
__table_args__ = (
Index("ix_web_notifications_user_read", "user_id", "read"),
Index("ix_web_notifications_created", "created_at"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(BigInteger, nullable=False, index=True)
identity_id = Column(String(36), nullable=True, index=True)
type = Column(String(32), nullable=False, default="system")
title = Column(String(255), nullable=False)
message = Column(Text, nullable=False, default="")
read = Column(Boolean, nullable=False, default=False)
data = Column(JSONB, nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class WebFlowEvent(DictLikeMixin, Base):
__tablename__ = "web_flow_events"
__table_args__ = (
Index("ix_web_flow_events_flow_node", "flow_id", "node_id"),
Index("ix_web_flow_events_created", "created_at"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
flow_id = Column(String(64), nullable=False)
node_id = Column(String(64), nullable=False)
node_type = Column(String(32), nullable=False, default="")
event_type = Column(String(32), nullable=False)
ab_variant = Column(String(16), nullable=True)
device = Column(String(16), nullable=True)
locale = Column(String(8), nullable=True)
authenticated = Column(Boolean, nullable=True)
event_metadata = Column("metadata", JSONB, nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class WebCustomElementBuild(DictLikeMixin, Base):
__tablename__ = "web_custom_element_builds"
__table_args__ = (
Index("ix_web_custom_element_builds_status", "status"),
Index("ix_web_custom_element_builds_created", "created_at"),
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
label = Column(String(255), nullable=False, default="")
slug = Column(String(128), nullable=False, default="")
runtime = Column(String(32), nullable=False, default="react-component")
source_kind = Column(String(32), nullable=False, default="inline-code")
source_value = Column(Text, nullable=False, default="")
export_name = Column(String(128), nullable=False, default="default")
props_schema_text = Column(Text, nullable=False, default="")
sample_props_text = Column(Text, nullable=False, default="")
events_text = Column(Text, nullable=False, default="")
notes = Column(Text, nullable=False, default="")
status = Column(String(32), nullable=False, default="queued")
summary = Column(Text, nullable=False, default="")
next_steps = Column(JSONB, nullable=False, default=list)
artifact = Column(JSONB, nullable=True)
upload_meta = Column(JSONB, nullable=True)
worker_id = Column(String(64), nullable=True)
worker_claimed_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
class WebFlow(DictLikeMixin, Base):
__tablename__ = "web_flows"
id = Column(String(64), primary_key=True, default="default")
name = Column(String(255), nullable=False, default="Основной flow")
nodes = Column(JSONB, nullable=False, default=list)
edges = Column(JSONB, nullable=False, default=list)
entry_node_id = Column(String(64), nullable=True)
version = Column(Integer, nullable=False, default=1)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))