654 lines
No EOL
22 KiB
HTML
654 lines
No EOL
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Player Debug - Dereth Tracker</title>
|
|
<style>
|
|
body {
|
|
font-family: "Segoe UI", sans-serif;
|
|
background: #111;
|
|
color: #eee;
|
|
margin: 20px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
border-bottom: 2px solid #333;
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: #222;
|
|
border: 1px solid #444;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
|
|
.stat-card h3 {
|
|
margin: 0 0 10px 0;
|
|
color: #88f;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.8rem;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
}
|
|
|
|
.sections {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 30px;
|
|
}
|
|
|
|
.section {
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.section h2 {
|
|
margin: 0 0 15px 0;
|
|
color: #88f;
|
|
border-bottom: 1px solid #333;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.event-log {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
background: #000;
|
|
border: 1px solid #333;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
font-family: monospace;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.event-enter {
|
|
color: #4f4;
|
|
}
|
|
|
|
.event-exit {
|
|
color: #f44;
|
|
}
|
|
|
|
.flapping-player {
|
|
background: #332;
|
|
border: 1px solid #664;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.flapping-player .name {
|
|
font-weight: bold;
|
|
color: #ff6;
|
|
}
|
|
|
|
.flapping-player .score {
|
|
color: #f88;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.history-entry {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px;
|
|
border-bottom: 1px solid #333;
|
|
font-family: monospace;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.history-entry:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.timestamp {
|
|
color: #888;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.player-count {
|
|
font-weight: bold;
|
|
color: #4f4;
|
|
}
|
|
|
|
.status {
|
|
text-align: center;
|
|
padding: 20px;
|
|
background: #222;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.loading {
|
|
color: #88f;
|
|
}
|
|
|
|
.error {
|
|
color: #f44;
|
|
}
|
|
|
|
.last-updated {
|
|
text-align: center;
|
|
color: #888;
|
|
font-size: 0.9rem;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.timing-player {
|
|
background: #223;
|
|
border: 1px solid #446;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.timing-player .name {
|
|
font-weight: bold;
|
|
color: #ff8;
|
|
}
|
|
|
|
.timing-player .issue {
|
|
color: #f88;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.timing-player .stats {
|
|
color: #aaa;
|
|
font-size: 0.8rem;
|
|
margin-top: 5px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.timing-good {
|
|
border-color: #464 !important;
|
|
background: #232 !important;
|
|
}
|
|
|
|
.timing-warning {
|
|
border-color: #664 !important;
|
|
background: #332 !important;
|
|
}
|
|
|
|
.timing-critical {
|
|
border-color: #644 !important;
|
|
background: #322 !important;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sections {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🔍 Player Debug Dashboard</h1>
|
|
<p>Real-time tracking of player list changes to debug flapping issues</p>
|
|
</div>
|
|
|
|
<div class="status" id="status">
|
|
<div class="loading">Loading player debug data...</div>
|
|
</div>
|
|
|
|
<div class="stats-grid" id="statsGrid" style="display: none;">
|
|
<div class="stat-card">
|
|
<h3>Current Players</h3>
|
|
<div class="stat-value" id="currentPlayers">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Total Events</h3>
|
|
<div class="stat-value" id="totalEvents">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Flapping Players</h3>
|
|
<div class="stat-value" id="flappingCount">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Recent Activity</h3>
|
|
<div class="stat-value" id="recentActivity">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Timing Issues</h3>
|
|
<div class="stat-value" id="timingIssues">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Tracked Players</h3>
|
|
<div class="stat-value" id="trackedPlayers">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>WebSocket Connections</h3>
|
|
<div class="stat-value" id="websocketConnections">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Database Queries</h3>
|
|
<div class="stat-value" id="databaseQueries">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Recent Activity</h3>
|
|
<div class="stat-value" id="recentActivityCount">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sections" id="sections" style="display: none;">
|
|
<div class="section">
|
|
<h2>🔄 Flapping Players</h2>
|
|
<div id="flappingPlayers">
|
|
<p style="color: #888; font-style: italic;">No flapping detected</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>⏰ Timing Issues</h2>
|
|
<div id="timingProblems">
|
|
<p style="color: #888; font-style: italic;">Loading timing analysis...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📊 Player History</h2>
|
|
<div id="playerHistory" class="event-log">
|
|
<p style="color: #888; font-style: italic;">Loading history...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📝 Recent Events</h2>
|
|
<div id="recentEvents" class="event-log">
|
|
<p style="color: #888; font-style: italic;">Loading events...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🎯 Most Active Players</h2>
|
|
<div id="frequentEvents">
|
|
<p style="color: #888; font-style: italic;">Loading activity...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📈 Telemetry Timing</h2>
|
|
<div id="telemetryTiming">
|
|
<p style="color: #888; font-style: italic;">Loading telemetry data...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🔌 WebSocket Health</h2>
|
|
<div id="websocketHealth">
|
|
<p style="color: #888; font-style: italic;">Loading WebSocket data...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🗄️ Database Performance</h2>
|
|
<div id="databasePerformance">
|
|
<p style="color: #888; font-style: italic;">Loading database data...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>⚡ Recent Activity</h2>
|
|
<div id="recentActivityFeed" class="event-log">
|
|
<p style="color: #888; font-style: italic;">Loading recent activity...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="last-updated" id="lastUpdated"></div>
|
|
</div>
|
|
|
|
<script>
|
|
let pollInterval;
|
|
let isPolling = false;
|
|
|
|
function formatTimestamp(isoString) {
|
|
const date = new Date(isoString);
|
|
return date.toLocaleTimeString();
|
|
}
|
|
|
|
function formatDate(isoString) {
|
|
const date = new Date(isoString);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
async function fetchDebugData() {
|
|
try {
|
|
// Fetch all four endpoints
|
|
const [flappingResponse, websocketResponse, databaseResponse, activityResponse] = await Promise.all([
|
|
fetch('/debug/player-flapping'),
|
|
fetch('/debug/websocket-health'),
|
|
fetch('/debug/database-performance'),
|
|
fetch('/debug/recent-activity')
|
|
]);
|
|
|
|
if (!flappingResponse.ok) {
|
|
throw new Error(`HTTP ${flappingResponse.status}: ${flappingResponse.statusText}`);
|
|
}
|
|
|
|
const data = await flappingResponse.json();
|
|
const websocketData = websocketResponse.ok ? await websocketResponse.json() : null;
|
|
const databaseData = databaseResponse.ok ? await databaseResponse.json() : null;
|
|
const activityData = activityResponse.ok ? await activityResponse.json() : null;
|
|
|
|
// Combine the data
|
|
const enhancedData = {
|
|
...data,
|
|
websocket_health: websocketData,
|
|
database_performance: databaseData,
|
|
recent_activity: activityData
|
|
};
|
|
|
|
updateUI(enhancedData);
|
|
showSuccess();
|
|
} catch (error) {
|
|
console.error('Failed to fetch debug data:', error);
|
|
showError(`Failed to fetch data: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function updateUI(data) {
|
|
// Update stats
|
|
document.getElementById('currentPlayers').textContent = data.current_players;
|
|
document.getElementById('totalEvents').textContent = data.tracking_stats.total_events;
|
|
document.getElementById('flappingCount').textContent = data.flapping_analysis.flapping_players.length;
|
|
|
|
const recentActivity = data.flapping_analysis.recent_activity;
|
|
document.getElementById('recentActivity').textContent =
|
|
`+${recentActivity.enters} -${recentActivity.exits} (${recentActivity.net_change > 0 ? '+' : ''}${recentActivity.net_change})`;
|
|
|
|
// Update flapping players
|
|
const flappingDiv = document.getElementById('flappingPlayers');
|
|
if (data.flapping_analysis.flapping_players.length === 0) {
|
|
flappingDiv.innerHTML = '<p style="color: #4f4; font-style: italic;">✅ No flapping detected</p>';
|
|
} else {
|
|
flappingDiv.innerHTML = data.flapping_analysis.flapping_players.map(player => `
|
|
<div class="flapping-player">
|
|
<div class="name">${player.character_name}</div>
|
|
<div class="score">Flap Score: ${player.flap_score} | Events: ${player.events}</div>
|
|
<div style="font-size: 0.8rem; color: #aaa; margin-top: 5px;">
|
|
Recent: ${player.recent_activity.join(' → ')}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Update history
|
|
const historyDiv = document.getElementById('playerHistory');
|
|
if (data.history.length === 0) {
|
|
historyDiv.innerHTML = '<p style="color: #888; font-style: italic;">No history available</p>';
|
|
} else {
|
|
historyDiv.innerHTML = data.history.slice().reverse().map(entry => `
|
|
<div class="history-entry">
|
|
<span class="timestamp">${formatTimestamp(entry.timestamp)}</span>
|
|
<span class="player-count">${entry.player_count} players</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Update recent events
|
|
const eventsDiv = document.getElementById('recentEvents');
|
|
if (data.recent_events.length === 0) {
|
|
eventsDiv.innerHTML = '<p style="color: #888; font-style: italic;">No recent events</p>';
|
|
} else {
|
|
eventsDiv.innerHTML = data.recent_events.slice().reverse().map(event => `
|
|
<div class="event-${event.type}">
|
|
${formatTimestamp(event.timestamp)} - ${event.character_name} ${event.type === 'enter' ? 'joined' : 'left'} (${event.total_players} total)
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Update frequent events
|
|
const frequentDiv = document.getElementById('frequentEvents');
|
|
if (data.flapping_analysis.frequent_events.length === 0) {
|
|
frequentDiv.innerHTML = '<p style="color: #888; font-style: italic;">No activity data</p>';
|
|
} else {
|
|
frequentDiv.innerHTML = data.flapping_analysis.frequent_events.map(player => `
|
|
<div style="display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid #333;">
|
|
<span>${player.character_name}</span>
|
|
<span style="color: #88f;">${player.event_count} events</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Update timing issues
|
|
const timingDiv = document.getElementById('timingProblems');
|
|
const timingData = data.timing_analysis || { problem_players: [], summary: { total_tracked_players: 0 } };
|
|
document.getElementById('timingIssues').textContent = timingData.problem_players.length;
|
|
document.getElementById('trackedPlayers').textContent = timingData.summary.total_tracked_players;
|
|
|
|
if (timingData.problem_players.length === 0) {
|
|
timingDiv.innerHTML = '<p style="color: #4f4; font-style: italic;">✅ No timing issues detected</p>';
|
|
} else {
|
|
timingDiv.innerHTML = timingData.problem_players.map(player => {
|
|
let severityClass = 'timing-good';
|
|
const maxInterval = player.max_interval || 0;
|
|
const avgInterval = player.avg_interval || 0;
|
|
const totalMessages = player.total_messages || 0;
|
|
|
|
if (maxInterval > 60) severityClass = 'timing-critical';
|
|
else if (maxInterval > 45) severityClass = 'timing-warning';
|
|
|
|
const issue = maxInterval > 30 ?
|
|
`Max gap: ${maxInterval.toFixed(1)}s (>${30}s threshold)` :
|
|
`Irregular timing patterns detected`;
|
|
|
|
return `
|
|
<div class="timing-player ${severityClass}">
|
|
<div class="name">${player.character_name || 'Unknown'}</div>
|
|
<div class="issue">${issue}</div>
|
|
<div class="stats">
|
|
Messages: ${totalMessages} |
|
|
Avg gap: ${avgInterval.toFixed(1)}s |
|
|
Max gap: ${maxInterval.toFixed(1)}s
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Update telemetry timing section
|
|
const telemetryDiv = document.getElementById('telemetryTiming');
|
|
if (timingData.summary.total_tracked_players === 0) {
|
|
telemetryDiv.innerHTML = '<p style="color: #888; font-style: italic;">No telemetry data available</p>';
|
|
} else {
|
|
const healthyCount = timingData.summary.total_tracked_players - timingData.problem_players.length;
|
|
telemetryDiv.innerHTML = `
|
|
<div style="margin-bottom: 15px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>Total Players Tracked:</span>
|
|
<span style="color: #88f; font-weight: bold;">${timingData.summary.total_tracked_players}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>Healthy Timing:</span>
|
|
<span style="color: #4f4; font-weight: bold;">${healthyCount}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span>Timing Issues:</span>
|
|
<span style="color: #f44; font-weight: bold;">${timingData.problem_players.length}</span>
|
|
</div>
|
|
</div>
|
|
<div style="font-size: 0.9rem; color: #aaa; line-height: 1.4;">
|
|
Players with telemetry gaps >30s are flagged as timing issues.
|
|
This can cause players to temporarily disappear from the active list.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Update WebSocket health data
|
|
const websocketData = data.websocket_health;
|
|
if (websocketData) {
|
|
// Update stat card
|
|
document.getElementById('websocketConnections').textContent =
|
|
`${websocketData.total_connections} (P:${websocketData.plugin_connections}/B:${websocketData.browser_connections})`;
|
|
|
|
// Update detailed section
|
|
const websocketDiv = document.getElementById('websocketHealth');
|
|
websocketDiv.innerHTML = `
|
|
<div style="margin-bottom: 15px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>Plugin Connections:</span>
|
|
<span style="color: #88f; font-weight: bold;">${websocketData.plugin_connections}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>Browser Connections:</span>
|
|
<span style="color: #4f4; font-weight: bold;">${websocketData.browser_connections}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span>Total Connections:</span>
|
|
<span style="color: #ff8; font-weight: bold;">${websocketData.total_connections}</span>
|
|
</div>
|
|
</div>
|
|
<div style="font-size: 0.9rem; color: #aaa;">
|
|
Plugin connections receive telemetry from game clients.<br>
|
|
Browser connections show live updates to web users.
|
|
</div>
|
|
`;
|
|
} else {
|
|
document.getElementById('websocketConnections').textContent = 'Error';
|
|
document.getElementById('websocketHealth').innerHTML = '<p style="color: #f44;">Failed to load WebSocket data</p>';
|
|
}
|
|
|
|
// Update database performance data
|
|
const databaseData = data.database_performance;
|
|
if (databaseData) {
|
|
// Update stat card
|
|
document.getElementById('databaseQueries').textContent =
|
|
`${databaseData.total_queries} (${databaseData.average_query_time}s avg)`;
|
|
|
|
// Update detailed section
|
|
const databaseDiv = document.getElementById('databasePerformance');
|
|
databaseDiv.innerHTML = `
|
|
<div style="margin-bottom: 15px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>Total Queries:</span>
|
|
<span style="color: #88f; font-weight: bold;">${databaseData.total_queries}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
|
<span>Total Query Time:</span>
|
|
<span style="color: #4f4; font-weight: bold;">${databaseData.total_query_time}s</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span>Average Query Time:</span>
|
|
<span style="color: #ff8; font-weight: bold;">${databaseData.average_query_time}s</span>
|
|
</div>
|
|
</div>
|
|
<div style="font-size: 0.9rem; color: #aaa;">
|
|
Tracks telemetry database write operations performance.<br>
|
|
Each telemetry message triggers 1-2 database queries.
|
|
</div>
|
|
`;
|
|
} else {
|
|
document.getElementById('databaseQueries').textContent = 'Error';
|
|
document.getElementById('databasePerformance').innerHTML = '<p style="color: #f44;">Failed to load database performance data</p>';
|
|
}
|
|
|
|
// Update recent activity data
|
|
const activityData = data.recent_activity;
|
|
if (activityData) {
|
|
// Update stat card
|
|
document.getElementById('recentActivityCount').textContent =
|
|
`${activityData.total_messages}/${activityData.max_messages}`;
|
|
|
|
// Update detailed section
|
|
const activityDiv = document.getElementById('recentActivityFeed');
|
|
if (activityData.recent_messages.length === 0) {
|
|
activityDiv.innerHTML = '<p style="color: #888; font-style: italic;">No recent activity</p>';
|
|
} else {
|
|
activityDiv.innerHTML = activityData.recent_messages.slice().reverse().map(msg => {
|
|
const timeStr = formatTimestamp(msg.timestamp);
|
|
const killInfo = msg.kill_delta > 0 ? ` (+${msg.kill_delta} kills)` : '';
|
|
const queryTime = msg.query_time ? ` ${msg.query_time}ms` : '';
|
|
return `
|
|
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0; border-bottom: 1px solid #333; font-family: monospace; font-size: 0.85rem;">
|
|
<span>
|
|
<span style="color: #888;">${timeStr}</span>
|
|
<span style="color: #88f; margin-left: 8px;">${msg.character_name}</span>
|
|
<span style="color: #aaa; margin-left: 8px;">kills:${msg.kills}${killInfo}</span>
|
|
</span>
|
|
<span style="color: #666;">${queryTime}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
} else {
|
|
document.getElementById('recentActivityCount').textContent = 'Error';
|
|
document.getElementById('recentActivityFeed').innerHTML = '<p style="color: #f44;">Failed to load recent activity data</p>';
|
|
}
|
|
|
|
// Update last updated
|
|
document.getElementById('lastUpdated').textContent = `Last updated: ${formatDate(new Date().toISOString())}`;
|
|
}
|
|
|
|
function showSuccess() {
|
|
document.getElementById('status').style.display = 'none';
|
|
document.getElementById('statsGrid').style.display = 'grid';
|
|
document.getElementById('sections').style.display = 'grid';
|
|
}
|
|
|
|
function showError(message) {
|
|
const statusDiv = document.getElementById('status');
|
|
statusDiv.innerHTML = `<div class="error">❌ ${message}</div>`;
|
|
statusDiv.style.display = 'block';
|
|
document.getElementById('statsGrid').style.display = 'none';
|
|
document.getElementById('sections').style.display = 'none';
|
|
}
|
|
|
|
function startPolling() {
|
|
if (isPolling) return;
|
|
|
|
isPolling = true;
|
|
fetchDebugData(); // Initial fetch
|
|
pollInterval = setInterval(fetchDebugData, 5000); // Poll every 5 seconds
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (pollInterval) {
|
|
clearInterval(pollInterval);
|
|
pollInterval = null;
|
|
}
|
|
isPolling = false;
|
|
}
|
|
|
|
// Handle page visibility changes
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
stopPolling();
|
|
} else {
|
|
startPolling();
|
|
}
|
|
});
|
|
|
|
// Start polling when page loads
|
|
window.addEventListener('load', startPolling);
|
|
|
|
// Stop polling when page unloads
|
|
window.addEventListener('beforeunload', stopPolling);
|
|
</script>
|
|
</body>
|
|
</html> |