Add cross-report comparison and fix delta/filter bugs

- Cross-report comparison: toggle panel to load fights from a second
  report code, select a fight to use as reference (mutually exclusive
  with same-report ref dropdown)
- Fix ref-row player filter: use hiddenPlayerNames (name-based) so
  ext-report targets with different actor IDs are correctly hidden
- Fix negative delta formatting: pass Math.abs(diff) to fmtDmg so
  negative values are abbreviated the same way as positive ones

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xziino 2026-05-20 16:18:43 +02:00
parent 8fe057e15b
commit 76c5d80cc2
3 changed files with 138 additions and 7 deletions

View File

@ -65,6 +65,27 @@
margin-left: auto; margin-left: auto;
} }
/* ── External report panel ───────────────────────────────────────────────── */
.ref-ext-row {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
#ref-ext-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.ref-report-input {
width: 200px;
padding: 4px 9px;
font-size: 12px;
}
/* ── AoE Timeline ────────────────────────────────────────────────────────── */ /* ── AoE Timeline ────────────────────────────────────────────────────────── */
.aoe-event { .aoe-event {
display: grid; display: grid;

View File

@ -56,12 +56,15 @@
} }
let hiddenPlayers = new Set(); let hiddenPlayers = new Set();
let hiddenPlayerNames = new Set();
let lastEvents = []; let lastEvents = [];
let lastFightStart = 0; let lastFightStart = 0;
let playerFilter = ''; let playerFilter = '';
let phaseFilter = { startTime: 0, endTime: Infinity }; let phaseFilter = { startTime: 0, endTime: Infinity };
let refEvents = []; let refEvents = [];
let refFightStart = 0; let refFightStart = 0;
let extFights = [];
let extReportCode = '';
// ── Player grid ────────────────────────────────────────────────────────── // ── Player grid ──────────────────────────────────────────────────────────
@ -74,9 +77,10 @@
}); });
hiddenPlayers = new Set(players.filter(p => p.role === 'tank').map(p => p.id)); hiddenPlayers = new Set(players.filter(p => p.role === 'tank').map(p => p.id));
hiddenPlayerNames = new Set(players.filter(p => p.role === 'tank').map(p => p.name));
grid.innerHTML = players.map(p => ` grid.innerHTML = players.map(p => `
<div class="player-card ${hiddenPlayers.has(p.id) ? 'player-hidden' : ''}" data-player-id="${p.id}"> <div class="player-card ${hiddenPlayers.has(p.id) ? 'player-hidden' : ''}" data-player-id="${p.id}" data-player-name="${p.name}">
<div class="player-job-icon role-${p.role}">${abbr(p.type)}</div> <div class="player-job-icon role-${p.role}">${abbr(p.type)}</div>
<div> <div>
<div class="player-name">${p.name}</div> <div class="player-name">${p.name}</div>
@ -90,11 +94,14 @@
const card = e.target.closest('.player-card'); const card = e.target.closest('.player-card');
if (!card) return; if (!card) return;
const id = parseInt(card.dataset.playerId, 10); const id = parseInt(card.dataset.playerId, 10);
const name = card.dataset.playerName;
if (hiddenPlayers.has(id)) { if (hiddenPlayers.has(id)) {
hiddenPlayers.delete(id); hiddenPlayers.delete(id);
hiddenPlayerNames.delete(name);
card.classList.remove('player-hidden'); card.classList.remove('player-hidden');
} else { } else {
hiddenPlayers.add(id); hiddenPlayers.add(id);
hiddenPlayerNames.add(name);
card.classList.add('player-hidden'); card.classList.add('player-hidden');
} }
renderTimeline(lastEvents, lastFightStart); renderTimeline(lastEvents, lastFightStart);
@ -147,6 +154,9 @@
const fight = (window.App?.fights ?? []).find(f => f.id === refId); const fight = (window.App?.fights ?? []).find(f => f.id === refId);
if (!fight) return; if (!fight) return;
// Clear ext-report selection
refExtFightSelect.value = '';
refFightSelect.disabled = true; refFightSelect.disabled = true;
try { try {
const res = await fetch('api/analysis.php', { const res = await fetch('api/analysis.php', {
@ -181,6 +191,91 @@
refFightSelect.style.display = ''; refFightSelect.style.display = '';
} }
// ── External report comparison ────────────────────────────────────────────
const refExtToggle = document.getElementById('ref-ext-toggle');
const refExtPanel = document.getElementById('ref-ext-panel');
const refReportInput = document.getElementById('ref-report-input');
const refReportLoad = document.getElementById('ref-report-load');
const refExtFightSelect = document.getElementById('ref-ext-fight-select');
refExtToggle.addEventListener('click', () => {
const hidden = refExtPanel.style.display === 'none';
refExtPanel.style.display = hidden ? '' : 'none';
});
refReportLoad.addEventListener('click', async () => {
const code = refReportInput.value.trim();
if (!code) return;
refReportLoad.disabled = true;
refReportLoad.textContent = 'Lädt…';
try {
const res = await fetch('api/fight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ report_code: code }),
});
const json = await res.json();
if (json.reauth) { window.location.href = 'auth/start.php'; return; }
const fights = json?.data?.reportData?.report?.fights ?? [];
extFights = fights;
extReportCode = code;
refExtFightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
fights.forEach(f => {
const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?');
const opt = document.createElement('option');
opt.value = f.id;
opt.textContent = `${f.name}${fmtDur(f.endTime - f.startTime)}${hp}`;
refExtFightSelect.appendChild(opt);
});
refExtFightSelect.style.display = fights.length ? '' : 'none';
} catch { }
refReportLoad.disabled = false;
refReportLoad.textContent = 'Laden';
});
refExtFightSelect.addEventListener('change', async () => {
const refId = parseInt(refExtFightSelect.value, 10);
if (!refId) {
refEvents = [];
refFightStart = 0;
renderTimeline(lastEvents, lastFightStart);
return;
}
const fight = extFights.find(f => f.id === refId);
if (!fight) return;
// Clear same-report selection
refFightSelect.value = '';
refExtFightSelect.disabled = true;
try {
const res = await fetch('api/analysis.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
report_code: extReportCode,
fight_id: refId,
start_time: fight.startTime,
end_time: fight.endTime,
}),
});
const json = await res.json();
if (!json.error && !json.reauth) {
refEvents = json.aoe_events ?? [];
refFightStart = json.fight_start ?? fight.startTime;
}
} catch { }
refExtFightSelect.disabled = false;
renderTimeline(lastEvents, lastFightStart);
});
// ── Timeline rendering ──────────────────────────────────────────────────── // ── Timeline rendering ────────────────────────────────────────────────────
function renderTimeline(events, fightStart) { function renderTimeline(events, fightStart) {
@ -271,7 +366,7 @@
let refHtml = ''; let refHtml = '';
if (refEv) { if (refEv) {
const refVisible = refEv.targets.filter(t => const refVisible = refEv.targets.filter(t =>
!hiddenPlayers.has(t.id) && !hiddenPlayerNames.has(t.name) &&
(!playerFilter || t.name.toLowerCase().includes(playerFilter)) (!playerFilter || t.name.toLowerCase().includes(playerFilter))
); );
if (refVisible.length) { if (refVisible.length) {
@ -284,7 +379,7 @@
const dead = t.hp === 0 && t.maxHp > 0; const dead = t.hp === 0 && t.maxHp > 0;
const deltaHtml = diff !== 0 const deltaHtml = diff !== 0
? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : ''}${fmtDmg(diff)}</span>` ? `<span class="${diff > 0 ? 'aoe-delta-worse' : 'aoe-delta-better'}">${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}</span>`
: ''; : '';
const refMitigIcons = (t.mitigations ?? []).map(m => { const refMitigIcons = (t.mitigations ?? []).map(m => {
@ -390,8 +485,13 @@
lastFightId = null; lastFightId = null;
refEvents = []; refEvents = [];
refFightStart = 0; refFightStart = 0;
extFights = [];
extReportCode = '';
refFightSelect.value = ''; refFightSelect.value = '';
refFightSelect.style.display = 'none'; refFightSelect.style.display = 'none';
refExtFightSelect.value = '';
refExtFightSelect.style.display = 'none';
refExtPanel.style.display = 'none';
}, },
}; };
})(); })();

View File

@ -18,6 +18,16 @@
</select> </select>
</div> </div>
<div id="player-grid" class="player-grid"></div> <div id="player-grid" class="player-grid"></div>
<div class="ref-ext-row">
<button id="ref-ext-toggle" class="btn btn-sm">+ Anderer Report</button>
<div id="ref-ext-panel" style="display:none">
<input type="text" id="ref-report-input" class="ref-report-input" placeholder="Report-Code">
<button id="ref-report-load" class="btn btn-sm">Laden</button>
<select id="ref-ext-fight-select" class="filter-input" style="display:none">
<option value=""> Fight auswählen </option>
</select>
</div>
</div>
</div> </div>
<div class="card"> <div class="card">