"""Stan feedu Swipe per urządzenie: widziane, zdarzenia, afiniczność, ulubione, blacklista. Tożsamość = `X-Device-Id` (bez kont), jak w reszcie aplikacji. Wszystko kluczowane po `device_id`, więc przenosi się kodem synchronizacji (`/me/import`). **`clip_seen`**: zbiór wykluczeń feedu. Wiersz powstaje **w chwili podania** klipu (`outcome='served'`), zdarzenia z apki podnoszą `outcome`. Złożony PK to jedyny indeks, jakiego potrzebuje `NOT EXISTS` w zapytaniu feedu (jedno trafienie na kandydata). Koszt zapisu przy podaniu: klip z prefetchu, którego użytkownik nie doszedł, znika z feedu (kilka na sesję); zysk: kursor bez listy widzianych i zero powtórek między stronami. **`clip_feed_events`**: append-only telemetria (impression z dwell, skip, complete, like, unlike, not_interested, autoswipe). Bez FK na klip (klip może zostać skasowany). Karmi afiniczność i mierzy, czy personalizacja coś daje (`slot` affinity vs explore). **`clip_affinity`**: wagi per urządzenie w JSONB (tag → waga, nisza → waga, twórca → waga), aktualizowane przy zdarzeniu, nocny decay ×0,97, cap 60/40/40 pozycji. To najbardziej intymne dane w aplikacji: kasowalne jednym wywołaniem (`DELETE /clips/affinity`), nie trafiają do logów. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Index, Integer, SmallInteger, String, func from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base class ClipSeen(Base): __tablename__ = "clip_seen" device_id: Mapped[str] = mapped_column(String(64), primary_key=True) clip_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("clips.id", ondelete="CASCADE"), primary_key=True ) #: served | skip | complete | like | not_interested (rosnąca „siła", nie cofamy). outcome: Mapped[str] = mapped_column(String(16), nullable=False, default="served") seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class ClipFeedEvent(Base): __tablename__ = "clip_feed_events" __table_args__ = ( Index("ix_clip_feed_events_device_created", "device_id", "created_at"), Index("ix_clip_feed_events_created", "created_at"), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) device_id: Mapped[str] = mapped_column(String(64), nullable=False) session_id: Mapped[str | None] = mapped_column(String(32), nullable=True) clip_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True) kind: Mapped[str] = mapped_column(String(16), nullable=False) dwell_ms: Mapped[int | None] = mapped_column(Integer, nullable=True) #: Powód „nie interesuje": `tag:` | `niche:` | `creator:` | `seen_it`. reason: Mapped[str | None] = mapped_column(String(160), nullable=True) #: `affinity` | `explore` (z którego koszyka klip przyszedł). slot: Mapped[str | None] = mapped_column(String(12), nullable=True) position: Mapped[int | None] = mapped_column(SmallInteger, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class ClipAffinity(Base): __tablename__ = "clip_affinity" device_id: Mapped[str] = mapped_column(String(64), primary_key=True) tag_weights: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) niche_weights: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) creator_weights: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) events_seen: Mapped[int] = mapped_column(Integer, nullable=False, default=0) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) class ClipFavorite(Base): __tablename__ = "clip_favorites" device_id: Mapped[str] = mapped_column(String(64), primary_key=True) clip_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("clips.id", ondelete="CASCADE"), primary_key=True ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class ClipBlacklist(Base): __tablename__ = "clip_blacklist" device_id: Mapped[str] = mapped_column(String(64), primary_key=True) #: tag | niche | creator kind: Mapped[str] = mapped_column(String(8), primary_key=True) value: Mapped[str] = mapped_column(String(128), primary_key=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False )