Files
timandClaude Sonnet 5 28bb045274 Rozsirenie analytiky: login event, prepinac vsetci/prihlaseni, plny nazov krajiny
Pageview beacony uz neposielaju player_id z klienta (nedovereny vstup na
neautentifikovanom endpointe) -- prihlasenie sa eviduje server-side ako
event "login" (aj z registracie), s player_id skutocneho uctu. Admin
dashboard dostal prepinac scope vsetci/prihlaseni, ktory konzistentne
pocita grafy aj tabulky (kazdy login samostatne, navstevnicky den pre
anonymnu navstevnost). PageView.country teraz uklada cely anglicky nazov
krajiny namiesto ISO kodu (potrebna zmena schemy). Pridane sledovanie
klikov na "Pravidla hry" aj z GameList.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 12:43:37 +02:00

113 lines
4.5 KiB
Python

"""ORM modely: Player, Game, Guess, PageView.
`won` sa neuklada -- vyplyva z `points > 0`
(trafeny tip = 10 + tip, inak 0; pozri Round.get_points_summary v bridzik.py).
"""
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column
from db.db import Base
class Player(Base):
"""Trvaly ucet hraca + autentifikacia (TOTP)."""
__tablename__ = "players"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(40), unique=True, index=True)
# Sifrovany cez db/crypto.py (Fernet) -- nikdy neuklada plaintext secret.
totp_secret: Mapped[str] = mapped_column(String(255))
# Posledny pouzity TOTP casovy krok -- ochrana proti replay v ramci okna.
totp_last_step: Mapped[int] = mapped_column(Integer, default=0)
# Session token pre auto-reconnect (poslany v Socket.IO `auth`).
# Uklada sa SHA-256 hash (db/crypto.hash_token), nie surovy token.
auth_token: Mapped[str | None] = mapped_column(
String(64), unique=True, nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now()
)
class Game(Base):
"""Jedna partia. Drzi priamo 4 ID hracov podla sedadla (0-3)."""
__tablename__ = "games"
id: Mapped[str] = mapped_column(
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
)
name: Mapped[str] = mapped_column(String(40), default="")
player0_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
player1_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
player2_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
player3_id: Mapped[int] = mapped_column(ForeignKey("players.id"))
# Aktualna pozicia hry. Spolu s rozdanim kariet nanovo plne urcuju stav hry
# po zaciatok kola -> staci na restore (pozri api/history.rebuild_core).
series: Mapped[int] = mapped_column(Integer, default=0)
round: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now()
)
ended_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
def player_id_for_seat(self, seat: int) -> int:
return (self.player0_id, self.player1_id, self.player2_id, self.player3_id)[seat]
class Guess(Base):
"""Tip a vysledok jedneho hraca v jednom kole.
Unikat (game_id, series_number, round_number, player_id) robi zapis
idempotentnym -- opakovany `record_completed_rounds` nezaklada duplikaty.
"""
__tablename__ = "guesses"
__table_args__ = (
UniqueConstraint(
"game_id",
"series_number",
"round_number",
"player_id",
name="uq_guess_round_player",
),
)
id: Mapped[int] = mapped_column(primary_key=True)
game_id: Mapped[str] = mapped_column(ForeignKey("games.id"), index=True)
player_id: Mapped[int] = mapped_column(ForeignKey("players.id"), index=True)
series_number: Mapped[int] = mapped_column(Integer)
round_number: Mapped[int] = mapped_column(Integer)
guess: Mapped[int] = mapped_column(Integer)
points: Mapped[int] = mapped_column(Integer)
class PageView(Base):
"""Jedna navsteva stranky (pre /track). Bez schema migracii -- create_all only."""
__tablename__ = "page_views"
id: Mapped[int] = mapped_column(primary_key=True)
# Konvencia: zacina "/" -> skutocna URL cesta; inak nazov eventu (napr.
# "rules_view", "login") -- ziadny osobitny stlpec, rovnaky stlpec pre oboje.
path: Mapped[str] = mapped_column(String(200))
# Vyplnene len ak je znamy prihlaseny hrac v momente zapisu (napr. event "login").
player_id: Mapped[int | None] = mapped_column(
ForeignKey("players.id"), nullable=True, index=True
)
referrer: Mapped[str] = mapped_column(String(300), default="")
user_agent: Mapped[str] = mapped_column(String(300), default="")
browser: Mapped[str] = mapped_column(String(40), default="")
os: Mapped[str] = mapped_column(String(40), default="")
device_type: Mapped[str] = mapped_column(String(20), default="") # mobile/tablet/pc/bot
ip: Mapped[str] = mapped_column(String(45), default="") # surova IP (IPv4/IPv6), "" ak nezname
country: Mapped[str] = mapped_column(String(60), default="") # anglicky nazov z GeoIP, "" ak nerozlusene
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now()
)