xziino d792d5b718 Initial commit: FFLogs mitigation analyzer
Two-tab app: report viewer + analysis tab with AoE timeline,
per-player mitigation icons (local XIVAPI PNGs), and fight-wide
buff/debuff window tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:42:38 +02:00

113 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

document.addEventListener('DOMContentLoaded', () => {
window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0 };
const form = document.getElementById('report-form');
const output = document.getElementById('output');
const outputCard = document.getElementById('output-card');
const initialHint = document.getElementById('initial-hint');
const fightSelectCard = document.getElementById('fight-select-card');
const fightSelect = document.getElementById('fight-select');
let allFights = [];
function formatDuration(ms) {
const min = Math.floor(ms / 60000);
const sec = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');
return `${min}:${sec}`;
}
function formatBossHp(fight) {
if (fight.kill) return 'Kill';
const pct = fight.fightPercentage;
if (pct == null) return '?';
// fightPercentage is 010000 (e.g. 5000 = 50.00%)
return (pct / 100).toFixed(2) + '%';
}
function displayFight(fight) {
output.textContent = JSON.stringify(fight, null, 2);
outputCard.style.display = 'block';
}
fightSelect.addEventListener('change', () => {
if (!fightSelect.value) return;
const id = parseInt(fightSelect.value, 10);
const fight = allFights.find(f => f.id === id);
if (!fight) return;
window.App.fightId = id;
window.App.fightStart = fight.startTime;
window.App.fightEnd = fight.endTime;
displayFight(fight);
window.analysisTab?.onFightSelected?.();
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
initialHint.style.display = 'none';
outputCard.style.display = 'block';
output.textContent = '// fetching...';
fightSelectCard.style.display = 'none';
fightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
allFights = [];
const reportCode = form.elements['report_code'].value.trim();
window.App.reportCode = reportCode;
window.App.fightId = null;
window.App.fightStart = 0;
window.App.fightEnd = 0;
window.analysisTab?.reset?.();
let response, json;
try {
response = await fetch('api/fight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ report_code: reportCode }),
});
json = await response.json();
} catch (err) {
output.textContent = '// network error: ' + err.message;
return;
}
if (json.reauth) {
output.textContent = '// session expired — redirecting...';
setTimeout(() => { window.location.href = 'auth/start.php'; }, 1500);
return;
}
if (json.errors) {
output.textContent = '// GraphQL error:\n' + JSON.stringify(json.errors, null, 2);
return;
}
const report = json?.data?.reportData?.report;
if (!report) {
output.textContent = JSON.stringify(json, null, 2);
return;
}
allFights = report.fights ?? [];
if (allFights.length === 0) {
output.textContent = '// Keine Fights in diesem Report gefunden.';
return;
}
allFights.forEach(fight => {
const duration = formatDuration(fight.endTime - fight.startTime);
const hp = formatBossHp(fight);
const opt = document.createElement('option');
opt.value = fight.id;
opt.textContent = `${fight.name}${duration}${hp}`;
fightSelect.appendChild(opt);
});
fightSelectCard.style.display = 'block';
output.textContent = '// Fight auswählen ↑';
});
});