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:
@@ -456,6 +456,14 @@ def list_characters():
|
||||
char_id = character.get('character_id')
|
||||
character['sessions'] = sessions_by_character.get(char_id, [])
|
||||
|
||||
# Fetch usage info for daily turn limits
|
||||
usage_info = {}
|
||||
try:
|
||||
usage_response = api_client.get("/api/v1/usage")
|
||||
usage_info = usage_response.get('result', {})
|
||||
except (APIError, APINotFoundError) as e:
|
||||
logger.debug("Could not fetch usage info", error=str(e))
|
||||
|
||||
logger.info(
|
||||
"Characters listed",
|
||||
user_id=user.get('id'),
|
||||
@@ -468,18 +476,46 @@ def list_characters():
|
||||
characters=characters,
|
||||
current_tier=current_tier,
|
||||
max_characters=max_characters,
|
||||
can_create=can_create
|
||||
can_create=can_create,
|
||||
# Usage display variables
|
||||
remaining=usage_info.get('remaining', 0),
|
||||
daily_limit=usage_info.get('daily_limit', 0),
|
||||
is_limited=usage_info.get('is_limited', False),
|
||||
is_unlimited=usage_info.get('is_unlimited', False),
|
||||
reset_time=usage_info.get('reset_time', '')
|
||||
)
|
||||
|
||||
except APITimeoutError:
|
||||
logger.error("API timeout while listing characters", user_id=user.get('id'))
|
||||
flash('Request timed out. Please try again.', 'error')
|
||||
return render_template('character/list.html', characters=[], can_create=False)
|
||||
return render_template(
|
||||
'character/list.html',
|
||||
characters=[],
|
||||
can_create=False,
|
||||
current_tier='free',
|
||||
max_characters=1,
|
||||
remaining=0,
|
||||
daily_limit=0,
|
||||
is_limited=False,
|
||||
is_unlimited=False,
|
||||
reset_time=''
|
||||
)
|
||||
|
||||
except APIError as e:
|
||||
logger.error("Failed to list characters", user_id=user.get('id'), error=str(e))
|
||||
flash('An error occurred while loading your characters.', 'error')
|
||||
return render_template('character/list.html', characters=[], can_create=False)
|
||||
return render_template(
|
||||
'character/list.html',
|
||||
characters=[],
|
||||
can_create=False,
|
||||
current_tier='free',
|
||||
max_characters=1,
|
||||
remaining=0,
|
||||
daily_limit=0,
|
||||
is_limited=False,
|
||||
is_unlimited=False,
|
||||
reset_time=''
|
||||
)
|
||||
|
||||
|
||||
@character_bp.route('/<character_id>')
|
||||
@@ -613,6 +649,58 @@ def create_session(character_id: str):
|
||||
return redirect(url_for('character_views.list_characters'))
|
||||
|
||||
|
||||
@character_bp.route('/sessions/<session_id>/delete', methods=['DELETE'])
|
||||
@require_auth_web
|
||||
def delete_session(session_id: str):
|
||||
"""
|
||||
Delete a game session via HTMX.
|
||||
|
||||
This permanently removes the session from the database.
|
||||
Returns a response that triggers page refresh to update UI properly
|
||||
(handles "Continue Playing" button visibility).
|
||||
|
||||
Args:
|
||||
session_id: ID of the session to delete
|
||||
"""
|
||||
from flask import make_response
|
||||
|
||||
user = get_current_user()
|
||||
api_client = get_api_client()
|
||||
|
||||
try:
|
||||
api_client.delete(f"/api/v1/sessions/{session_id}")
|
||||
|
||||
logger.info(
|
||||
"Session deleted via web UI",
|
||||
user_id=user.get('id'),
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# Return empty response with HX-Refresh to reload the page
|
||||
# This ensures "Continue Playing" button visibility is correct
|
||||
response = make_response('', 200)
|
||||
response.headers['HX-Refresh'] = 'true'
|
||||
return response
|
||||
|
||||
except APINotFoundError:
|
||||
logger.warning(
|
||||
"Session not found for deletion",
|
||||
user_id=user.get('id'),
|
||||
session_id=session_id
|
||||
)
|
||||
# Return error message that HTMX can display
|
||||
return '<span class="error-message">Session not found</span>', 404
|
||||
|
||||
except APIError as e:
|
||||
logger.error(
|
||||
"Failed to delete session",
|
||||
user_id=user.get('id'),
|
||||
session_id=session_id,
|
||||
error=str(e)
|
||||
)
|
||||
return '<span class="error-message">Failed to delete session</span>', 500
|
||||
|
||||
|
||||
@character_bp.route('/<character_id>/skills')
|
||||
@require_auth_web
|
||||
def view_skills(character_id: str):
|
||||
|
||||
@@ -201,6 +201,14 @@ def play_session(session_id: str):
|
||||
# Get user tier
|
||||
user_tier = _get_user_tier(client)
|
||||
|
||||
# Fetch usage info for daily turn limits
|
||||
usage_info = {}
|
||||
try:
|
||||
usage_response = client.get("/api/v1/usage")
|
||||
usage_info = usage_response.get('result', {})
|
||||
except (APINotFoundError, APIError) as e:
|
||||
logger.debug("could_not_fetch_usage_info", error=str(e))
|
||||
|
||||
# Build session object for template
|
||||
session = {
|
||||
'session_id': session_id,
|
||||
@@ -220,7 +228,13 @@ def play_session(session_id: str):
|
||||
npcs=npcs,
|
||||
discovered_locations=discovered_locations,
|
||||
actions=DEFAULT_ACTIONS,
|
||||
user_tier=user_tier
|
||||
user_tier=user_tier,
|
||||
# Usage display variables
|
||||
remaining=usage_info.get('remaining', 0),
|
||||
daily_limit=usage_info.get('daily_limit', 0),
|
||||
is_limited=usage_info.get('is_limited', False),
|
||||
is_unlimited=usage_info.get('is_unlimited', False),
|
||||
reset_time=usage_info.get('reset_time', '')
|
||||
)
|
||||
|
||||
except APINotFoundError:
|
||||
@@ -259,13 +273,27 @@ def character_panel(session_id: str):
|
||||
character = _build_character_from_api(char_data)
|
||||
user_tier = _get_user_tier(client)
|
||||
|
||||
# Fetch usage info for daily turn limits
|
||||
usage_info = {}
|
||||
try:
|
||||
usage_response = client.get("/api/v1/usage")
|
||||
usage_info = usage_response.get('result', {})
|
||||
except (APINotFoundError, APIError) as e:
|
||||
logger.debug("could_not_fetch_usage_info", error=str(e))
|
||||
|
||||
return render_template(
|
||||
'game/partials/character_panel.html',
|
||||
session_id=session_id,
|
||||
character=character,
|
||||
location=location,
|
||||
actions=DEFAULT_ACTIONS,
|
||||
user_tier=user_tier
|
||||
user_tier=user_tier,
|
||||
# Usage display variables
|
||||
remaining=usage_info.get('remaining', 0),
|
||||
daily_limit=usage_info.get('daily_limit', 0),
|
||||
is_limited=usage_info.get('is_limited', False),
|
||||
is_unlimited=usage_info.get('is_unlimited', False),
|
||||
reset_time=usage_info.get('reset_time', '')
|
||||
)
|
||||
except APIError as e:
|
||||
logger.error("failed_to_refresh_character_panel", session_id=session_id, error=str(e))
|
||||
|
||||
Reference in New Issue
Block a user