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>
190 lines
8.9 KiB
HTML
190 lines
8.9 KiB
HTML
{#
|
|
Character Panel - Left sidebar
|
|
Displays character stats, resource bars, and action buttons
|
|
#}
|
|
<div class="character-panel">
|
|
{# Character Header #}
|
|
<div class="character-header">
|
|
<div class="character-name">{{ character.name }}</div>
|
|
<div class="character-info">
|
|
<span class="character-class">{{ character.class_name }}</span>
|
|
<span class="character-level">Level {{ character.level }}</span>
|
|
</div>
|
|
<div class="character-usage">
|
|
{% include 'components/usage_display.html' %}
|
|
</div>
|
|
</div>
|
|
|
|
{# Resource Bars #}
|
|
<div class="resource-bars">
|
|
{# HP Bar #}
|
|
<div class="resource-bar resource-bar--hp">
|
|
<div class="resource-bar-label">
|
|
<span class="resource-bar-name">HP</span>
|
|
<span class="resource-bar-value">{{ character.current_hp }} / {{ character.max_hp }}</span>
|
|
</div>
|
|
<div class="resource-bar-track">
|
|
{% set hp_percent = (character.current_hp / character.max_hp * 100)|int %}
|
|
<div class="resource-bar-fill" style="width: {{ hp_percent }}%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{# MP Bar #}
|
|
<div class="resource-bar resource-bar--mp">
|
|
<div class="resource-bar-label">
|
|
<span class="resource-bar-name">MP</span>
|
|
<span class="resource-bar-value">{{ character.current_mp }} / {{ character.max_mp }}</span>
|
|
</div>
|
|
<div class="resource-bar-track">
|
|
{% set mp_percent = (character.current_mp / character.max_mp * 100)|int %}
|
|
<div class="resource-bar-fill" style="width: {{ mp_percent }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Stats Accordion (Collapsed by default) #}
|
|
<div class="panel-accordion collapsed" data-panel-accordion="stats">
|
|
<button class="panel-accordion-header" onclick="togglePanelAccordion(this)">
|
|
<span>Stats</span>
|
|
<span class="panel-accordion-icon">▼</span>
|
|
</button>
|
|
<div class="panel-accordion-content">
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-abbr">STR</div>
|
|
<div class="stat-value">{{ character.stats.strength }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-abbr">DEX</div>
|
|
<div class="stat-value">{{ character.stats.dexterity }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-abbr">CON</div>
|
|
<div class="stat-value">{{ character.stats.constitution }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-abbr">INT</div>
|
|
<div class="stat-value">{{ character.stats.intelligence }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-abbr">WIS</div>
|
|
<div class="stat-value">{{ character.stats.wisdom }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-abbr">CHA</div>
|
|
<div class="stat-value">{{ character.stats.charisma }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Quick Actions (Equipment, NPC, Travel) #}
|
|
<div class="quick-actions">
|
|
{# Equipment & Gear - Opens modal #}
|
|
<button class="action-btn action-btn--special"
|
|
hx-get="{{ url_for('game.equipment_modal', session_id=session_id) }}"
|
|
hx-target="#modal-container"
|
|
hx-swap="innerHTML">
|
|
⚔️ Equipment & Gear
|
|
</button>
|
|
|
|
{# Talk to NPC - Opens NPC accordion #}
|
|
<button class="action-btn action-btn--special"
|
|
hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}"
|
|
hx-target="#accordion-npcs"
|
|
hx-swap="innerHTML"
|
|
onclick="document.querySelector('[data-accordion=npcs]').classList.remove('collapsed')">
|
|
💬 Talk to NPC...
|
|
</button>
|
|
|
|
{# Travel - Opens modal #}
|
|
<button class="action-btn action-btn--special"
|
|
hx-get="{{ url_for('game.travel_modal', session_id=session_id) }}"
|
|
hx-target="#modal-container"
|
|
hx-swap="innerHTML">
|
|
🗺️ Travel to...
|
|
</button>
|
|
</div>
|
|
|
|
{# Actions Section #}
|
|
<div class="actions-section">
|
|
<div class="actions-title">Actions</div>
|
|
|
|
{# Free Tier Actions #}
|
|
<div class="actions-tier">
|
|
<div class="actions-tier-label">Free Actions</div>
|
|
<div class="actions-list">
|
|
{% for action in actions.free %}
|
|
{% set available = action.context == ['any'] or location.location_type in action.context %}
|
|
<button class="action-btn action-btn--free"
|
|
hx-post="{{ url_for('game.take_action', session_id=session_id) }}"
|
|
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
|
|
hx-target="#narrative-content"
|
|
hx-swap="innerHTML"
|
|
hx-disabled-elt="this"
|
|
{% if not available %}disabled title="Not available in this location"{% endif %}>
|
|
<span class="action-btn-text">{{ action.display_text }}</span>
|
|
{% if action.cooldown %}
|
|
<span class="action-btn-cooldown" title="{{ action.cooldown }} turn cooldown">⏱</span>
|
|
{% endif %}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{# Premium Tier Actions #}
|
|
<div class="actions-tier">
|
|
<div class="actions-tier-label">Premium Actions</div>
|
|
<div class="actions-list">
|
|
{% for action in actions.premium %}
|
|
{% set available = action.context == ['any'] or location.location_type in action.context %}
|
|
{% set locked = user_tier not in ['premium', 'elite'] %}
|
|
<button class="action-btn {% if locked %}action-btn--locked{% else %}action-btn--premium{% endif %}"
|
|
{% if not locked %}
|
|
hx-post="{{ url_for('game.take_action', session_id=session_id) }}"
|
|
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
|
|
hx-target="#narrative-content"
|
|
hx-swap="innerHTML"
|
|
hx-disabled-elt="this"
|
|
{% endif %}
|
|
{% if locked %}disabled title="Requires Premium tier"{% elif not available %}disabled title="Not available in this location"{% endif %}>
|
|
<span class="action-btn-text">{{ action.display_text }}</span>
|
|
{% if locked %}
|
|
<span class="action-btn-lock">🔒</span>
|
|
{% elif action.cooldown %}
|
|
<span class="action-btn-cooldown" title="{{ action.cooldown }} turn cooldown">⏱</span>
|
|
{% endif %}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{# Elite Tier Actions #}
|
|
<div class="actions-tier">
|
|
<div class="actions-tier-label">Elite Actions</div>
|
|
<div class="actions-list">
|
|
{% for action in actions.elite %}
|
|
{% set available = action.context == ['any'] or location.location_type in action.context %}
|
|
{% set locked = user_tier != 'elite' %}
|
|
<button class="action-btn {% if locked %}action-btn--locked{% else %}action-btn--elite{% endif %}"
|
|
{% if not locked %}
|
|
hx-post="{{ url_for('game.take_action', session_id=session_id) }}"
|
|
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
|
|
hx-target="#narrative-content"
|
|
hx-swap="innerHTML"
|
|
hx-disabled-elt="this"
|
|
{% endif %}
|
|
{% if locked %}disabled title="Requires Elite tier"{% elif not available %}disabled title="Not available in this location"{% endif %}>
|
|
<span class="action-btn-text">{{ action.display_text }}</span>
|
|
{% if locked %}
|
|
<span class="action-btn-lock">🔒</span>
|
|
{% elif action.cooldown %}
|
|
<span class="action-btn-cooldown" title="{{ action.cooldown }} turn cooldown">⏱</span>
|
|
{% endif %}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|