first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

0
public_web/docs/.gitkeep Normal file
View File

View File

@@ -0,0 +1,651 @@
# HTMX Integration Patterns - Public Web Frontend
**Last Updated:** November 18, 2025
---
## Overview
This document outlines HTMX usage patterns, best practices, and common implementations for the Code of Conquest web frontend.
**HTMX Benefits:**
- Server-side rendering with dynamic interactions
- No JavaScript framework overhead
- Progressive enhancement (works without JS)
- Clean separation of concerns
- Natural integration with Flask/Jinja2
---
## Core HTMX Attributes
### hx-get / hx-post / hx-put / hx-delete
Make HTTP requests from HTML elements:
```html
<!-- GET request -->
<button hx-get="/api/v1/characters" hx-target="#characters">
Load Characters
</button>
<!-- POST request -->
<form hx-post="/api/v1/characters" hx-target="#result">
<!-- form fields -->
<button type="submit">Create</button>
</form>
<!-- DELETE request -->
<button hx-delete="/api/v1/characters/{{ character_id }}"
hx-target="closest .character-card"
hx-swap="outerHTML">
Delete
</button>
```
### hx-target
Specify where to insert the response:
```html
<!-- CSS selector -->
<button hx-get="/content" hx-target="#content-div">Load</button>
<!-- Relative selectors -->
<button hx-delete="/item" hx-target="closest .item">Delete</button>
<button hx-post="/append" hx-target="next .list">Add</button>
<!-- Special values -->
<button hx-get="/modal" hx-target="body">Show Modal</button>
```
### hx-swap
Control how content is swapped:
```html
<!-- innerHTML (default) -->
<div hx-get="/content" hx-target="#div" hx-swap="innerHTML"></div>
<!-- outerHTML (replace element itself) -->
<div hx-get="/content" hx-target="#div" hx-swap="outerHTML"></div>
<!-- beforebegin (before target) -->
<div hx-get="/content" hx-target="#div" hx-swap="beforebegin"></div>
<!-- afterbegin (first child) -->
<div hx-get="/content" hx-target="#div" hx-swap="afterbegin"></div>
<!-- beforeend (last child) -->
<div hx-get="/content" hx-target="#div" hx-swap="beforeend"></div>
<!-- afterend (after target) -->
<div hx-get="/content" hx-target="#div" hx-swap="afterend"></div>
<!-- none (no swap, just trigger) -->
<div hx-get="/trigger" hx-swap="none"></div>
```
### hx-trigger
Specify what triggers the request:
```html
<!-- Click (default for buttons) -->
<button hx-get="/content">Click Me</button>
<!-- Change (default for inputs) -->
<select hx-get="/filter" hx-trigger="change">...</select>
<!-- Custom events -->
<div hx-get="/content" hx-trigger="customEvent">...</div>
<!-- Multiple triggers -->
<div hx-get="/content" hx-trigger="mouseenter, focus">...</div>
<!-- Trigger modifiers -->
<input hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#results">
<!-- Polling -->
<div hx-get="/status" hx-trigger="every 5s">Status: ...</div>
<!-- Load once -->
<div hx-get="/content" hx-trigger="load">...</div>
```
---
## Common Patterns
### Form Submission
**Pattern:** Submit form via AJAX, replace form with result
```html
<form hx-post="/api/v1/characters"
hx-target="#form-container"
hx-swap="outerHTML">
<input type="text" name="name" placeholder="Character Name" required>
<select name="class">
<option value="vanguard">Vanguard</option>
<option value="luminary">Luminary</option>
</select>
<button type="submit">Create Character</button>
</form>
```
**Backend Response:**
```python
@characters_bp.route('/', methods=['POST'])
def create_character():
# Create character via API
response = api_client.post('/characters', request.form)
if response.status_code == 200:
character = response.json()['result']
return render_template('partials/character_card.html', character=character)
else:
return render_template('partials/form_error.html', error=response.json()['error'])
```
### Delete with Confirmation
**Pattern:** Confirm before deleting, remove element on success
```html
<div class="character-card" id="char-{{ character.character_id }}">
<h3>{{ character.name }}</h3>
<button hx-delete="/api/v1/characters/{{ character.character_id }}"
hx-confirm="Are you sure you want to delete {{ character.name }}?"
hx-target="closest .character-card"
hx-swap="outerHTML swap:1s">
Delete
</button>
</div>
```
**Backend Response (empty for delete):**
```python
@characters_bp.route('/<character_id>', methods=['DELETE'])
def delete_character(character_id):
api_client.delete(f'/characters/{character_id}')
return '', 200 # Empty response removes element
```
### Search/Filter
**Pattern:** Live search with debouncing
```html
<input type="text"
name="search"
placeholder="Search characters..."
hx-get="/characters/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#character-list"
hx-indicator="#search-spinner">
<span id="search-spinner" class="htmx-indicator">Searching...</span>
<div id="character-list">
<!-- Results appear here -->
</div>
```
**Backend:**
```python
@characters_bp.route('/search')
def search_characters():
query = request.args.get('search', '')
characters = api_client.get(f'/characters?search={query}')
return render_template('partials/character_list.html', characters=characters)
```
### Pagination
**Pattern:** Load more items
```html
<div id="character-list">
{% for character in characters %}
{% include 'partials/character_card.html' %}
{% endfor %}
</div>
{% if has_more %}
<button hx-get="/characters?page={{ page + 1 }}"
hx-target="#character-list"
hx-swap="beforeend"
hx-select=".character-card">
Load More
</button>
{% endif %}
```
### Inline Edit
**Pattern:** Click to edit, save inline
```html
<div class="character-name" id="name-{{ character.character_id }}">
<span hx-get="/characters/{{ character.character_id }}/edit-name"
hx-target="closest div"
hx-swap="outerHTML">
{{ character.name }} ✏️
</span>
</div>
```
**Edit Form Response:**
```html
<div class="character-name" id="name-{{ character.character_id }}">
<form hx-put="/characters/{{ character.character_id }}/name"
hx-target="closest div"
hx-swap="outerHTML">
<input type="text" name="name" value="{{ character.name }}" autofocus>
<button type="submit">Save</button>
<button type="button" hx-get="/characters/{{ character.character_id }}/name" hx-target="closest div" hx-swap="outerHTML">Cancel</button>
</form>
</div>
```
### Modal Dialog
**Pattern:** Load modal content dynamically
```html
<button hx-get="/characters/{{ character.character_id }}/details"
hx-target="#modal-container"
hx-swap="innerHTML">
View Details
</button>
<div id="modal-container"></div>
```
**Modal Response:**
```html
<div class="modal" id="character-modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<h2>{{ character.name }}</h2>
<!-- character details -->
</div>
</div>
<script>
function closeModal() {
document.getElementById('character-modal').remove();
}
</script>
```
### Tabs
**Pattern:** Tab switching without page reload
```html
<div class="tabs">
<button hx-get="/dashboard/overview"
hx-target="#tab-content"
class="tab active">
Overview
</button>
<button hx-get="/dashboard/characters"
hx-target="#tab-content"
class="tab">
Characters
</button>
<button hx-get="/dashboard/sessions"
hx-target="#tab-content"
class="tab">
Sessions
</button>
</div>
<div id="tab-content">
<!-- Tab content loads here -->
</div>
```
### Polling for Updates
**Pattern:** Auto-refresh session status
```html
<div hx-get="/sessions/{{ session_id }}/status"
hx-trigger="every 5s"
hx-target="#session-status">
Loading status...
</div>
```
**Conditional Polling (stop when complete):**
```html
<div hx-get="/sessions/{{ session_id }}/status"
hx-trigger="every 5s[status !== 'completed']"
hx-target="#session-status">
Status: {{ session.status }}
</div>
```
### Infinite Scroll
**Pattern:** Load more as user scrolls
```html
<div id="character-list">
{% for character in characters %}
{% include 'partials/character_card.html' %}
{% endfor %}
</div>
{% if has_more %}
<div hx-get="/characters?page={{ page + 1 }}"
hx-trigger="revealed"
hx-target="#character-list"
hx-swap="beforeend">
Loading more...
</div>
{% endif %}
```
---
## Advanced Patterns
### Dependent Dropdowns
**Pattern:** Update second dropdown based on first selection
```html
<select name="class"
hx-get="/characters/abilities"
hx-target="#ability-select"
hx-trigger="change">
<option value="vanguard">Vanguard</option>
<option value="luminary">Luminary</option>
</select>
<div id="ability-select">
<!-- Abilities dropdown loads here based on class -->
</div>
```
### Out of Band Swaps
**Pattern:** Update multiple areas from single response
```html
<button hx-post="/combat/attack"
hx-target="#combat-log"
hx-swap="beforeend">
Attack
</button>
<div id="combat-log"><!-- Combat log --></div>
<div id="character-hp">HP: 50/100</div>
```
**Backend Response:**
```html
<!-- Primary swap target -->
<div>You dealt 15 damage!</div>
<!-- Out of band swap -->
<div id="character-hp" hx-swap-oob="true">HP: 45/100</div>
```
### Optimistic UI
**Pattern:** Show result immediately, revert on error
```html
<button hx-post="/characters/{{ character_id }}/favorite"
hx-target="#favorite-btn"
hx-swap="outerHTML"
class="btn">
⭐ Favorite
</button>
```
**Immediate feedback with CSS:**
```css
.htmx-request .htmx-indicator {
display: inline;
}
```
### Progressive Enhancement
**Pattern:** Fallback to normal form submission
```html
<form action="/characters" method="POST"
hx-post="/characters"
hx-target="#result">
<!-- If HTMX fails, form still works via normal POST -->
<input type="text" name="name">
<button type="submit">Create</button>
</form>
```
---
## HTMX + Appwrite Realtime
### Hybrid Approach
Use HTMX for user actions, Appwrite Realtime for server push updates:
```html
<!-- HTMX for user actions -->
<button hx-post="/sessions/{{ session_id }}/ready"
hx-target="#lobby-status">
Ready
</button>
<!-- Appwrite Realtime for live updates -->
<script>
const client = new Appwrite.Client()
.setEndpoint('{{ config.appwrite_endpoint }}')
.setProject('{{ config.appwrite_project_id }}');
client.subscribe('channel', response => {
// Trigger HTMX to reload specific section
htmx.ajax('GET', '/sessions/{{ session_id }}/status', {
target: '#session-status',
swap: 'innerHTML'
});
});
</script>
```
---
## Error Handling
### Show Error Messages
```html
<form hx-post="/characters"
hx-target="#form-container"
hx-target-error="#error-container">
<!-- form fields -->
</form>
<div id="error-container"></div>
```
**Backend Error Response:**
```python
@characters_bp.route('/', methods=['POST'])
def create_character():
try:
# Create character
pass
except ValidationError as e:
response = make_response(render_template('partials/error.html', error=str(e)), 400)
response.headers['HX-Retarget'] = '#error-container'
return response
```
### Retry on Failure
```html
<div hx-get="/api/v1/data"
hx-trigger="load, error from:body delay:5s">
Loading...
</div>
```
---
## Loading Indicators
### Global Indicator
```html
<style>
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline;
}
</style>
<button hx-post="/action" hx-indicator="#spinner">
Submit
</button>
<span id="spinner" class="htmx-indicator">Loading...</span>
```
### Inline Indicator
```html
<button hx-get="/content">
<span class="button-text">Load</span>
<span class="htmx-indicator">Loading...</span>
</button>
```
---
## Best Practices
### 1. Use Semantic HTML
```html
<!-- Good -->
<button hx-post="/action">Submit</button>
<!-- Avoid -->
<div hx-post="/action">Submit</div>
```
### 2. Provide Fallbacks
```html
<form action="/submit" method="POST" hx-post="/submit">
<!-- Works without JavaScript -->
</form>
```
### 3. Use hx-indicator for Loading States
```html
<button hx-post="/action" hx-indicator="#spinner">
Submit
</button>
<span id="spinner" class="htmx-indicator"></span>
```
### 4. Debounce Search Inputs
```html
<input hx-get="/search"
hx-trigger="keyup changed delay:500ms">
```
### 5. Use CSRF Protection
```html
<form hx-post="/action">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
</form>
```
---
## Debugging
### HTMX Events
Listen to HTMX events for debugging:
```html
<script>
document.body.addEventListener('htmx:beforeRequest', (event) => {
console.log('Before request:', event.detail);
});
document.body.addEventListener('htmx:afterRequest', (event) => {
console.log('After request:', event.detail);
});
document.body.addEventListener('htmx:responseError', (event) => {
console.error('Response error:', event.detail);
});
</script>
```
### HTMX Logger Extension
```html
<script src="https://unpkg.com/htmx.org/dist/ext/debug.js"></script>
<body hx-ext="debug">
```
---
## Performance Tips
### 1. Use hx-select to Extract Partial
```html
<button hx-get="/full-page"
hx-select="#content-section"
hx-target="#result">
Load Section
</button>
```
### 2. Disable During Request
```html
<button hx-post="/action" hx-disable-during-request>
Submit
</button>
```
### 3. Use hx-sync for Sequential Requests
```html
<div hx-sync="this:replace">
<button hx-get="/content">Load</button>
</div>
```
---
## Related Documentation
- **[TEMPLATES.md](TEMPLATES.md)** - Template structure and conventions
- **[TESTING.md](TESTING.md)** - Manual testing guide
- **[/api/docs/API_REFERENCE.md](../../api/docs/API_REFERENCE.md)** - API endpoints
- **[HTMX Official Docs](https://htmx.org/docs/)** - Complete HTMX documentation
---
**Document Version:** 1.1
**Created:** November 18, 2025
**Last Updated:** November 21, 2025

View File

@@ -0,0 +1,738 @@
# Multiplayer System - Web Frontend
**Status:** Planned
**Phase:** 6 (Multiplayer Sessions)
**Last Updated:** November 18, 2025
---
## Overview
The Web Frontend handles the UI/UX for multiplayer sessions, including lobby screens, active session displays, combat interfaces, and realtime synchronization via JavaScript/HTMX patterns.
**Frontend Responsibilities:**
- Render multiplayer session creation forms
- Display lobby with player list and ready status
- Show active session UI (timer, party status, combat)
- Handle realtime updates via Appwrite Realtime WebSocket
- Submit player actions to API backend
- Display session completion and rewards
**Technical Stack:**
- **Templates**: Jinja2
- **Interactivity**: HTMX for AJAX interactions
- **Realtime**: Appwrite JavaScript SDK for WebSocket subscriptions
- **Styling**: Custom CSS (responsive design)
---
## UI/UX Design
### Session Creation Screen (Host)
**Route:** `/multiplayer/create`
**Template:** `templates/multiplayer/create.html`
```html
{% extends "base.html" %}
{% block content %}
<div class="multiplayer-create">
<h1>Create Multiplayer Session <span class="badge-premium">Premium</span></h1>
<p>Invite your friends to a 2-hour co-op adventure!</p>
<form hx-post="/api/v1/sessions/multiplayer/create"
hx-target="#session-result"
hx-swap="innerHTML">
<div class="form-group">
<label>Party Size:</label>
<div class="radio-group">
<input type="radio" name="max_players" value="2" id="size-2">
<label for="size-2">2 Players</label>
<input type="radio" name="max_players" value="3" id="size-3">
<label for="size-3">3 Players</label>
<input type="radio" name="max_players" value="4" id="size-4" checked>
<label for="size-4">4 Players</label>
</div>
</div>
<div class="form-group">
<label>Difficulty:</label>
<div class="radio-group">
<input type="radio" name="difficulty" value="easy" id="diff-easy">
<label for="diff-easy">Easy</label>
<input type="radio" name="difficulty" value="medium" id="diff-medium" checked>
<label for="diff-medium">Medium</label>
<input type="radio" name="difficulty" value="hard" id="diff-hard">
<label for="diff-hard">Hard</label>
<input type="radio" name="difficulty" value="deadly" id="diff-deadly">
<label for="diff-deadly">Deadly</label>
</div>
</div>
<p class="info-text">
AI will generate a custom campaign for your party based on your
characters' levels and the selected difficulty.
</p>
<button type="submit" class="btn btn-primary">Create Session</button>
</form>
<div id="session-result"></div>
</div>
{% endblock %}
```
**HTMX Pattern:**
- Form submission via `hx-post` to API endpoint
- Response replaces `#session-result` div
- API returns session details and redirects to lobby
### Lobby Screen
**Route:** `/multiplayer/lobby/{session_id}`
**Template:** `templates/multiplayer/lobby.html`
```html
{% extends "base.html" %}
{% block content %}
<div class="multiplayer-lobby">
<h1>Multiplayer Lobby
<button class="btn btn-secondary"
hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/leave"
hx-confirm="Are you sure you want to leave?">
Leave Lobby
</button>
</h1>
<div class="invite-section">
<p><strong>Session Code:</strong> {{ session.invite_code }}</p>
<p>
<strong>Invite Link:</strong>
<input type="text"
id="invite-link"
value="https://codeofconquest.com/join/{{ session.invite_code }}"
readonly>
<button onclick="copyInviteLink()" class="btn btn-sm">Copy Link</button>
</p>
</div>
<div class="lobby-info">
<p><strong>Difficulty:</strong> {{ session.campaign.difficulty|title }}</p>
<p><strong>Duration:</strong> 2 hours</p>
</div>
<div id="party-list" class="party-list">
<!-- Dynamically updated via realtime -->
{% for member in session.party_members %}
<div class="party-member" data-user-id="{{ member.user_id }}">
{% if member.is_host %}
<span class="crown">👑</span>
{% endif %}
<span class="username">{{ member.username }} {% if member.is_host %}(Host){% endif %}</span>
<span class="character">{{ member.character_snapshot.name }} - {{ member.character_snapshot.player_class.name }} Lvl {{ member.character_snapshot.level }}</span>
<span class="ready-status">
{% if member.is_ready %}
✅ Ready
{% else %}
⏳ Not Ready
{% endif %}
</span>
</div>
{% endfor %}
<!-- Empty slots -->
{% for i in range(session.max_players - session.party_members|length) %}
<div class="party-member empty">
<span>[Waiting for player...]</span>
</div>
{% endfor %}
</div>
<div class="lobby-actions">
{% if current_user.user_id == session.host_user_id %}
<!-- Host controls -->
<button id="start-session-btn"
class="btn btn-primary"
hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/start"
hx-target="#lobby-status"
{% if not all_ready %}disabled{% endif %}>
Start Session
</button>
{% else %}
<!-- Player ready toggle -->
<button class="btn btn-primary"
hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/ready"
hx-target="#lobby-status">
{% if current_member.is_ready %}Not Ready{% else %}Ready{% endif %}
</button>
{% endif %}
</div>
<div id="lobby-status"></div>
</div>
<script>
// Realtime subscription for lobby updates
const { Client, Databases } = Appwrite;
const client = new Client()
.setEndpoint('{{ appwrite_endpoint }}')
.setProject('{{ appwrite_project_id }}');
const databases = new Databases(client);
// Subscribe to session updates
client.subscribe(`databases.{{ db_id }}.collections.multiplayer_sessions.documents.{{ session.session_id }}`, response => {
console.log('Session updated:', response);
// Update party list
updatePartyList(response.payload);
// Check if all ready, enable/disable start button
checkAllReady(response.payload);
// If session started, redirect to active session
if (response.payload.status === 'active') {
window.location.href = '/multiplayer/session/{{ session.session_id }}';
}
});
function updatePartyList(sessionData) {
// Update party member ready status dynamically
sessionData.party_members.forEach(member => {
const memberEl = document.querySelector(`.party-member[data-user-id="${member.user_id}"]`);
if (memberEl) {
const statusEl = memberEl.querySelector('.ready-status');
statusEl.innerHTML = member.is_ready ? '✅ Ready' : '⏳ Not Ready';
}
});
}
function checkAllReady(sessionData) {
const allReady = sessionData.party_members.every(m => m.is_ready);
const startBtn = document.getElementById('start-session-btn');
if (startBtn) {
startBtn.disabled = !allReady;
}
}
function copyInviteLink() {
const linkInput = document.getElementById('invite-link');
linkInput.select();
document.execCommand('copy');
alert('Invite link copied to clipboard!');
}
</script>
{% endblock %}
```
**Realtime Pattern:**
- JavaScript subscribes to Appwrite Realtime for session updates
- Updates player ready status dynamically
- Redirects to active session when host starts
- No page reload required
### Active Session Screen
**Route:** `/multiplayer/session/{session_id}`
**Template:** `templates/multiplayer/session.html`
```html
{% extends "base.html" %}
{% block content %}
<div class="multiplayer-session">
<div class="session-header">
<h1>{{ session.campaign.title }}</h1>
<div class="timer" id="session-timer">
⏱️ <span id="time-remaining">{{ time_remaining }}</span> Remaining
</div>
</div>
<div class="campaign-progress">
<p>Campaign Progress:
<span class="progress-bar">
<span class="progress-fill" style="width: {{ (session.current_encounter_index / session.campaign.encounters|length) * 100 }}%"></span>
</span>
({{ session.current_encounter_index }}/{{ session.campaign.encounters|length }} encounters)
</p>
</div>
<div class="party-status">
<h3>Party:</h3>
<div class="party-members" id="party-status">
{% for member in session.party_members %}
<div class="party-member-status">
<span class="name">{{ member.character_snapshot.name }}</span>
<span class="hp">HP: <span id="hp-{{ member.character_id }}">{{ member.character_snapshot.current_hp }}</span>/{{ member.character_snapshot.max_hp }}</span>
{% if not member.is_connected %}
<span class="disconnected">⚠️ Disconnected</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<div class="narrative-panel" id="narrative-panel">
<h3>Narrative:</h3>
<div class="conversation-history">
{% for entry in session.conversation_history %}
<div class="conversation-entry {{ entry.role }}">
<strong>{{ entry.role|title }}:</strong> {{ entry.content }}
</div>
{% endfor %}
</div>
</div>
{% if session.combat_encounter %}
<!-- Combat UI -->
<div class="combat-section" id="combat-section">
<h3>Combat: {{ current_encounter.title }}</h3>
<div class="turn-order" id="turn-order">
<h4>Turn Order:</h4>
<ol>
{% for combatant_id in session.combat_encounter.turn_order %}
<li class="{% if loop.index0 == session.combat_encounter.current_turn_index %}current-turn{% endif %}">
{{ get_combatant_name(combatant_id) }}
{% if is_current_user_turn(combatant_id) %}
<strong>(Your Turn!)</strong>
{% endif %}
</li>
{% endfor %}
</ol>
</div>
{% if is_current_user_turn() %}
<!-- Combat action form (only visible on player's turn) -->
<div class="combat-actions">
<h4>Your Action:</h4>
<form hx-post="/api/v1/sessions/multiplayer/{{ session.session_id }}/combat/action"
hx-target="#combat-result"
hx-swap="innerHTML">
<button type="submit" name="action_type" value="attack" class="btn btn-action">Attack</button>
<select name="ability_id" class="ability-select">
<option value="">Use Ability ▼</option>
{% for ability in current_character.abilities %}
<option value="{{ ability.ability_id }}">{{ ability.name }}</option>
{% endfor %}
</select>
<select name="item_id" class="item-select">
<option value="">Use Item ▼</option>
{% for item in current_character.inventory %}
<option value="{{ item.item_id }}">{{ item.name }}</option>
{% endfor %}
</select>
<button type="submit" name="action_type" value="defend" class="btn btn-action">Defend</button>
</form>
<div id="combat-result"></div>
</div>
{% else %}
<p class="waiting-turn">Waiting for other players...</p>
{% endif %}
</div>
{% endif %}
</div>
<script>
// Realtime subscription for combat and session updates
const client = new Client()
.setEndpoint('{{ appwrite_endpoint }}')
.setProject('{{ appwrite_project_id }}');
// Subscribe to session updates
client.subscribe(`databases.{{ db_id }}.collections.multiplayer_sessions.documents.{{ session.session_id }}`, response => {
console.log('Session updated:', response);
// Update timer
updateTimer(response.payload.time_remaining_seconds);
// Update party status
updatePartyStatus(response.payload.party_members);
// Update combat state
if (response.payload.combat_encounter) {
updateCombatUI(response.payload.combat_encounter);
}
// Check for session expiration
if (response.payload.status === 'expired' || response.payload.status === 'completed') {
window.location.href = `/multiplayer/complete/{{ session.session_id }}`;
}
});
// Timer countdown
let timeRemaining = {{ session.time_remaining_seconds }};
setInterval(() => {
if (timeRemaining > 0) {
timeRemaining--;
updateTimerDisplay(timeRemaining);
}
}, 1000);
function updateTimerDisplay(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
const display = `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
document.getElementById('time-remaining').innerText = display;
// Warnings
if (seconds === 600) alert('⚠️ 10 minutes remaining!');
if (seconds === 300) alert('⚠️ 5 minutes remaining!');
if (seconds === 60) alert('🚨 1 minute remaining!');
}
function updateTimer(seconds) {
timeRemaining = seconds;
}
function updatePartyStatus(partyMembers) {
partyMembers.forEach(member => {
const hpEl = document.getElementById(`hp-${member.character_id}`);
if (hpEl) {
hpEl.innerText = member.character_snapshot.current_hp;
}
});
}
function updateCombatUI(combat) {
// Update turn order highlighting
const turnItems = document.querySelectorAll('.turn-order li');
turnItems.forEach((item, index) => {
item.classList.toggle('current-turn', index === combat.current_turn_index);
});
// Reload page section if turn changed (HTMX can handle partial updates)
htmx.ajax('GET', `/multiplayer/session/{{ session.session_id }}/combat-ui`, {target: '#combat-section', swap: 'outerHTML'});
}
</script>
{% endblock %}
```
**Realtime Pattern:**
- Subscribe to session document changes
- Update timer countdown locally
- Update party HP dynamically
- Reload combat UI section when turn changes
- Redirect to completion screen when session ends
### Session Complete Screen
**Route:** `/multiplayer/complete/{session_id}`
**Template:** `templates/multiplayer/complete.html`
```html
{% extends "base.html" %}
{% block content %}
<div class="session-complete">
<h1>Campaign Complete!</h1>
<div class="completion-banner">
<h2>🎉 {{ session.campaign.title }} - VICTORY! 🎉</h2>
<p>{{ session.campaign.description }}</p>
</div>
<div class="completion-stats">
<p><strong>Time:</strong> {{ session_duration }} (Completion Bonus: +{{ completion_bonus_percent }}%)</p>
</div>
<div class="rewards-summary">
<h3>Rewards:</h3>
<ul>
<li>💰 {{ rewards.gold_per_player }} gold ({{ base_gold }} + {{ bonus_gold }} bonus)</li>
<li>⭐ {{ rewards.experience_per_player }} XP ({{ base_xp }} + {{ bonus_xp }} bonus)</li>
<li>📦 Loot: {{ rewards.shared_items|join(', ') }}</li>
</ul>
{% if leveled_up %}
<p class="level-up">🎊 Level Up! {{ current_character.name }} reached level {{ current_character.level }}!</p>
{% endif %}
</div>
<div class="party-stats">
<h3>Party Members:</h3>
<ul>
{% for member in session.party_members %}
<li>
<strong>{{ member.username }}</strong> ({{ member.character_snapshot.name }})
{% if member.mvp_badge %}
- {{ member.mvp_badge }}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<div class="actions">
<a href="/sessions/history/{{ session.session_id }}" class="btn btn-secondary">View Full Session Log</a>
<a href="/dashboard" class="btn btn-primary">Return to Main Menu</a>
</div>
</div>
{% endblock %}
```
---
## HTMX Integration Patterns
### Form Submissions
All multiplayer actions use HTMX for seamless AJAX submissions:
**Pattern:**
```html
<form hx-post="/api/v1/sessions/multiplayer/{session_id}/action"
hx-target="#result-div"
hx-swap="innerHTML">
<!-- form fields -->
<button type="submit">Submit</button>
</form>
```
**Benefits:**
- No page reload
- Partial page updates
- Progressive enhancement (works without JS)
### Realtime Updates
Combine HTMX with Appwrite Realtime for optimal UX:
**Pattern:**
```javascript
// Listen for realtime events
client.subscribe(`channel`, response => {
// Update via HTMX partial reload
htmx.ajax('GET', '/partial-url', {
target: '#target-div',
swap: 'outerHTML'
});
});
```
### Polling Fallback
For browsers without WebSocket support, use HTMX polling:
```html
<div hx-get="/api/v1/sessions/multiplayer/{session_id}/status"
hx-trigger="every 5s"
hx-target="#status-div">
<!-- Status updates every 5 seconds -->
</div>
```
---
## Template Organization
### Directory Structure
```
templates/multiplayer/
├── create.html # Session creation form
├── lobby.html # Lobby screen
├── session.html # Active session UI
├── complete.html # Session complete screen
├── partials/
│ ├── party_list.html # Reusable party member list
│ ├── combat_ui.html # Combat interface partial
│ └── timer.html # Timer component
└── components/
├── invite_link.html # Invite link copy widget
└── ready_toggle.html # Ready status toggle button
```
### Partial Template Pattern
**Example:** `templates/multiplayer/partials/party_list.html`
```html
<!-- Reusable party list component -->
<div class="party-list">
{% for member in party_members %}
<div class="party-member" data-user-id="{{ member.user_id }}">
{% if member.is_host %}
<span class="crown">👑</span>
{% endif %}
<span class="username">{{ member.username }}</span>
<span class="character">{{ member.character_snapshot.name }} - {{ member.character_snapshot.player_class.name }} Lvl {{ member.character_snapshot.level }}</span>
<span class="ready-status">
{% if member.is_ready %}✅ Ready{% else %}⏳ Not Ready{% endif %}
</span>
</div>
{% endfor %}
</div>
```
**Usage:**
```html
{% include 'multiplayer/partials/party_list.html' with party_members=session.party_members %}
```
---
## JavaScript/Appwrite Realtime
### Setup
**Base template** (`templates/base.html`):
```html
<head>
<!-- Appwrite SDK -->
<script src="https://cdn.jsdelivr.net/npm/appwrite@latest"></script>
</head>
```
### Subscription Patterns
**Session Updates:**
```javascript
const client = new Appwrite.Client()
.setEndpoint('{{ config.appwrite_endpoint }}')
.setProject('{{ config.appwrite_project_id }}');
// Subscribe to specific session
client.subscribe(
`databases.{{ db_id }}.collections.multiplayer_sessions.documents.{{ session_id }}`,
response => {
console.log('Session updated:', response.payload);
// Handle different event types
switch(response.events[0]) {
case 'databases.*.collections.*.documents.*.update':
handleSessionUpdate(response.payload);
break;
}
}
);
```
**Combat Updates:**
```javascript
client.subscribe(
`databases.{{ db_id }}.collections.combat_encounters.documents.{{ encounter_id }}`,
response => {
updateCombatUI(response.payload);
}
);
```
### Error Handling
```javascript
client.subscribe(channel, response => {
// Handle updates
}, error => {
console.error('Realtime error:', error);
// Fallback to polling
startPolling();
});
function startPolling() {
setInterval(() => {
htmx.ajax('GET', '/api/v1/sessions/multiplayer/{{ session_id }}', {
target: '#session-container',
swap: 'outerHTML'
});
}, 5000);
}
```
---
## View Layer Implementation
### Flask View Functions
**Create Session View:**
```python
@multiplayer_bp.route('/create', methods=['GET', 'POST'])
@require_auth
def create_session():
"""Render session creation form."""
if request.method == 'POST':
# Forward to API backend
response = requests.post(
f"{API_BASE_URL}/api/v1/sessions/multiplayer/create",
json=request.form.to_dict(),
headers={"Authorization": f"Bearer {session['auth_token']}"}
)
if response.status_code == 200:
session_data = response.json()['result']
return redirect(url_for('multiplayer.lobby', session_id=session_data['session_id']))
else:
flash('Failed to create session', 'error')
return render_template('multiplayer/create.html')
@multiplayer_bp.route('/lobby/<session_id>')
@require_auth
def lobby(session_id):
"""Display lobby screen."""
# Fetch session from API
response = requests.get(
f"{API_BASE_URL}/api/v1/sessions/multiplayer/{session_id}",
headers={"Authorization": f"Bearer {session['auth_token']}"}
)
session_data = response.json()['result']
return render_template('multiplayer/lobby.html', session=session_data)
```
---
## Testing Checklist
### Manual Testing Tasks
- [ ] Session creation form submits correctly
- [ ] Invite link copies to clipboard
- [ ] Lobby updates when players join
- [ ] Ready status toggles work
- [ ] Host can start session when all ready
- [ ] Timer displays and counts down correctly
- [ ] Party HP updates during combat
- [ ] Combat actions submit correctly
- [ ] Turn order highlights current player
- [ ] Realtime updates work across multiple browsers
- [ ] Session completion screen displays rewards
- [ ] Disconnection handling shows warnings
---
## Related Documentation
- **[/api/docs/MULTIPLAYER.md](../../api/docs/MULTIPLAYER.md)** - Backend API endpoints and business logic
- **[TEMPLATES.md](TEMPLATES.md)** - Template structure and conventions
- **[HTMX_PATTERNS.md](HTMX_PATTERNS.md)** - HTMX integration patterns
- **[TESTING.md](TESTING.md)** - Manual testing guide
---
**Document Version:** 1.0 (Microservices Split)
**Created:** November 18, 2025
**Last Updated:** November 18, 2025

25
public_web/docs/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Public Web Frontend Documentation
This folder contains documentation specific to the public web frontend service.
## Documents
- **[TEMPLATES.md](TEMPLATES.md)** - Template structure, naming conventions, and Jinja2 best practices
- **[HTMX_PATTERNS.md](HTMX_PATTERNS.md)** - HTMX integration patterns and dynamic UI updates
- **[TESTING.md](TESTING.md)** - Manual testing checklist and browser testing guide
- **[MULTIPLAYER.md](MULTIPLAYER.md)** - Multiplayer lobby and session UI implementation
## Quick Reference
**Service Role:** Thin UI layer that makes HTTP requests to API backend
**Tech Stack:** Flask + Jinja2 + HTMX + Vanilla CSS
**Port:** 5001 (development), 8080 (production)
## Related Documentation
- **[../CLAUDE.md](../CLAUDE.md)** - Web frontend development guidelines
- **[../README.md](../README.md)** - Setup and usage guide
- **[../../docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md)** - System architecture overview
- **[../../api/docs/API_REFERENCE.md](../../api/docs/API_REFERENCE.md)** - API endpoints to call

View File

@@ -0,0 +1,431 @@
# Template Structure and Conventions - Public Web Frontend
**Last Updated:** November 18, 2025
---
## Overview
This document outlines the template structure, naming conventions, and best practices for Jinja2 templates in the Code of Conquest web frontend.
**Template Philosophy:**
- Clean, semantic HTML
- Separation of concerns (templates, styles, scripts)
- Reusable components via includes and macros
- Responsive design patterns
- Accessibility-first
---
## Directory Structure
```
templates/
├── base.html # Base template (all pages extend this)
├── errors/ # Error pages
│ ├── 404.html
│ ├── 500.html
│ └── 403.html
├── auth/ # Authentication pages
│ ├── login.html
│ ├── register.html
│ └── forgot_password.html
├── dashboard/ # User dashboard
│ └── index.html
├── characters/ # Character management
│ ├── list.html
│ ├── create.html
│ ├── view.html
│ └── edit.html
├── sessions/ # Game sessions
│ ├── create.html
│ ├── active.html
│ ├── history.html
│ └── view.html
├── multiplayer/ # Multiplayer sessions
│ ├── create.html
│ ├── lobby.html
│ ├── session.html
│ └── complete.html
├── partials/ # Reusable partial templates
│ ├── navigation.html
│ ├── footer.html
│ ├── character_card.html
│ ├── combat_ui.html
│ └── session_summary.html
├── components/ # Reusable UI components (macros)
│ ├── forms.html
│ ├── buttons.html
│ ├── alerts.html
│ └── modals.html
└── macros/ # Jinja2 macros
├── form_fields.html
└── ui_elements.html
```
---
## Base Template
**File:** `templates/base.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Code of Conquest{% endblock %}</title>
<!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
{% block extra_css %}{% endblock %}
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- Appwrite SDK (for realtime) -->
<script src="https://cdn.jsdelivr.net/npm/appwrite@latest"></script>
{% block extra_head %}{% endblock %}
</head>
<body>
{% include 'partials/navigation.html' %}
<main class="container">
{% block flash_messages %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="alerts">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% endblock %}
{% block content %}
<!-- Page content goes here -->
{% endblock %}
</main>
{% include 'partials/footer.html' %}
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
```
---
## Template Naming Conventions
### File Names
- Use lowercase with underscores: `character_list.html`, `session_create.html`
- Partial templates prefix with underscore: `_card.html`, `_form.html` (optional)
- Component files describe what they contain: `forms.html`, `buttons.html`
### Template Variables
- Use snake_case: `character`, `session_data`, `user_info`
- Prefix collections with descriptive names: `characters_list`, `sessions_active`
- Boolean flags use `is_` or `has_` prefix: `is_authenticated`, `has_premium`
### Block Names
- Use descriptive names: `{% block sidebar %}`, `{% block page_header %}`
- Common blocks:
- `title` - Page title
- `content` - Main content area
- `extra_css` - Additional CSS files
- `extra_js` - Additional JavaScript files
- `extra_head` - Additional head elements
---
## Template Patterns
### Extending Base Template
```html
{% extends "base.html" %}
{% block title %}Character List - Code of Conquest{% endblock %}
{% block content %}
<div class="character-list">
<h1>Your Characters</h1>
<!-- Content -->
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/characters.js') }}"></script>
{% endblock %}
```
### Including Partial Templates
```html
{% include 'partials/character_card.html' with character=char %}
```
**Or without context:**
```html
{% include 'partials/navigation.html' %}
```
### Using Macros
**Define macro** in `templates/macros/form_fields.html`:
```html
{% macro text_input(name, label, value="", required=False, placeholder="") %}
<div class="form-group">
<label for="{{ name }}">{{ label }}{% if required %} <span class="required">*</span>{% endif %}</label>
<input type="text"
id="{{ name }}"
name="{{ name }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}>
</div>
{% endmacro %}
```
**Use macro:**
```html
{% from 'macros/form_fields.html' import text_input %}
<form>
{{ text_input('character_name', 'Character Name', required=True, placeholder='Enter name') }}
</form>
```
### Conditional Rendering
```html
{% if user.is_authenticated %}
<p>Welcome, {{ user.username }}!</p>
{% else %}
<a href="{{ url_for('auth.login') }}">Login</a>
{% endif %}
```
### Loops
```html
<div class="character-grid">
{% for character in characters %}
<div class="character-card">
<h3>{{ character.name }}</h3>
<p>Level {{ character.level }} {{ character.player_class.name }}</p>
</div>
{% else %}
<p>No characters found. <a href="{{ url_for('characters.create') }}">Create one</a>?</p>
{% endfor %}
</div>
```
---
## HTMX Integration in Templates
### Basic HTMX Attributes
```html
<!-- Form submission via HTMX -->
<form hx-post="{{ url_for('characters.create') }}"
hx-target="#character-list"
hx-swap="beforeend">
<!-- form fields -->
<button type="submit">Create Character</button>
</form>
```
### HTMX with Confirmation
```html
<button hx-delete="{{ url_for('characters.delete', character_id=char.character_id) }}"
hx-confirm="Are you sure you want to delete this character?"
hx-target="closest .character-card"
hx-swap="outerHTML">
Delete
</button>
```
### HTMX Polling
```html
<div hx-get="{{ url_for('sessions.status', session_id=session.session_id) }}"
hx-trigger="every 5s"
hx-target="#session-status">
Loading...
</div>
```
---
## Component Patterns
### Character Card Component
**File:** `templates/partials/character_card.html`
```html
<div class="character-card" data-character-id="{{ character.character_id }}">
<div class="card-header">
<h3>{{ character.name }}</h3>
<span class="level-badge">Lvl {{ character.level }}</span>
</div>
<div class="card-body">
<p class="class">{{ character.player_class.name }}</p>
<p class="stats">
HP: {{ character.current_hp }}/{{ character.max_hp }} |
Gold: {{ character.gold }}
</p>
</div>
<div class="card-actions">
<a href="{{ url_for('characters.view', character_id=character.character_id) }}" class="btn btn-primary">View</a>
<a href="{{ url_for('sessions.create', character_id=character.character_id) }}" class="btn btn-secondary">Play</a>
</div>
</div>
```
**Usage:**
```html
{% for character in characters %}
{% include 'partials/character_card.html' with character=character %}
{% endfor %}
```
### Alert Component Macro
**File:** `templates/components/alerts.html`
```html
{% macro alert(message, category='info', dismissible=True) %}
<div class="alert alert-{{ category }}{% if dismissible %} alert-dismissible{% endif %}" role="alert">
{{ message }}
{% if dismissible %}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
{% endif %}
</div>
{% endmacro %}
```
**Usage:**
```html
{% from 'components/alerts.html' import alert %}
{{ alert('Character created successfully!', 'success') }}
{{ alert('Invalid character name', 'error') }}
```
---
## Accessibility Guidelines
### Semantic HTML
- Use proper heading hierarchy (`<h1>`, `<h2>`, etc.)
- Use `<nav>`, `<main>`, `<article>`, `<section>` elements
- Use `<button>` for actions, `<a>` for navigation
### ARIA Labels
```html
<button aria-label="Delete character">
<span class="icon icon-trash"></span>
</button>
<nav aria-label="Main navigation">
<!-- navigation links -->
</nav>
```
### Form Labels
```html
<label for="character-name">Character Name</label>
<input type="text" id="character-name" name="character_name">
```
### Focus Management
```html
<button class="btn" autofocus>Primary Action</button>
```
---
## Best Practices
### 1. Keep Templates DRY (Don't Repeat Yourself)
- Use includes for repeated sections
- Create macros for reusable components
- Extend base template consistently
### 2. Separate Logic from Presentation
- Avoid complex Python logic in templates
- Use template filters instead of inline calculations
- Pass fully-formed data from views
### 3. Use Template Comments
```html
{# This is a Jinja2 comment, not rendered in HTML #}
<!-- This is an HTML comment, visible in source -->
```
### 4. Whitespace Control
```html
{% for item in items -%}
<li>{{ item }}</li>
{%- endfor %}
```
### 5. Template Inheritance Hierarchy
```
base.html
├── dashboard/base.html (extends base.html)
│ ├── dashboard/index.html
│ └── dashboard/profile.html
└── sessions/base.html (extends base.html)
├── sessions/create.html
└── sessions/active.html
```
---
## Testing Templates
### Manual Checklist
- [ ] Templates extend base.html correctly
- [ ] All blocks are properly closed
- [ ] Variables are defined before use
- [ ] Forms have CSRF protection
- [ ] Links use `url_for()` instead of hardcoded paths
- [ ] Images have alt text
- [ ] Buttons have descriptive text or aria-labels
- [ ] Mobile responsive (test at 320px, 768px, 1024px)
### Template Linting
- Use `djLint` for Jinja2 template linting
- Ensure consistent indentation (2 or 4 spaces)
- Validate HTML with W3C validator
---
## Related Documentation
- **[HTMX_PATTERNS.md](HTMX_PATTERNS.md)** - HTMX integration patterns
- **[TESTING.md](TESTING.md)** - Manual testing guide
- **[/api/docs/API_REFERENCE.md](../../api/docs/API_REFERENCE.md)** - API endpoints for HTMX calls
---
**Document Version:** 1.0
**Created:** November 18, 2025
**Last Updated:** November 18, 2025

710
public_web/docs/TESTING.md Normal file
View File

@@ -0,0 +1,710 @@
# Manual Testing Guide - Public Web Frontend
**Last Updated:** November 18, 2025
---
## Overview
This document outlines the manual testing procedures for the Code of Conquest web frontend. Since we prefer manual testing over automated UI tests, this guide provides comprehensive checklists for testing all features.
**Testing Philosophy:**
- Test in multiple browsers (Chrome, Firefox, Safari, Edge)
- Test on multiple devices (Desktop, Tablet, Mobile)
- Test with and without JavaScript enabled (progressive enhancement)
- Test realtime features with multiple concurrent users
- Test all HTMX interactions
---
## Testing Environment Setup
### Local Testing
```bash
# Start API backend
cd api
source venv/bin/activate
python wsgi.py # → http://localhost:5000
# Start web frontend
cd public_web
source venv/bin/activate
python wsgi.py # → http://localhost:8000
# Start Redis (for sessions)
docker-compose up redis
# Start Appwrite (for database/auth)
docker-compose up appwrite
```
### Test Browsers
- **Desktop:**
- Chrome (latest)
- Firefox (latest)
- Safari (latest, macOS only)
- Edge (latest)
- **Mobile:**
- Chrome (Android)
- Safari (iOS)
### Test Accounts
Create test accounts for each tier:
- **Free Tier:** `test_free@example.com` / `password123`
- **Basic Tier:** `test_basic@example.com` / `password123`
- **Premium Tier:** `test_premium@example.com` / `password123`
- **Elite Tier:** `test_elite@example.com` / `password123`
---
## Authentication Testing
### Registration
**Test Steps:**
1. Navigate to `/register`
2. Fill in registration form:
- Username: `testuser`
- Email: `testuser@example.com`
- Password: `SecurePassword123!`
- Confirm Password: `SecurePassword123!`
3. Click "Register"
**Expected Results:**
- [ ] Form validates (required fields, email format, password strength)
- [ ] User account created in Appwrite
- [ ] Redirected to `/dashboard`
- [ ] Session cookie set
- [ ] Welcome message displayed
**Error Cases to Test:**
- [ ] Empty fields show validation errors
- [ ] Invalid email format rejected
- [ ] Password mismatch shows error
- [ ] Weak password rejected
- [ ] Duplicate email shows error
### Login
**Test Steps:**
1. Navigate to `/login`
2. Enter credentials
3. Click "Login"
**Expected Results:**
- [ ] User authenticated
- [ ] Redirected to `/dashboard`
- [ ] Session cookie set
- [ ] Navigation shows user menu
**Error Cases:**
- [ ] Invalid credentials show error
- [ ] Empty fields show validation
- [ ] Account lockout after 5 failed attempts (if implemented)
### Logout
**Test Steps:**
1. Click "Logout" in navigation
2. Confirm logout
**Expected Results:**
- [ ] Session destroyed
- [ ] Redirected to `/`
- [ ] Navigation shows "Login" link
- [ ] Cannot access protected pages
---
## Character Management Testing
### Character Creation
**Test Steps:**
1. Navigate to `/characters/create`
2. Fill in character form:
- Name: `Thorin Ironshield`
- Class: `Vanguard`
- Background: `Soldier`
3. Click "Create Character"
**Expected Results:**
- [ ] Character created via API
- [ ] Redirected to `/characters/{character_id}`
- [ ] Character details displayed
- [ ] Character appears in character list
**HTMX Behavior:**
- [ ] Form submits without page reload (if using HTMX)
- [ ] Success message appears
- [ ] Form resets or replaced with character card
**Error Cases:**
- [ ] Name validation (3-50 characters)
- [ ] Required fields enforced
- [ ] Duplicate name handling
### Character List
**Test Steps:**
1. Navigate to `/characters`
2. View character list
**Expected Results:**
- [ ] All user's characters displayed
- [ ] Character cards show: name, class, level, HP
- [ ] "Create Character" button visible
- [ ] Empty state if no characters
**Interactions:**
- [ ] Click character card → `/characters/{id}`
- [ ] Click "Play" → creates session
- [ ] Click "Delete" → confirmation modal
- [ ] Search/filter works (if implemented)
### Character View
**Test Steps:**
1. Navigate to `/characters/{character_id}`
2. View character details
**Expected Results:**
- [ ] Full character sheet displayed
- [ ] Stats, abilities, inventory shown
- [ ] Equipment slots visible
- [ ] Edit/Delete buttons visible (if owner)
### Character Edit
**Test Steps:**
1. Navigate to `/characters/{character_id}/edit`
2. Modify character details
3. Click "Save"
**Expected Results:**
- [ ] Changes saved via API
- [ ] Redirected to character view
- [ ] Updates visible immediately
**Inline Edit (HTMX):**
- [ ] Click name to edit inline
- [ ] Save without page reload
- [ ] Cancel reverts changes
### Character Delete
**Test Steps:**
1. Click "Delete" on character card
2. Confirm deletion
**Expected Results:**
- [ ] Confirmation modal appears
- [ ] Character deleted via API
- [ ] Character removed from list (HTMX removes element)
- [ ] Success message displayed
---
## Session Testing
### Session Creation
**Test Steps:**
1. Navigate to `/sessions/create`
2. Select character
3. Click "Start Session"
**Expected Results:**
- [ ] Session created via API
- [ ] Redirected to `/sessions/{session_id}`
- [ ] Session UI loaded
- [ ] Timer started (if applicable)
### Active Session
**Test Steps:**
1. Navigate to `/sessions/{session_id}`
2. Interact with session UI
**Expected Results:**
- [ ] Character info displayed
- [ ] Location/narrative shown
- [ ] Action buttons visible
- [ ] Conversation history loaded
**Interactions:**
- [ ] Click action button → sends to API
- [ ] Response appears in conversation
- [ ] HTMX updates without page reload
- [ ] Scrolls to latest message
### Combat Session
**Test Steps:**
1. Start combat encounter
2. Take combat action
**Expected Results:**
- [ ] Combat UI displayed
- [ ] Turn order shown
- [ ] Action buttons enabled on player's turn
- [ ] Damage/effects displayed
- [ ] HP bars update
**HTMX Behavior:**
- [ ] Combat actions submit via HTMX
- [ ] Combat log updates dynamically
- [ ] Turn advances without reload
- [ ] Victory/defeat screen appears
### Session History
**Test Steps:**
1. Navigate to `/sessions/history`
2. View past sessions
**Expected Results:**
- [ ] All sessions listed (active and completed)
- [ ] Session summaries shown
- [ ] Click session → view details
- [ ] Filter/search works (if implemented)
---
## Multiplayer Testing
### Create Multiplayer Session
**Test Steps:**
1. Navigate to `/multiplayer/create`
2. Select party size (4 players)
3. Select difficulty (Medium)
4. Click "Create Session"
**Expected Results:**
- [ ] Session created via API
- [ ] Redirected to lobby
- [ ] Invite code displayed
- [ ] Invite link copyable
### Lobby (Host)
**Test Steps:**
1. View lobby as host
2. Wait for players to join
**Expected Results:**
- [ ] Host badge displayed
- [ ] Player list updates when players join (realtime)
- [ ] "Start Session" button disabled until all ready
- [ ] "Start Session" enabled when all ready
**Realtime Updates:**
- [ ] New player joins → list updates
- [ ] Player ready status changes → updates
- [ ] No page reload required
### Lobby (Player)
**Test Steps:**
1. Click invite link in different browser
2. Select character
3. Join session
**Expected Results:**
- [ ] Session info displayed
- [ ] Character selection shown
- [ ] Join button enabled
- [ ] Redirected to lobby after join
**Lobby Actions:**
- [ ] Toggle "Ready" status
- [ ] See other players' status update
- [ ] Host starts session → redirect to active session
### Active Multiplayer Session
**Test Steps:**
1. Play through multiplayer session
2. Take turns in combat
**Expected Results:**
- [ ] Campaign title displayed
- [ ] Timer counts down
- [ ] Party status shows all members
- [ ] Narrative updates
- [ ] Combat turn order shown
**Realtime Behavior:**
- [ ] Other players' actions appear immediately
- [ ] Turn advances when player acts
- [ ] HP updates across all clients
- [ ] Timer warnings appear
**Combat Turn:**
- [ ] Action buttons enabled only on your turn
- [ ] "Waiting for other players..." when not your turn
- [ ] Action submits via HTMX
- [ ] Combat log updates
### Session Complete
**Test Steps:**
1. Complete multiplayer session
2. View rewards screen
**Expected Results:**
- [ ] Victory message displayed
- [ ] Completion time shown
- [ ] Rewards listed (gold, XP, items)
- [ ] Level up notification (if applicable)
- [ ] Party stats shown (MVP, etc.)
---
## Responsive Design Testing
### Mobile (320px - 480px)
**Test on:**
- iPhone SE (375x667)
- iPhone 12 (390x844)
- Android (360x640)
**Check:**
- [ ] Navigation menu collapses to hamburger
- [ ] Forms are usable (inputs not cut off)
- [ ] Buttons are tap-friendly (min 44x44px)
- [ ] Character cards stack vertically
- [ ] Combat UI scales properly
- [ ] No horizontal scroll
### Tablet (481px - 768px)
**Test on:**
- iPad Mini (768x1024)
- iPad Air (820x1180)
**Check:**
- [ ] Layout uses medium breakpoint
- [ ] Character grid shows 2 columns
- [ ] Navigation partially expanded
- [ ] Touch targets adequate
### Desktop (769px+)
**Test on:**
- 1024x768 (small desktop)
- 1920x1080 (standard desktop)
- 2560x1440 (large desktop)
**Check:**
- [ ] Full navigation visible
- [ ] Character grid shows 3-4 columns
- [ ] Combat UI uses full width
- [ ] No excessive whitespace
---
## Accessibility Testing
### Keyboard Navigation
**Test Steps:**
1. Navigate site using only keyboard (Tab, Shift+Tab, Enter, Space)
2. Test all interactive elements
**Expected Results:**
- [ ] All links/buttons focusable
- [ ] Focus indicator visible
- [ ] Logical tab order
- [ ] Forms submittable via Enter
- [ ] Modals closable via Escape
### Screen Reader
**Test with:**
- NVDA (Windows)
- JAWS (Windows)
- VoiceOver (macOS/iOS)
**Check:**
- [ ] All images have alt text
- [ ] Form labels associated correctly
- [ ] ARIA labels on icon-only buttons
- [ ] Headings in logical hierarchy
- [ ] Focus announcements clear
### Color Contrast
**Tools:**
- WAVE (browser extension)
- axe DevTools (browser extension)
**Check:**
- [ ] Text contrast ≥ 4.5:1 (AA)
- [ ] Large text contrast ≥ 3:1 (AA)
- [ ] Interactive elements have visible focus
- [ ] Color not sole indicator (e.g., errors)
---
## HTMX Specific Testing
### Form Submission
**Test:**
- [ ] Form submits without page reload
- [ ] Success response replaces form
- [ ] Error response shows validation
- [ ] Loading indicator appears
- [ ] Double-submit prevented
### Delete Actions
**Test:**
- [ ] Confirmation dialog appears
- [ ] Element removed on success
- [ ] Error message on failure
- [ ] No page reload
### Live Search
**Test:**
- [ ] Search triggers after typing stops (debounce)
- [ ] Results update without reload
- [ ] Loading indicator shown
- [ ] Empty state displayed correctly
### Polling
**Test:**
- [ ] Content updates at interval
- [ ] Polling stops when condition met
- [ ] Network errors handled gracefully
### Realtime + HTMX Hybrid
**Test:**
- [ ] Appwrite Realtime triggers HTMX reload
- [ ] Multiple browser tabs sync
- [ ] No duplicate updates
- [ ] Disconnection handled
---
## Error Handling Testing
### API Errors
**Simulate:**
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 500 Internal Server Error
**Expected:**
- [ ] User-friendly error message displayed
- [ ] Error logged to console (dev only)
- [ ] No sensitive data exposed
- [ ] Retry option available (if applicable)
### Network Errors
**Simulate:**
- Disconnect network
- Slow 3G connection
- API backend down
**Expected:**
- [ ] Loading indicator appears
- [ ] Timeout after reasonable delay
- [ ] Error message displayed
- [ ] Fallback behavior (if applicable)
---
## Performance Testing
### Page Load Speed
**Test:**
- [ ] First contentful paint < 1.5s
- [ ] Time to interactive < 3s
- [ ] No layout shift (CLS < 0.1)
**Tools:**
- Chrome DevTools (Lighthouse)
- WebPageTest.org
### HTMX Requests
**Test:**
- [ ] Requests complete < 500ms (local)
- [ ] Debouncing prevents spam
- [ ] Caching used where appropriate
---
## Browser Compatibility
### Feature Detection
**Test:**
- [ ] Works without JavaScript (forms submit)
- [ ] WebSocket support detected
- [ ] Fallback to polling if no WebSocket
- [ ] HTMX gracefully degrades
### Browser-Specific Issues
**Chrome:**
- [ ] HTMX works correctly
- [ ] Realtime WebSocket stable
**Firefox:**
- [ ] All features functional
- [ ] No console errors
**Safari:**
- [ ] WebSocket support
- [ ] Form validation
- [ ] CSS grid/flexbox
**Edge:**
- [ ] All features functional
---
## Security Testing
### Authentication
**Test:**
- [ ] Protected routes redirect to login
- [ ] Session timeout works
- [ ] Cannot access other users' data
- [ ] CSRF protection enabled on forms
### Input Validation
**Test:**
- [ ] XSS prevention (input sanitized)
- [ ] SQL injection prevention (API handles)
- [ ] File upload validation (if applicable)
- [ ] Rate limiting enforced
---
## Testing Checklist Template
Copy this template for each release:
```markdown
## Release X.X.X Testing Checklist
**Tester:** [Your Name]
**Date:** [YYYY-MM-DD]
**Environment:** [Local / Staging / Production]
**Browser:** [Chrome / Firefox / Safari / Edge]
### Authentication
- [ ] Registration works
- [ ] Login works
- [ ] Logout works
### Characters
- [ ] Create character
- [ ] View character list
- [ ] Edit character
- [ ] Delete character
### Sessions
- [ ] Create solo session
- [ ] Active session UI
- [ ] Combat works
- [ ] Session history
### Multiplayer
- [ ] Create multiplayer session
- [ ] Lobby (host)
- [ ] Lobby (player)
- [ ] Active multiplayer session
- [ ] Session complete
### Responsive
- [ ] Mobile (375px)
- [ ] Tablet (768px)
- [ ] Desktop (1920px)
### Accessibility
- [ ] Keyboard navigation
- [ ] Screen reader compatible
- [ ] Color contrast
### Performance
- [ ] Page load < 3s
- [ ] No console errors
- [ ] HTMX requests fast
### Notes:
[Any issues found]
```
---
## Reporting Issues
### Issue Template
```markdown
**Title:** [Brief description]
**Environment:**
- Browser: [Chrome 120]
- OS: [Windows 11]
- Screen Size: [1920x1080]
**Steps to Reproduce:**
1. Navigate to `/characters`
2. Click "Delete" on character card
3. Confirm deletion
**Expected Result:**
Character should be deleted and removed from list.
**Actual Result:**
Character deleted but card still visible until page refresh.
**Console Errors:**
[Paste any console errors]
**Screenshots:**
[Attach screenshots if applicable]
**Severity:**
- [ ] Critical (blocking)
- [ ] High (major feature broken)
- [x] Medium (feature degraded)
- [ ] Low (cosmetic)
```
---
## Related Documentation
- **[TEMPLATES.md](TEMPLATES.md)** - Template structure
- **[HTMX_PATTERNS.md](HTMX_PATTERNS.md)** - HTMX integration patterns
- **[/api/docs/API_TESTING.md](../../api/docs/API_TESTING.md)** - API backend testing
---
**Document Version:** 1.0
**Created:** November 18, 2025
**Last Updated:** November 18, 2025