feat(api,web): tier-based session limits and daily turn usage display
Backend Changes:
- Add tier-based max_sessions config (free: 1, basic: 2, premium: 3, elite: 5)
- Add DELETE /api/v1/sessions/{id} endpoint for hard session deletion
- Cascade delete chat messages when session is deleted
- Add GET /api/v1/usage endpoint for daily turn limit info
- Replace hardcoded TIER_LIMITS with config-based ai_calls_per_day
- Handle unlimited (-1) tier in rate limiter service
Frontend Changes:
- Add inline session delete buttons with HTMX on character list
- Add usage_display.html component showing remaining daily turns
- Display usage indicator on character list and game play pages
- Page refresh after session deletion to update UI state
Documentation:
- Update API_REFERENCE.md with new endpoints and tier limits
- Update API_TESTING.md with session endpoint examples
- Update SESSION_MANAGEMENT.md with tier-based limits
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,15 +19,13 @@ from app.services.database_service import get_database_service
|
||||
from app.services.appwrite_service import AppwriteService
|
||||
from app.services.character_service import get_character_service, CharacterNotFound
|
||||
from app.services.location_loader import get_location_loader
|
||||
from app.services.chat_message_service import get_chat_message_service
|
||||
from app.utils.logging import get_logger
|
||||
from app.config import get_config
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
# Session limits per user
|
||||
MAX_ACTIVE_SESSIONS = 5
|
||||
|
||||
|
||||
class SessionNotFound(Exception):
|
||||
"""Raised when session ID doesn't exist or user doesn't own it."""
|
||||
pass
|
||||
@@ -129,16 +127,22 @@ class SessionService:
|
||||
if not starting_location_type:
|
||||
starting_location_type = LocationType.TOWN
|
||||
|
||||
# Check session limit
|
||||
# Check session limit based on user's subscription tier
|
||||
user_tier = self.appwrite.get_user_tier(user_id)
|
||||
config = get_config()
|
||||
tier_config = config.rate_limiting.tiers.get(user_tier)
|
||||
max_sessions = tier_config.max_sessions if tier_config else 1
|
||||
|
||||
active_count = self.count_user_sessions(user_id, active_only=True)
|
||||
if active_count >= MAX_ACTIVE_SESSIONS:
|
||||
if active_count >= max_sessions:
|
||||
logger.warning("Session limit exceeded",
|
||||
user_id=user_id,
|
||||
tier=user_tier,
|
||||
current=active_count,
|
||||
limit=MAX_ACTIVE_SESSIONS)
|
||||
limit=max_sessions)
|
||||
raise SessionLimitExceeded(
|
||||
f"Maximum active sessions reached ({active_count}/{MAX_ACTIVE_SESSIONS}). "
|
||||
f"Please end an existing session to start a new one."
|
||||
f"Maximum active sessions reached for {user_tier} tier ({active_count}/{max_sessions}). "
|
||||
f"Please delete an existing session to start a new one."
|
||||
)
|
||||
|
||||
# Generate unique session ID
|
||||
@@ -409,6 +413,58 @@ class SessionService:
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
def delete_session(self, session_id: str, user_id: str) -> bool:
|
||||
"""
|
||||
Permanently delete a session from the database.
|
||||
|
||||
Unlike end_session(), this method removes the session document entirely
|
||||
from the database. Use this when the user wants to free up their session
|
||||
slot and doesn't need to preserve the game history.
|
||||
|
||||
Also deletes all chat messages associated with this session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to delete
|
||||
user_id: User ID for ownership validation
|
||||
|
||||
Returns:
|
||||
True if deleted successfully
|
||||
|
||||
Raises:
|
||||
SessionNotFound: If session doesn't exist or user doesn't own it
|
||||
"""
|
||||
try:
|
||||
logger.info("Deleting session", session_id=session_id, user_id=user_id)
|
||||
|
||||
# Verify ownership first (raises SessionNotFound if invalid)
|
||||
self.get_session(session_id, user_id)
|
||||
|
||||
# Delete associated chat messages first
|
||||
chat_service = get_chat_message_service()
|
||||
deleted_messages = chat_service.delete_messages_by_session(session_id)
|
||||
logger.info("Deleted associated chat messages",
|
||||
session_id=session_id,
|
||||
message_count=deleted_messages)
|
||||
|
||||
# Delete session from database
|
||||
self.db.delete_document(
|
||||
collection_id=self.collection_id,
|
||||
document_id=session_id
|
||||
)
|
||||
|
||||
logger.info("Session deleted successfully",
|
||||
session_id=session_id,
|
||||
user_id=user_id)
|
||||
return True
|
||||
|
||||
except SessionNotFound:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete session",
|
||||
session_id=session_id,
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
def add_conversation_entry(
|
||||
self,
|
||||
session_id: str,
|
||||
|
||||
Reference in New Issue
Block a user