Compare commits

..

8 Commits

14 changed files with 2190 additions and 211 deletions

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"python-envs.pythonProjects": [
{
"path": ".",
"envManager": "ms-python.python:system",
"packageManager": "ms-python.python:pip"
}
]
}

View File

@ -21,6 +21,8 @@ if (empty($_SESSION['admin_csrf'])) {
$_SESSION['admin_csrf'] = bin2hex(random_bytes(16));
}
$csrf = $_SESSION['admin_csrf'];
$returnPath = safe_return_path($_GET['return'] ?? null);
$adminAction = 'admin.php?return=' . rawurlencode($returnPath);
function admin_h(?string $value): string {
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
@ -68,6 +70,63 @@ function git_last_commit(): string {
return $out !== '' ? $out : '(unknown)';
}
function git_current_branch(): string {
return git_output(['rev-parse', '--abbrev-ref', 'HEAD']);
}
function git_upstream_for_branch(string $branch): ?string {
$result = git_run(['rev-parse', '--abbrev-ref', $branch . '@{upstream}']);
return $result['code'] === 0 && $result['output'] !== '' ? $result['output'] : null;
}
function git_is_ancestor(string $ancestor, string $descendant): bool {
return git_run(['merge-base', '--is-ancestor', $ancestor, $descendant])['code'] === 0;
}
function git_pull_all_tracked_branches(): array {
$lines = [];
$exitCode = 0;
$fetch = git_run(['fetch', '--all', '--prune']);
$lines[] = '$ git fetch --all --prune';
$lines[] = $fetch['output'] ?: '(no output)';
if ($fetch['code'] !== 0) return ['code' => $fetch['code'], 'output' => implode("\n", $lines)];
$current = git_current_branch();
foreach (git_local_branches() as $branch) {
$upstream = git_upstream_for_branch($branch);
if ($upstream === null) {
$lines[] = '';
$lines[] = $branch . ': no upstream configured, skipped';
continue;
}
$lines[] = '';
$lines[] = $branch . ' <- ' . $upstream;
if ($branch === $current) {
$pull = git_run(['merge', '--ff-only', $upstream]);
$lines[] = '$ git merge --ff-only ' . $upstream;
$lines[] = $pull['output'] ?: '(no output)';
if ($pull['code'] !== 0) $exitCode = $pull['code'];
continue;
}
if (git_is_ancestor($branch, $upstream)) {
$update = git_run(['branch', '-f', $branch, $upstream]);
$lines[] = '$ git branch -f ' . $branch . ' ' . $upstream;
$lines[] = $update['output'] ?: '(fast-forwarded)';
if ($update['code'] !== 0) $exitCode = $update['code'];
} elseif (git_is_ancestor($upstream, $branch)) {
$lines[] = 'already up to date or ahead';
} else {
$lines[] = 'diverged, skipped';
if ($exitCode === 0) $exitCode = 1;
}
}
return ['code' => $exitCode, 'output' => implode("\n", $lines)];
}
function resolve_selected_branch_ref(string $ref, array $localBranches, array $remoteBranches, bool $preferLocalForRemote): ?string {
if (str_starts_with($ref, 'local:')) {
$branch = substr($ref, 6);
@ -94,11 +153,11 @@ if ($allowed && $_SERVER['REQUEST_METHOD'] === 'POST') {
} else {
$action = $_POST['action'] ?? '';
if ($action === 'fetch') {
$result = git_run(['fetch', '--prune']);
$message = 'git fetch --prune';
$result = git_run(['fetch', '--all', '--prune']);
$message = 'git fetch --all --prune';
} elseif ($action === 'pull') {
$result = git_run(['pull', '--ff-only']);
$message = 'git pull --ff-only';
$result = git_pull_all_tracked_branches();
$message = 'pull all tracked branches';
} elseif ($action === 'push') {
$result = git_run(['push']);
$message = 'git push';
@ -118,6 +177,7 @@ if ($allowed && $_SERVER['REQUEST_METHOD'] === 'POST') {
}
} elseif ($action === 'merge') {
$ref = trim((string)($_POST['merge_branch'] ?? ''));
git_run(['fetch', '--all', '--prune']);
$localBranches = git_local_branches();
$remoteBranches = git_remote_branches();
$branch = resolve_selected_branch_ref($ref, $localBranches, $remoteBranches, false);
@ -164,7 +224,7 @@ $branches = [];
$remoteBranches = [];
$lastCommit = '';
if ($allowed) {
$currentBranch = git_output(['rev-parse', '--abbrev-ref', 'HEAD']);
$currentBranch = git_current_branch();
$status = git_output(['status', '-sb']);
$branches = git_local_branches();
$remoteBranches = git_remote_branches();
@ -203,9 +263,9 @@ if ($allowed) {
<body>
<div id="app">
<header id="topbar">
<a class="logo" href="index.php" style="text-decoration:none">REPORT VIEWER <span>Admin</span></a>
<a class="logo" href="<?= admin_h($returnPath) ?>" style="text-decoration:none">REPORT VIEWER <span>Admin</span></a>
<div class="topbar-actions">
<a class="topbar-link" href="index.php">Back</a>
<a class="topbar-link" href="<?= admin_h($returnPath) ?>">Back</a>
<div class="topbar-user">
<?= $user ? admin_h($user['name'] ?: 'FFLogs User') . ' #' . admin_h($user['id']) : 'FFLogs user unavailable' ?>
</div>
@ -242,14 +302,14 @@ if ($allowed) {
<div class="admin-grid section-gap">
<div class="card">
<div class="card-title">Aktionen</div>
<form method="post" class="admin-actions">
<form method="post" action="<?= admin_h($adminAction) ?>" class="admin-actions">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<button class="btn" name="action" value="fetch" type="submit">Fetch</button>
<button class="btn btn-gold" name="action" value="pull" type="submit">Pull</button>
<button class="btn btn-gold" name="action" value="pull" type="submit">Pull All</button>
<button class="btn" name="action" value="push" type="submit">Push</button>
</form>
<form method="post" class="admin-actions section-gap">
<form method="post" action="<?= admin_h($adminAction) ?>" class="admin-actions section-gap">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<input type="hidden" name="action" value="switch">
<div class="fg">
@ -274,7 +334,7 @@ if ($allowed) {
<button class="btn" type="submit">Switch</button>
</form>
<form method="post" class="section-gap">
<form method="post" action="<?= admin_h($adminAction) ?>" class="section-gap">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<input type="hidden" name="action" value="commit">
<div class="fg">
@ -284,7 +344,7 @@ if ($allowed) {
<button class="btn btn-gold section-gap" type="submit">Commit all changes</button>
</form>
<form method="post" class="admin-actions section-gap">
<form method="post" action="<?= admin_h($adminAction) ?>" class="admin-actions section-gap">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<input type="hidden" name="action" value="merge">
<div class="fg">

View File

@ -18,6 +18,7 @@ $startTime = (float)($_POST['start_time'] ?? 0);
$endTime = (float)($_POST['end_time'] ?? 0);
$language = strtolower(trim($_POST['language'] ?? 'en'));
$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en';
$noCache = filter_var($_POST['no_cache'] ?? false, FILTER_VALIDATE_BOOLEAN);
$translate = 'true';
if (!$reportCode || !$fightId || !$endTime) {
@ -27,7 +28,7 @@ if (!$reportCode || !$fightId || !$endTime) {
}
$cacheParts = [$fightId, (int)$startTime, (int)$endTime];
$cached = read_cached_log('analysis', $reportCode, $language, $cacheParts);
$cached = $noCache ? null : read_cached_log('analysis', $reportCode, $language, $cacheParts);
if ($cached !== null) {
echo $cached;
exit;
@ -196,8 +197,11 @@ function resolveMitigations(string $buffStr, array $mitigIdMap, array $buffSourc
'buffType' => $mitigIdMap[$id]['buffType'],
'extraAbilityGameID' => $mitigIdMap[$id]['extraAbilityGameID'] ?? null,
];
$source = findBuffSourcePlayer($buffSourceTimeline, $id, $ts, $players);
if ($source) $entry['sourcePlayerType'] = $source['type'];
$source = findBuffSourceEntry($buffSourceTimeline, $id, $ts, $players);
if ($source) {
$entry['sourcePlayerType'] = $source['player']['type'];
$entry['appliedAt'] = $source['apply'];
}
$result[] = $entry;
}
}
@ -205,7 +209,7 @@ function resolveMitigations(string $buffStr, array $mitigIdMap, array $buffSourc
}
// Findet den Spieler der einen Buff zum Zeitpunkt $ts gecastet hat (anhand der applybuff-Timeline).
function findBuffSourcePlayer(array $sourceTimeline, int $statusId, float $ts, array $players): ?array {
function findBuffSourceEntry(array $sourceTimeline, int $statusId, float $ts, array $players): ?array {
$best = null;
foreach ($sourceTimeline[$statusId] ?? [] as $entry) {
if ($entry['apply'] > $ts + 200) continue; // noch nicht aktiv
@ -213,7 +217,11 @@ function findBuffSourcePlayer(array $sourceTimeline, int $statusId, float $ts, a
if ($best === null || $entry['apply'] > $best['apply']) $best = $entry;
}
if ($best === null || empty($best['sourceId'])) return null;
return $players[$best['sourceId']] ?? null;
if (!isset($players[$best['sourceId']])) return null;
return [
'player' => $players[$best['sourceId']],
'apply' => $best['apply'],
];
}
// Fallback for shields consumed by a hit: the damage event's buffs field no
@ -233,6 +241,7 @@ function shieldsActiveAt(array $shieldTimeline, int $targetId, float $ts, array
'dr' => $m['dr'],
'buffType' => $m['buffType'],
'extraAbilityGameID' => $m['extraAbilityGameID'] ?? null,
'appliedAt' => $iv['apply'],
];
}
break;
@ -653,10 +662,37 @@ foreach ($clusters as $group) {
usort($bossEvents, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']);
usort($aoeEvents, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']);
$mitigationCasts = [];
$castSeen = [];
foreach ($buffSourceTimeline as $statusId => $entries) {
if (!isset($mitigIdMap[$statusId])) continue;
$meta = $mitigIdMap[$statusId];
foreach ($entries as $entry) {
$sourceId = (int)($entry['sourceId'] ?? 0);
$apply = (float)($entry['apply'] ?? 0);
if ($sourceId <= 0 || $apply <= 0 || !isset($players[$sourceId])) continue;
$dedupeKey = $statusId . ':' . $sourceId . ':' . (int)round($apply / 100);
if (isset($castSeen[$dedupeKey])) continue;
$castSeen[$dedupeKey] = true;
$mitigationCasts[] = [
'key' => $meta['key'] ?? $meta['name'],
'name' => $meta['name'],
'buffType' => $meta['buffType'],
'sourcePlayerType' => $players[$sourceId]['type'] ?? '',
'sourceName' => $players[$sourceId]['name'] ?? '',
'timestamp' => $apply,
'remove' => $entry['remove'] ?? null,
'extraAbilityGameID' => $meta['extraAbilityGameID'] ?? null,
];
}
}
usort($mitigationCasts, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']);
$response = json_encode([
'players' => array_values($players),
'boss_events' => $bossEvents,
'aoe_events' => $aoeEvents,
'mitigation_casts' => $mitigationCasts,
'fight_start' => (int)$startTime,
'mitigation_names' => $mitigationNames,
]);

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
const CACHED_LOG_DIR = __DIR__ . '/../cached_logs';
const CACHED_LOG_VERSION = 'v12';
const CACHED_LOG_VERSION = 'v14';
function cache_language(string $language): string {
$language = strtolower(trim($language));

View File

@ -15,6 +15,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? '');
$language = strtolower(trim($_POST['language'] ?? 'en'));
$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en';
$noCache = filter_var($_POST['no_cache'] ?? false, FILTER_VALIDATE_BOOLEAN);
if (strlen($reportCode) < 1) {
http_response_code(400);
@ -22,7 +23,7 @@ if (strlen($reportCode) < 1) {
exit;
}
$cached = read_cached_log('fight', $reportCode, $language);
$cached = $noCache ? null : read_cached_log('fight', $reportCode, $language);
if ($cached !== null) {
echo $cached;
exit;

View File

@ -98,6 +98,43 @@
color: var(--t2);
}
.report-load-row {
flex-wrap: nowrap;
}
.report-code-field {
flex: 1 1 360px;
min-width: 240px;
}
.report-language-field {
flex: 0 0 120px;
min-width: 100px;
}
.report-fight-field {
flex: 1 1 360px;
min-width: 260px;
}
.report-fight-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.report-fight-label a {
color: var(--gold);
font-size: 12px;
}
@media (max-width: 1100px) {
.report-load-row {
flex-wrap: wrap;
}
}
/* ── Topbar user badge ───────────────────────────────────────────────────────── */
.topbar-user {
font-size: 13px;

View File

@ -143,6 +143,14 @@
color: var(--t3);
}
.planner-header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
/* ── Job Slots ────────────────────────────────────────────────────────────────── */
.job-slots-grid {
display: flex;
@ -222,6 +230,435 @@
padding: 3px 6px;
}
/* ── Compare Modal ──────────────────────────────────────────────────────────── */
.planner-compare-modal-box {
display: flex;
flex-direction: column;
width: min(1680px, 98vw) !important;
height: 96vh !important;
max-width: none !important;
max-height: 96vh !important;
overflow: hidden;
padding: 0 !important;
}
.compare-modal-header {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 22px 26px 16px;
border-bottom: 1px solid var(--border);
background: var(--bgcard);
}
.compare-modal-header .modal-title {
margin-bottom: 4px;
}
.compare-modal-subtitle {
color: var(--t3);
font-size: 13px;
}
.compare-modal-header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
flex-shrink: 0;
}
.compare-modal-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 18px 26px 14px;
}
.compare-setup-grid {
display: grid;
grid-template-columns: minmax(360px, 0.8fr) minmax(0, 1.2fr);
gap: 14px;
align-items: end;
min-width: 0;
}
.compare-setup-grid .modal-section {
margin: 0;
min-width: 0;
}
.compare-setup-grid .name-import-input-row {
min-width: 0;
}
.compare-setup-grid .name-import-input-row .btn {
flex: 0 0 auto;
}
.compare-setup-grid input,
.compare-setup-grid select {
width: 100%;
min-width: 0;
}
.compare-modal-footer {
flex: 0 0 auto;
position: sticky;
bottom: 0;
z-index: 8;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
margin-top: 0 !important;
padding: 14px 26px 18px;
border-top: 1px solid var(--border);
background: var(--bgcard);
}
.planner-compare-result { margin-top: 16px; }
.compare-show-ok-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--t2);
font-size: 12px;
}
.compare-show-ok-toggle input {
width: auto;
margin: 0;
}
.compare-live-header {
position: sticky;
top: 0;
z-index: 4;
display: grid;
grid-template-columns: minmax(180px, 1fr) auto;
gap: 14px;
align-items: center;
padding: 12px;
border: 1px solid var(--border);
border-radius: var(--r);
background: rgba(14,18,27,.96);
}
.compare-live-header strong {
display: block;
color: var(--t1);
font-size: 15px;
}
.compare-live-header span {
color: var(--t3);
font-size: 12px;
}
.compare-summary {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 7px;
}
.compare-stat {
display: inline-flex;
min-width: 58px;
flex-direction: column;
align-items: center;
gap: 1px;
padding: 5px 8px;
border: 1px solid var(--border);
border-radius: var(--r);
background: var(--bg2);
color: var(--t2);
}
.compare-stat strong {
color: inherit;
font-size: 15px;
line-height: 1;
}
.compare-stat small {
color: inherit;
font-size: 10px;
text-transform: uppercase;
}
.compare-list {
display: flex;
flex-direction: column;
gap: 7px;
margin-top: 10px;
}
.compare-row {
display: grid;
grid-template-columns: 92px minmax(0, 1fr);
min-height: 68px;
border: 1px solid var(--border);
border-radius: var(--r);
background: rgba(255,255,255,.018);
overflow: hidden;
}
.compare-row-status {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
padding: 10px;
border-right: 1px solid var(--border);
background: rgba(255,255,255,.03);
text-align: center;
}
.compare-row-status strong {
color: var(--t1);
font-size: 12px;
text-transform: uppercase;
}
.compare-row-status span {
color: var(--gold);
font-size: 13px;
font-weight: 700;
}
.compare-row-body {
min-width: 0;
padding: 10px 12px;
}
.compare-row-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
color: var(--t1);
font-size: 13px;
}
.compare-row-title small {
color: var(--t3);
font-size: 11px;
}
.compare-row-warning {
color: var(--red);
font-size: 12px;
}
.compare-columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.compare-column-label {
margin-bottom: 4px;
color: var(--t3);
font-size: 10px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
}
.compare-chip-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.compare-status-line {
display: grid;
grid-template-columns: 68px minmax(0, 1fr);
align-items: start;
gap: 7px;
margin-bottom: 5px;
width: 100%;
}
.compare-status-line:last-child {
margin-bottom: 0;
}
.compare-status-line--no-label {
display: block;
}
.compare-status-label {
align-self: center;
padding: 3px 5px;
border: 1px solid var(--border);
border-radius: var(--r);
background: rgba(255,255,255,.025);
color: var(--t3);
font-size: 10px;
font-weight: 700;
text-align: center;
text-transform: uppercase;
}
.compare-status-label--missing { color: var(--red); border-color: rgba(224,92,92,.45); }
.compare-status-label--late { color: #ffb07a; border-color: rgba(255,145,75,.50); }
.compare-status-label--early { color: #ffd18a; border-color: rgba(255,190,90,.45); }
.compare-status-label--extra { color: var(--blue); border-color: rgba(74,158,255,.45); }
.compare-status-label--unknown { color: var(--t3); border-color: rgba(180,190,205,.35); }
.compare-status-label--ok { color: #8fd99e; border-color: rgba(88,180,116,.45); }
.compare-mini-chip {
display: inline-flex;
align-items: center;
gap: 5px;
max-width: 100%;
min-height: 25px;
padding: 3px 7px 3px 4px;
border: 1px solid var(--border);
border-radius: var(--r);
background: var(--bg2);
color: var(--t2);
font-size: 12px;
}
.compare-mini-chip img {
width: 18px;
height: 18px;
object-fit: contain;
flex-shrink: 0;
}
.compare-mini-chip strong {
color: var(--t1);
font-weight: 700;
}
.compare-mini-chip span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.compare-mini-chip small {
color: inherit;
font-size: 11px;
font-weight: 700;
}
.compare-stat--ok,
.compare-mini-chip--ok,
.compare-row--ok .compare-row-status {
border-color: rgba(88,180,116,.45);
color: #8fd99e;
}
.compare-stat--early,
.compare-mini-chip--early,
.compare-row--early .compare-row-status {
border-color: rgba(255,190,90,.50);
color: #ffd18a;
}
.compare-stat--late,
.compare-mini-chip--late,
.compare-row--late .compare-row-status {
border-color: rgba(255,145,75,.55);
color: #ffb07a;
}
.compare-stat--missing,
.compare-mini-chip--missing,
.compare-stat--unmatched,
.compare-row--missing .compare-row-status {
border-color: rgba(224,92,92,.55);
color: var(--red);
}
.compare-stat--extra,
.compare-mini-chip--extra,
.compare-row--extra .compare-row-status {
border-color: rgba(74,158,255,.45);
color: var(--blue);
}
.compare-stat--unknown,
.compare-mini-chip--unknown,
.compare-row--unknown .compare-row-status {
border-color: rgba(180,190,205,.35);
color: var(--t3);
}
.compare-empty,
.compare-loading {
color: var(--t3);
font-size: 13px;
}
.compare-empty--panel {
padding: 18px;
border: 1px solid var(--border);
border-radius: var(--r);
background: var(--bg2);
text-align: center;
}
.compare-error {
padding: 10px 12px;
border: 1px solid rgba(224,92,92,.45);
border-radius: var(--r);
color: var(--red);
background: rgba(224,92,92,.08);
font-size: 13px;
}
@media (max-width: 760px) {
.planner-compare-modal-box {
width: 98vw;
height: 94vh;
}
.compare-modal-header,
.compare-modal-body,
.compare-modal-footer {
padding-left: 14px;
padding-right: 14px;
}
.compare-modal-header {
align-items: flex-start;
flex-direction: column;
}
.compare-modal-header-actions {
justify-content: flex-start;
width: 100%;
}
.compare-setup-grid,
.compare-live-header,
.compare-columns,
.compare-row {
grid-template-columns: 1fr;
}
.compare-row-status {
border-right: none;
border-bottom: 1px solid var(--border);
text-align: left;
}
}
/* ── Mechanic Cards ──────────────────────────────────────────────────────────── */
.mechanic-card {
display: grid;
@ -711,11 +1148,26 @@
/* ── Planner Timeline ───────────────────────────────────────────────────────── */
.timeline-hint {
font-size: 12px;
color: var(--t3);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 12px;
color: var(--t3);
margin-bottom: 8px;
}
.timeline-zoom-hint {
display: inline-flex;
align-items: center;
padding: 2px 7px;
border: 1px solid rgba(200,168,75,.25);
border-radius: var(--r);
background: rgba(200,168,75,.08);
color: var(--gold);
font-weight: 600;
}
.timeline-controls {
display: flex;
align-items: center;
@ -850,18 +1302,42 @@
position: absolute;
top: 0;
bottom: 0;
width: 1px;
width: 10px;
transform: translateX(-50%);
background: rgba(200,168,75,.38);
pointer-events: none;
background: transparent;
pointer-events: auto;
cursor: help;
z-index: 1;
}
.timeline-hit-line::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
transform: translateX(-50%);
background: rgba(200,168,75,.38);
}
.timeline-hit-line--tankbuster {
width: 12px;
}
.timeline-hit-line--tankbuster::before {
width: 2px;
background: rgba(177,112,255,.62);
}
.timeline-hit-line:hover::before {
background: var(--gold);
}
.timeline-hit-line--tankbuster:hover::before {
background: #b170ff;
}
.timeline-player-row .timeline-track {
background-color: rgba(255,255,255,0.015);
}
@ -993,6 +1469,39 @@
opacity: 0.78;
}
.timeline-inactive-gap {
position: absolute;
top: 10px;
height: 20px;
z-index: 2;
pointer-events: auto;
cursor: help;
}
.timeline-inactive-gap-fit,
.timeline-inactive-gap-late {
position: absolute;
top: 9px;
height: 0;
}
.timeline-inactive-gap-fit {
left: 0;
border-top: 2px dotted rgba(180,190,205,.42);
}
.timeline-inactive-gap-late {
border-top: 2px dotted rgba(224,92,92,.70);
}
.timeline-inactive-gap:hover .timeline-inactive-gap-fit {
border-top-color: var(--gold);
}
.timeline-inactive-gap:hover .timeline-inactive-gap-late {
border-top-color: var(--red);
}
.timeline-mitigation img {
width: 18px;
height: 18px;
@ -1233,11 +1742,229 @@
font-size: 12px;
}
.timeline-menu-time {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 9px 9px;
margin-bottom: 4px;
border-bottom: 1px solid var(--border);
color: var(--t3);
font-size: 11px;
text-transform: uppercase;
}
.timeline-menu-time input {
width: 76px !important;
padding: 5px 7px !important;
font-size: 12px !important;
}
/* ── View Toggle ─────────────────────────────────────────────────────────── */
.view-toggle-btns {
display: flex;
gap: 4px;
.planner-card-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.planner-class-filter-wrap {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--t3);
font-size: 11px;
white-space: nowrap;
}
.planner-class-filter {
width: auto;
min-width: 88px;
padding: 4px 9px;
border: 1px solid var(--border);
border-radius: var(--r);
background: var(--bg2);
color: var(--t1);
font-size: 12px;
cursor: pointer;
}
.planner-class-filter:hover {
border-color: var(--t3);
}
.cactbot-export-modal-box {
max-width: 720px;
max-height: min(86vh, 780px);
overflow-y: auto;
padding: 0;
}
.cactbot-modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
padding: 24px 28px 18px;
border-bottom: 1px solid var(--border);
}
.cactbot-modal-header .modal-title {
margin-bottom: 5px;
}
.cactbot-modal-subtitle {
color: var(--t3);
font-size: 13px;
}
.cactbot-export-options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding: 18px 28px 0;
}
.cactbot-option-card,
.cactbot-check-card {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid var(--border);
border-radius: var(--r);
background: var(--bg2);
cursor: pointer;
transition: border-color .15s, background .15s;
}
.cactbot-option-card {
padding: 12px;
}
.cactbot-option-card:hover,
.cactbot-check-card:hover {
border-color: var(--borderem);
background: var(--bg3);
}
.cactbot-option-card input,
.cactbot-check-card input {
width: auto;
margin: 0;
flex-shrink: 0;
}
.cactbot-option-card span,
.cactbot-check-card span {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.cactbot-option-card strong,
.cactbot-check-card strong {
color: var(--t1);
font-size: 13px;
font-weight: 600;
}
.cactbot-option-card small,
.cactbot-check-card small {
color: var(--t3);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cactbot-field {
display: flex;
flex-direction: column;
gap: 6px;
padding: 16px 28px 0;
}
.cactbot-field label,
.cactbot-section-title {
color: var(--t2);
font-size: 11px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
}
.cactbot-field input {
font-size: 13px;
}
.cactbot-check-grid,
.cactbot-role-grid,
.cactbot-player-columns {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 8px;
}
.cactbot-role-grid,
.cactbot-player-columns {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.cactbot-player-columns {
gap: 10px;
align-items: start;
}
.cactbot-player-column {
display: flex;
min-width: 0;
flex-direction: column;
gap: 8px;
}
.cactbot-player-column-title {
padding: 0 2px 2px;
color: var(--gold);
font-size: 11px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
}
.cactbot-check-card {
padding: 9px 10px;
}
.cactbot-check-card--role {
min-height: 40px;
}
.cactbot-section {
display: flex;
flex-direction: column;
gap: 9px;
padding: 18px 28px 0;
}
.cactbot-modal-actions {
position: sticky;
bottom: 0;
justify-content: flex-end;
padding: 18px 28px 24px;
margin-top: 8px;
border-top: 1px solid var(--border);
background: var(--bgcard);
}
@media (max-width: 680px) {
.cactbot-export-options,
.cactbot-role-grid,
.cactbot-player-columns {
grid-template-columns: 1fr;
}
}
.view-toggle-btn {
@ -1264,7 +1991,7 @@
font-weight: 600;
}
/* ── Meine Spells ────────────────────────────────────────────────────────── */
/* ── Simple View Rows ────────────────────────────────────────────────────── */
.myspells-controls {
display: flex;
@ -1333,6 +2060,18 @@
white-space: nowrap;
}
.myspells-ability .badge-remove {
opacity: .45;
}
.myspells-ability:hover .badge-remove {
opacity: .8;
}
.myspells-ability .badge-remove:hover {
opacity: 1;
}
.myspells-ability.myspells-type--debuff { color: var(--orange); border-color: rgba(255,140,0,0.3); }
.myspells-ability.myspells-type--shield { color: var(--blue); border-color: rgba(88,166,255,0.3); }
.myspells-ability.myspells-type--personal { color: #dbc7ff; border-color: rgba(177,112,255,0.4); }

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
<div class="card section-gap" id="fight-select-card" style="display:none">
<div class="card-title-row">
<div class="card-title">Fight auswählen</div>
<a id="fflogs-report-link" class="btn btn-sm" href="#" target="_blank" rel="noopener" style="display:none;text-decoration:none;margin-left:auto">FFLogs öffnen</a>
</div>
<select id="fight-select">
<option value=""> Fight auswählen </option>
</select>
</div>

View File

@ -80,6 +80,51 @@
</div>
</div>
<!-- Planner Compare Modal -->
<div id="planner-compare-modal" class="modal-overlay" style="display:none">
<div class="modal-box planner-compare-modal-box">
<div class="compare-modal-header">
<div>
<div class="modal-title">Plan mit FFLogs vergleichen</div>
<div class="compare-modal-subtitle">Schnellcheck für live Logs: fehlende, frühe, späte und extra Mitigations.</div>
</div>
<div class="compare-modal-header-actions">
<label class="compare-show-ok-toggle" id="compare-show-ok-wrap" style="display:none">
<input type="checkbox" id="compare-show-ok">
<span>OK anzeigen</span>
</label>
<button id="compare-run-btn" class="btn btn-gold" style="display:none">Vergleichen</button>
<button id="compare-refresh-btn" class="btn" style="display:none">Refresh live log</button>
</div>
</div>
<div class="compare-modal-body">
<div class="compare-setup-grid">
<div class="modal-section">
<div class="modal-label">Report-Code</div>
<div class="name-import-input-row">
<input type="text" id="compare-report-input" placeholder="Report-Code oder URL…">
<button id="compare-load-btn" class="btn btn-sm">Fights laden</button>
</div>
</div>
<div class="modal-section" id="compare-fight-section" style="display:none">
<div class="modal-label">Fight</div>
<select id="compare-fight-select">
<option value=""> Fight auswählen </option>
</select>
</div>
</div>
<div id="compare-result" class="planner-compare-result" style="display:none"></div>
</div>
<div class="modal-actions compare-modal-footer">
<button id="compare-cancel-btn" class="btn">Schließen</button>
</div>
</div>
</div>
<!-- Name Import Modal (Planner) -->
<div id="planner-name-import-modal" class="modal-overlay" style="display:none">
<div class="modal-box">

View File

@ -1,8 +1,8 @@
<div class="card section-gap">
<div class="card-title">Report laden</div>
<form id="report-form">
<div class="form-row">
<div class="fg">
<div class="form-row report-load-row">
<div class="fg report-code-field">
<label>Report Code</label>
<input
type="text"
@ -13,8 +13,8 @@
required
>
</div>
<div class="fg">
<label>Namen</label>
<div class="fg report-language-field">
<label>Language</label>
<select name="language" id="language-select">
<option value="en">EN</option>
<option value="de">DE</option>
@ -23,7 +23,15 @@
</select>
</div>
<button class="btn btn-gold" type="submit" style="align-self:flex-end">Fetch</button>
<a class="btn" href="<?= htmlspecialchars(auth_start_href(), ENT_QUOTES) ?>" style="align-self:flex-end;text-decoration:none">Reconnect</a>
<div class="fg report-fight-field" id="fight-select-card" style="display:none">
<label class="report-fight-label">
Fight
<a id="fflogs-report-link" href="#" target="_blank" rel="noopener" style="display:none;text-decoration:none">FFLogs öffnen</a>
</label>
<select id="fight-select">
<option value=""> Fight auswählen </option>
</select>
</div>
</div>
</form>
</div>

View File

@ -1,3 +1,5 @@
<?php require __DIR__ . '/report-form.php'; ?>
<div id="analysis-loading" class="loading" style="display:none">
<div class="spinner"></div>
<span>Analysiere Fight-Daten...</span>
@ -5,7 +7,7 @@
<div id="analysis-empty" class="empty">
<div class="empty-icon">📊</div>
<h3 id="analysis-empty-msg">Bitte zuerst einen Fight im Report-Tab auswählen</h3>
<h3 id="analysis-empty-msg">Bitte zuerst einen Fight auswählen</h3>
</div>
<div id="analysis-content" style="display:none">

View File

@ -1,4 +1,2 @@
<?php require __DIR__ . '/report-form.php'; ?>
<?php require __DIR__ . '/fight-select.php'; ?>
<?php require __DIR__ . '/event-explorer.php'; ?>
<?php require __DIR__ . '/output-card.php'; ?>

View File

@ -1,14 +1,14 @@
<header id="topbar">
<div class="logo">REPORT VIEWER <span>Final Fantasy XIV</span></div>
<nav class="tabs">
<button class="tab active" data-tab="report"> Report</button>
<button class="tab" data-tab="analysis"> Analyse</button>
<button class="tab" data-tab="planner"> Planer</button>
<button class="tab active" data-tab="report"> Debug</button>
</nav>
<?php $fflogsUser = current_fflogs_user(); ?>
<div class="topbar-actions">
<?php if (is_admin_user()): ?>
<a class="topbar-link" href="admin.php">Admin</a>
<a class="topbar-link" href="admin.php?return=<?= rawurlencode(current_return_path()) ?>">Admin</a>
<?php endif; ?>
<div class="topbar-user">
<?php if ($fflogsUser): ?>
@ -16,5 +16,6 @@
<?php endif; ?>
Token gültig bis: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?>
</div>
<a class="topbar-link" href="<?= htmlspecialchars(auth_start_href(), ENT_QUOTES) ?>">Reconnect</a>
</div>
</header>