v2: In-app support chat with Telegram notifications (REVIEW — not deployed)
Backend: - New models: SupportThread, SupportMessage (models/support.py) - Support router: user thread/messages, admin threads/reply, Telegram outbound - Context auto-attached: active program, points, gate status, last workout - Rate limited: 10 messages/user/day - Admin auth: username == 'scot' check - Telegram: fire-and-forget sendMessage via @AureusGoldBot, never blocks - config.py: added telegram_bot_token, telegram_chat_id Frontend: - Support.jsx: chat-style thread, send messages, confirmation, auto-scroll - SupportAdmin.jsx: thread list with unread badges, thread detail with context sidebar, reply - App.jsx: floating '?' help button with unread badge (polls every 60s), routes - api.js: support endpoints (user + admin) Config: - docker-compose.yml: TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars NOT DEPLOYED — requires Telegram env vars in Coolify
This commit is contained in:
parent
42632b1b50
commit
10f13a9f62
10 changed files with 988 additions and 4 deletions
54
backend/app/models/support.py
Normal file
54
backend/app/models/support.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Index, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class SupportThread(Base):
|
||||
"""One thread per user — private conversation with admin."""
|
||||
__tablename__ = "support_threads"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id"), unique=True, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
last_message_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
unread_user: Mapped[int] = mapped_column(Integer, default=0)
|
||||
unread_admin: Mapped[int] = mapped_column(Integer, default=0)
|
||||
|
||||
messages: Mapped[list["SupportMessage"]] = relationship(
|
||||
back_populates="thread", cascade="all, delete-orphan",
|
||||
order_by="SupportMessage.created_at"
|
||||
)
|
||||
|
||||
|
||||
class SupportMessage(Base):
|
||||
"""Individual message within a support thread."""
|
||||
__tablename__ = "support_messages"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
thread_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("support_threads.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
sender: Mapped[str] = mapped_column(String(10), nullable=False) # 'user' or 'admin'
|
||||
body: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
context: Mapped[dict | None] = mapped_column(JSONB)
|
||||
read_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
thread: Mapped["SupportThread"] = relationship(back_populates="messages")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_support_messages_thread", "thread_id", "created_at"),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue