Initial commit: FFLogs PKCE OAuth viewer
This commit is contained in:
commit
cf188c1727
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.claude/
|
||||||
|
debug/
|
||||||
|
fflogs-schema.json
|
||||||
48
CLAUDE.md
Normal file
48
CLAUDE.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# ff14-auth — FFLogs Report Viewer
|
||||||
|
|
||||||
|
## Projekt
|
||||||
|
Einfache PHP/HTML/JS-Seite zum Verbinden mit FFLogs via OAuth2 PKCE und Abrufen von Report-Daten über die GraphQL-API. Kein Framework, kein Composer, kein npm — Plain PHP für Shared Hosting.
|
||||||
|
|
||||||
|
## Dateistruktur
|
||||||
|
```
|
||||||
|
config.php — Konstanten (CLIENT_ID, URIs) + session_start_safe()
|
||||||
|
index.php — Haupt-UI: Connect-Button / Report-Formular / Terminal-Output
|
||||||
|
auth/
|
||||||
|
start.php — PKCE generieren, Session speichern, Redirect zu FFLogs
|
||||||
|
callback.php — Code gegen Token tauschen, Token in Session speichern
|
||||||
|
api/
|
||||||
|
fight.php — POST-Endpunkt: GraphQL-Query → JSON
|
||||||
|
js/
|
||||||
|
app.js — Formular, Dropdown, Fetch, Ausgabe
|
||||||
|
debug/
|
||||||
|
schema.php — Einmaliges Schema-Explorer Tool (nicht produktiv deployen)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
- `config.php`: `CLIENT_ID`, `REDIRECT_URI`, `DEV_MODE` anpassen
|
||||||
|
- `DEV_MODE = true` deaktiviert SSL-Verifizierung (nur lokal, nie in Produktion)
|
||||||
|
- `session.cookie_secure` ist bei `DEV_MODE` automatisch `false`
|
||||||
|
|
||||||
|
## FFLogs API
|
||||||
|
- OAuth2 PKCE (kein Client Secret, öffentliche App)
|
||||||
|
- App registrieren: https://www.fflogs.com/api/clients/
|
||||||
|
- GraphQL Endpoint (user-scoped): `https://www.fflogs.com/api/v2/user`
|
||||||
|
- Token Endpoint: `https://www.fflogs.com/oauth/token`
|
||||||
|
- Kein Refresh Token für öffentliche Clients — abgelaufene Sessions starten PKCE neu
|
||||||
|
|
||||||
|
## Lokale Entwicklung
|
||||||
|
```
|
||||||
|
php -S localhost:8080
|
||||||
|
```
|
||||||
|
Dann `http://localhost:8080` im Browser öffnen.
|
||||||
|
Redirect URI in FFLogs App und config.php: `http://localhost:8080/auth/callback.php`
|
||||||
|
|
||||||
|
## Bekannte Schema-Infos (ReportFight)
|
||||||
|
Verfügbare aber noch nicht genutzte Felder: `friendlyPlayers`, `enemyNPCs`,
|
||||||
|
`lastPhase`, `standardComposition`, `hasEcho`, `combatTime`, `phaseTransitions`
|
||||||
|
Vollständiges Schema: siehe `debug/schema.php` oder `fflogs-schema.json`
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
- `DEV_MODE` auf `false` setzen
|
||||||
|
- `REDIRECT_URI` auf produktive HTTPS-URL anpassen
|
||||||
|
- `debug/` Ordner nicht deployen
|
||||||
98
api/fight.php
Normal file
98
api/fight.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
session_start_safe();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Method not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? '');
|
||||||
|
|
||||||
|
if (strlen($reportCode) < 1) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Missing or invalid report_code']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_SESSION['access_token'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Not authenticated', 'reauth' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($_SESSION['token_expires'] ?? 0) <= time()) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Token expired', 'reauth' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = <<<'GQL'
|
||||||
|
query GetReportData($reportCode: String!) {
|
||||||
|
reportData {
|
||||||
|
report(code: $reportCode) {
|
||||||
|
title
|
||||||
|
startTime
|
||||||
|
endTime
|
||||||
|
fights {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
startTime
|
||||||
|
endTime
|
||||||
|
kill
|
||||||
|
difficulty
|
||||||
|
bossPercentage
|
||||||
|
fightPercentage
|
||||||
|
averageItemLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GQL;
|
||||||
|
|
||||||
|
$payload = json_encode([
|
||||||
|
'query' => $query,
|
||||||
|
'variables' => ['reportCode' => $reportCode],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ch = curl_init(GRAPHQL_URI);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $_SESSION['access_token'],
|
||||||
|
],
|
||||||
|
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
||||||
|
]);
|
||||||
|
$body = curl_exec($ch);
|
||||||
|
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
http_response_code(502);
|
||||||
|
echo json_encode(['error' => 'Network error: ' . $curlError]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
if ($decoded === null) {
|
||||||
|
http_response_code(502);
|
||||||
|
echo json_encode(['error' => 'Invalid response from FFLogs', 'raw' => substr($body, 0, 500)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpStatus === 401) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'FFLogs rejected token', 'reauth' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code($httpStatus === 200 ? 200 : $httpStatus);
|
||||||
|
echo $body;
|
||||||
|
exit;
|
||||||
67
auth/callback.php
Normal file
67
auth/callback.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
session_start_safe();
|
||||||
|
|
||||||
|
// user denied access
|
||||||
|
if (isset($_GET['error'])) {
|
||||||
|
header('Location: ../index.php?error=' . urlencode($_GET['error']));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF check
|
||||||
|
if (
|
||||||
|
empty($_GET['state']) ||
|
||||||
|
empty($_SESSION['oauth_state']) ||
|
||||||
|
!hash_equals($_SESSION['oauth_state'], $_GET['state'])
|
||||||
|
) {
|
||||||
|
session_destroy();
|
||||||
|
http_response_code(400);
|
||||||
|
echo 'Invalid state parameter. Possible CSRF attack. <a href="../index.php">Back</a>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_GET['code'])) {
|
||||||
|
header('Location: ../index.php?error=missing_code');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$verifier = $_SESSION['pkce_verifier'];
|
||||||
|
unset($_SESSION['pkce_verifier'], $_SESSION['oauth_state']);
|
||||||
|
|
||||||
|
$post = http_build_query([
|
||||||
|
'grant_type' => 'authorization_code',
|
||||||
|
'client_id' => CLIENT_ID,
|
||||||
|
'redirect_uri' => REDIRECT_URI,
|
||||||
|
'code' => $_GET['code'],
|
||||||
|
'code_verifier' => $verifier,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ch = curl_init(TOKEN_URI);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $post,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
|
||||||
|
CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
|
||||||
|
]);
|
||||||
|
$body = curl_exec($ch);
|
||||||
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
|
||||||
|
if ($curlError || $status !== 200 || empty($data['access_token'])) {
|
||||||
|
$_SESSION['token_debug'] = [
|
||||||
|
'curl_error' => $curlError ?: null,
|
||||||
|
'http_status' => $status,
|
||||||
|
'response_body' => $body,
|
||||||
|
];
|
||||||
|
header('Location: ../index.php?error=token_failed');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['access_token'] = $data['access_token'];
|
||||||
|
$_SESSION['token_expires'] = time() + ($data['expires_in'] ?? 3600);
|
||||||
|
|
||||||
|
header('Location: ../index.php');
|
||||||
|
exit;
|
||||||
22
auth/start.php
Normal file
22
auth/start.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
session_start_safe();
|
||||||
|
|
||||||
|
$verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
|
||||||
|
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
|
||||||
|
$state = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
|
$_SESSION['pkce_verifier'] = $verifier;
|
||||||
|
$_SESSION['oauth_state'] = $state;
|
||||||
|
|
||||||
|
$params = http_build_query([
|
||||||
|
'response_type' => 'code',
|
||||||
|
'client_id' => CLIENT_ID,
|
||||||
|
'redirect_uri' => REDIRECT_URI,
|
||||||
|
'state' => $state,
|
||||||
|
'code_challenge' => $challenge,
|
||||||
|
'code_challenge_method' => 'S256',
|
||||||
|
]);
|
||||||
|
|
||||||
|
header('Location: ' . AUTHORIZE_URI . '?' . $params);
|
||||||
|
exit;
|
||||||
21
config.php
Normal file
21
config.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
define('DEV_MODE', true); // set to false in production
|
||||||
|
define('CLIENT_ID', 'a1d27cba-b7f8-48dd-aefd-4697b457cc67');
|
||||||
|
define('REDIRECT_URI', 'http://localhost:8080/auth/callback.php');
|
||||||
|
define('AUTHORIZE_URI','https://www.fflogs.com/oauth/authorize');
|
||||||
|
define('TOKEN_URI', 'https://www.fflogs.com/oauth/token');
|
||||||
|
define('GRAPHQL_URI', 'https://www.fflogs.com/api/v2/user');
|
||||||
|
|
||||||
|
function session_start_safe(): void {
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => false, // set to true in production (HTTPS)
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
216
index.php
Normal file
216
index.php
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
session_start_safe();
|
||||||
|
|
||||||
|
$authenticated = !empty($_SESSION['access_token'])
|
||||||
|
&& ($_SESSION['token_expires'] ?? 0) > time();
|
||||||
|
|
||||||
|
$tokenExpired = !empty($_SESSION['access_token'])
|
||||||
|
&& ($_SESSION['token_expires'] ?? 0) <= time();
|
||||||
|
|
||||||
|
$error = $_GET['error'] ?? null;
|
||||||
|
$errorMessages = [
|
||||||
|
'access_denied' => 'Access denied — you declined the FFLogs authorization.',
|
||||||
|
'token_failed' => 'Could not retrieve access token from FFLogs. Please try again.',
|
||||||
|
'missing_code' => 'Authorization code missing in callback.',
|
||||||
|
];
|
||||||
|
$errorText = $error ? ($errorMessages[$error] ?? 'Unknown error: ' . htmlspecialchars($error)) : null;
|
||||||
|
$tokenDebug = $_SESSION['token_debug'] ?? null;
|
||||||
|
unset($_SESSION['token_debug']);
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>FFLogs Report Viewer</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 860px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #a78bfa;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
background: #3b0f0f;
|
||||||
|
border: 1px solid #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #1e2a1e;
|
||||||
|
border: 1px solid #365936;
|
||||||
|
color: #86efac;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
background: #7c3aed;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.btn:hover { background: #6d28d9; }
|
||||||
|
.btn-secondary {
|
||||||
|
background: #374151;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.4rem 0.9rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { background: #4b5563; }
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 180px;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
background: #111827;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input[type="text"]:focus, select:focus { border-color: #7c3aed; }
|
||||||
|
input[type="text"]::placeholder { color: #4b5563; }
|
||||||
|
select option { background: #111827; }
|
||||||
|
#fight-select-row { display: none; }
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #2d2d2d;
|
||||||
|
color: #a3e635;
|
||||||
|
padding: 1.25rem;
|
||||||
|
min-height: 260px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.footer-link a { color: #6b7280; text-decoration: none; }
|
||||||
|
.footer-link a:hover { color: #9ca3af; }
|
||||||
|
|
||||||
|
.connect-area {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.connect-area h1 { margin-bottom: 0.5rem; }
|
||||||
|
.connect-area p {
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<?php if ($errorText): ?>
|
||||||
|
<div class="error-box"><?= $errorText ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($tokenDebug): ?>
|
||||||
|
<div class="error-box">
|
||||||
|
<strong>Debug — FFLogs Token Response:</strong><br>
|
||||||
|
HTTP <?= (int)$tokenDebug['http_status'] ?><?= $tokenDebug['curl_error'] ? ' | curl: ' . htmlspecialchars($tokenDebug['curl_error']) : '' ?><br>
|
||||||
|
<pre style="margin-top:0.5rem;white-space:pre-wrap;font-size:0.8rem;"><?= htmlspecialchars($tokenDebug['response_body'] ?? '(empty)') ?></pre>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!$authenticated && !$tokenExpired): ?>
|
||||||
|
<div class="connect-area">
|
||||||
|
<h1>// FFLogs Report Viewer</h1>
|
||||||
|
<p>Connect your FFLogs account to fetch report data.</p>
|
||||||
|
<a class="btn" href="auth/start.php">Connect to FFLogs</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php elseif ($tokenExpired): ?>
|
||||||
|
<div class="connect-area">
|
||||||
|
<h1>// FFLogs Report Viewer</h1>
|
||||||
|
<div class="info-box">Session expired. Please reconnect.</div>
|
||||||
|
<a class="btn" href="auth/start.php">Reconnect to FFLogs</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<h1>// FFLogs Report Viewer</h1>
|
||||||
|
|
||||||
|
<form id="report-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="report_code"
|
||||||
|
placeholder="Report Code (e.g. aBcDeFgH1234)"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<button class="btn" type="submit">Fetch</button>
|
||||||
|
<a class="btn btn-secondary" href="auth/start.php">Reconnect</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="form-row" id="fight-select-row">
|
||||||
|
<select id="fight-select">
|
||||||
|
<option value="">— Fight auswählen —</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre id="output" class="terminal">// paste a report code above and hit Fetch</pre>
|
||||||
|
|
||||||
|
<div class="footer-link">
|
||||||
|
Token valid until: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
95
js/app.js
Normal file
95
js/app.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const form = document.getElementById('report-form');
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const fightSelectRow = document.getElementById('fight-select-row');
|
||||||
|
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 0–10000 (e.g. 5000 = 50.00%)
|
||||||
|
return (pct / 100).toFixed(2) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayFight(fight) {
|
||||||
|
output.textContent = JSON.stringify(fight, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fightSelect.addEventListener('change', () => {
|
||||||
|
const id = parseInt(fightSelect.value, 10);
|
||||||
|
const fight = allFights.find(f => f.id === id);
|
||||||
|
if (fight) displayFight(fight);
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
output.textContent = '// fetching...';
|
||||||
|
fightSelectRow.style.display = 'none';
|
||||||
|
fightSelect.innerHTML = '<option value="">— Fight auswählen —</option>';
|
||||||
|
allFights = [];
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
report_code: form.elements['report_code'].value.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let response, json;
|
||||||
|
try {
|
||||||
|
response = await fetch('api/fight.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: params.toString(),
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate dropdown
|
||||||
|
allFights.forEach(fight => {
|
||||||
|
const duration = formatDuration(fight.endTime - fight.startTime);
|
||||||
|
const hp = formatBossHp(fight);
|
||||||
|
const label = `${fight.name} — ${duration} — ${hp}`;
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = fight.id;
|
||||||
|
opt.textContent = label;
|
||||||
|
fightSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
fightSelectRow.style.display = 'flex';
|
||||||
|
output.textContent = '// Fight auswählen ↑';
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user