eve-indy-job-tracker/dashboard.php

625 lines
26 KiB
PHP
Executable file

<?php
require_once __DIR__ . "/session_bootstrap.php";
require_once "functions.php";
// Set a timestamp for when the page was loaded
$page_load_time = time();
// Check if this is an AJAX request for additional data
$is_ajax = isset($_GET["ajax"]) && $_GET["ajax"] === "1";
// Initialize job data array for progressive loading
$all_jobs = [];
$character_ids = [];
// Refresh tokens only when necessary
if (isset($_SESSION["characters"]) && !empty($_SESSION["characters"])) {
// Only refresh tokens once every 5 minutes per user session
$last_token_refresh = $_SESSION["last_token_refresh"] ?? 0;
if ($page_load_time - $last_token_refresh > 300) {
foreach ($_SESSION["characters"] as $character_id => &$charData) {
if (isset($charData["refresh_token"])) {
// Check if the access token is expired or missing
if (
!isset($charData["access_token"]) ||
is_token_expired($charData["access_token"])
) {
$new_tokens = refresh_token($charData["refresh_token"]);
if (!empty($new_tokens["access_token"])) {
// Update session with new access token
$charData["access_token"] = $new_tokens["access_token"];
} else {
error_log(
"Failed to refresh token for character ID $character_id"
);
}
}
}
}
unset($charData); // Break reference to avoid unexpected behavior
$_SESSION["last_token_refresh"] = $page_load_time;
}
// Get list of character IDs
$character_ids = array_keys($_SESSION["characters"]);
}
if (!isset($_SESSION["characters"]) || empty($_SESSION["characters"])) {
echo "No characters logged in. <a href='index.php'>Go back</a>";
exit();
}
// For AJAX requests, only fetch requested character data and return JSON
if ($is_ajax) {
$requested_char_id = isset($_GET["character_id"])
? $_GET["character_id"]
: null;
if (
$requested_char_id &&
isset($_SESSION["characters"][$requested_char_id])
) {
$charData = $_SESSION["characters"][$requested_char_id];
$access_token = $charData["access_token"] ?? null;
if ($access_token) {
$jobs = fetch_character_jobs($requested_char_id, $access_token);
// Format jobs for JSON response
$formatted_jobs = [];
foreach ($jobs as $job) {
$end_timestamp = is_numeric($job["end_time_unix"])
? $job["end_time_unix"]
: 0;
$formatted_jobs[] = [
"character" => htmlspecialchars($job["character"]),
"blueprint" => htmlspecialchars($job["blueprint"]),
"activity" => htmlspecialchars($job["activity"]),
"location" => htmlspecialchars($job["location"]),
"start_time" => htmlspecialchars($job["start_time"]),
"end_time" => htmlspecialchars($job["end_time"]),
"end_time_unix" => $end_timestamp,
"time_left" => htmlspecialchars($job["time_left"]),
];
}
header("Content-Type: application/json");
echo json_encode(["jobs" => $formatted_jobs]);
exit();
}
}
// If we get here, something went wrong
header("HTTP/1.1 400 Bad Request");
echo json_encode(["error" => "Invalid request"]);
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Industry Jobs Dashboard</title>
<style>
th.sorted-asc::after { content: " ▲"; }
th.sorted-desc::after { content: " ▼"; }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-row td {
text-align: center;
padding: 20px;
background-color: #1e1e1e;
color: #d4d4d4;
border-bottom: 1px solid #333;
}
</style>
</head>
<body>
<h1>Industry Jobs</h1>
<table id="jobsTable">
<thead>
<tr>
<th>Character</th>
<th>Blueprint</th>
<th>Activity</th>
<th>Location</th>
<th>Start Time</th>
<th>End Time</th>
<th>Time Left</th>
</tr>
</thead>
<tbody>
<?php // Load data for the first character only (for fast initial load)
if (!empty($character_ids)) {
$first_char_id = $character_ids[0];
$charData = $_SESSION["characters"][$first_char_id];
$access_token = $charData["access_token"] ?? null;
if ($access_token) {
$jobs = fetch_character_jobs($first_char_id, $access_token);
if (empty($jobs)) {
echo "<table class='no-jobs-table'><tr><td>No jobs found for " . htmlspecialchars($charData["name"]) . "</td></tr></table>";
} else {
foreach ($jobs as $job) {
$end_timestamp = is_numeric($job["end_time_unix"])
? $job["end_time_unix"]
: 0;
$time_left_display = htmlspecialchars(
$job["time_left"]
);
echo "<tr>";
echo "<td>" .
htmlspecialchars($job["character"]) .
"</td>";
echo "<td>" .
htmlspecialchars($job["blueprint"]) .
"</td>";
echo "<td>" .
htmlspecialchars($job["activity"]) .
"</td>";
echo "<td>" .
htmlspecialchars($job["location"]) .
"</td>";
echo "<td>" .
htmlspecialchars($job["start_time"]) .
"</td>";
echo "<td>" .
htmlspecialchars($job["end_time"]) .
"</td>";
echo "<td class='time-left' data-endtime='$end_timestamp'>" .
$time_left_display .
"</td>";
echo "</tr>";
}
}
}
// Add loading placeholder rows for other characters
if (count($character_ids) > 1) {
foreach (array_slice($character_ids, 1) as $char_id) {
$char_name =
$_SESSION["characters"][$char_id]["name"] ??
"Character";
echo "<tr id='loading-$char_id' class='loading-row'>";
echo "<td colspan='7'>Loading jobs for " .
htmlspecialchars($char_name) .
" <div class='loader'></div></td>";
echo "</tr>";
}
}
} ?>
</tbody>
</table>
<!-- List of all logged-in toons -->
<div class="character-list">
<h2>Logged-in Characters</h2>
<table id="jobsTable">
<thead>
<tr>
<th>Name</th>
<th>Character ID</th>
</tr>
</thead>
<tbody>
<?php foreach (
$_SESSION["characters"]
as $character_id => $charData
): ?>
<tr>
<td><?php echo htmlspecialchars(
$charData["name"]
); ?></td>
<td><?php echo $character_id; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
// Track page visibility to handle inactive tabs
let pageVisible = true;
document.addEventListener("visibilitychange", () => {
pageVisible = document.visibilityState === "visible";
if (pageVisible) {
// Force a data refresh if the page has been hidden for a while
const lastRefresh = localStorage.getItem('lastDashboardRefresh');
const now = Date.now();
if (lastRefresh && (now - parseInt(lastRefresh) > 1800000)) { // 30 minutes
window.location.reload();
}
}
});
// Store the last refresh time
localStorage.setItem('lastDashboardRefresh', Date.now().toString());
document.addEventListener("DOMContentLoaded", () => {
let sortDirection = [];
let animationFrameId = null;
let jobsLoading = true;
let lastSortedCol = null;
let lastSortedDir = null;
// Jobs Table Sorting (disabled while loading)
const jobsTableHead = document.getElementById('jobsTable').querySelector('thead');
jobsTableHead.addEventListener('click', (event) => {
if (jobsLoading) {
// Optionally show a tooltip or visual cue here
return;
}
const th = event.target;
if (th.tagName === 'TH') {
const col = Array.from(th.parentNode.children).indexOf(th);
sortTable('jobsTable', col);
lastSortedCol = col;
lastSortedDir = sortDirection[col];
}
});
// Visual cue: change cursor to not-allowed while loading
jobsTableHead.style.cursor = "not-allowed";
// Character Table Sorting
const characterTable = document.getElementById('characterTable');
if (characterTable) {
characterTable.querySelector('thead').addEventListener('click', (event) => {
const th = event.target;
if (th.tagName === 'TH') {
const col = Array.from(th.parentNode.children).indexOf(th);
sortTable('characterTable', col);
}
});
}
function sortTable(tableId, col) {
const table = document.getElementById(tableId);
const rows = Array.from(table.tBodies[0].rows).map(row => {
return {
element: row,
data: Array.from(row.cells).map(cell => {
// Try to convert numeric strings to numbers for sorting
const cellText = cell.textContent.trim();
const number = parseFloat(cellText.replace(/,/g, ""));
return isNaN(number) ? cellText : number;
})
};
});
const dir = sortDirection[col] = !sortDirection[col];
// Create a document fragment for better performance
const fragment = document.createDocumentFragment();
rows.sort((a, b) => {
const aText = a.data[col];
const bText = b.data[col];
// Special handling for Time Left column in jobsTable (index 6)
if (tableId === 'jobsTable' && col === 6) {
const aTime = parseInt(a.element.cells[col].dataset.endtime) || 0;
const bTime = parseInt(b.element.cells[col].dataset.endtime) || 0;
// Ensures consistent sorting for time regardless of data type
return dir
? (typeof bTime === "number" && typeof aTime === "number" ? bTime - aTime : aText.toString().localeCompare(bText.toString()))
: (typeof aTime === "number" && typeof bTime === "number" ? aTime - bTime : aText.toString().localeCompare(bText.toString()));
}
// Numeric sort if both are numbers
if (!isNaN(aText) && !isNaN(bText) && aText !== "" && bText !== "") {
return dir ? bText - aText : aText - bText;
}
// Return comparison by trying to respect data types
return dir
? (typeof bText === "number" && typeof aText === "number" ? bText - aText : bText.toString().localeCompare(aText.toString()))
: (typeof aText === "number" && typeof bText === "number" ? aText - bText : aText.toString().localeCompare(bText.toString()));
});
rows.map(row => row.element).forEach(element => fragment.appendChild(element));
const tbody = table.tBodies[0];
tbody.innerHTML = '';
tbody.appendChild(fragment);
Array.from(table.querySelectorAll("th")).forEach((th, idx) => {
th.classList.remove("sorted-asc", "sorted-desc");
if (idx === col) {
th.classList.add(dir ? "sorted-desc" : "sorted-asc");
}
});
}
function formatDuration(seconds) {
if (seconds <= 0) return "Completed";
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${d > 0 ? d + "d " : ""}${h}h ${m}m ${s}s`;
}
function updateCountdowns() {
const now = Math.floor(Date.now() / 1000);
const cells = document.querySelectorAll(".time-left");
cells.forEach(cell => {
const end = parseInt(cell.dataset.endtime);
if (isNaN(end) || end === 0) return;
const secondsLeft = end - now;
// Update all timers every second
cell.textContent = formatDuration(secondsLeft);
if (secondsLeft <= 3600) {
cell.classList.add("soon");
} else {
cell.classList.remove("soon");
}
});
// Schedule next update using requestAnimationFrame for smooth animation
if (cells.length > 0) {
animationFrameId = requestAnimationFrame(() => {
setTimeout(updateCountdowns, 1000);
});
}
}
// Progressive loading of character data
let remainingCharacters = <?php echo json_encode(
array_slice($character_ids, 1)
); ?>;
let loadTimeout;
function loadNextCharacter() {
// Clear any existing timeout
if (loadTimeout) {
clearTimeout(loadTimeout);
}
// Remove any empty characters from the start
while (remainingCharacters.length > 0 && (!remainingCharacters[0] || remainingCharacters[0] === "")) {
remainingCharacters.shift();
}
if (remainingCharacters.length === 0) {
jobsLoading = false;
jobsTableHead.style.cursor = "pointer";
if (lastSortedCol !== null) {
sortTable('jobsTable', lastSortedCol);
}
return;
}
const charId = remainingCharacters.shift();
const loadingRow = document.getElementById(`loading-${charId}`);
if (!loadingRow) {
setTimeout(loadNextCharacter, 100);
return;
}
// Set a timeout for this character's loading
loadTimeout = setTimeout(() => {
console.log(`Loading timed out for character ${charId}`);
if (loadingRow) {
loadingRow.remove();
}
setTimeout(loadNextCharacter, 100);
}, 10000); // 10 second timeout
fetch(`dashboard.php?ajax=1&character_id=${charId}&t=${Date.now()}`)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
clearTimeout(loadTimeout);
if (data.jobs && data.jobs.length > 0) {
const fragment = document.createDocumentFragment();
data.jobs.forEach(job => {
if (!job || !job.character) return; // Skip invalid jobs
const row = document.createElement('tr');
row.innerHTML = `
<td>${job.character || ''}</td>
<td>${job.blueprint || ''}</td>
<td>${job.activity || ''}</td>
<td>${job.location || ''}</td>
<td>${job.start_time || ''}</td>
<td>${job.end_time || ''}</td>
<td class="time-left" data-endtime="${job.end_time_unix || ''}">${job.time_left || ''}</td>
`;
fragment.appendChild(row);
});
if (loadingRow && loadingRow.parentNode) {
loadingRow.parentNode.replaceChild(fragment, loadingRow);
} else {
const tbody = document.querySelector('#jobsTable tbody');
if (tbody) tbody.appendChild(fragment);
}
} else if (loadingRow) {
loadingRow.innerHTML = '<td colspan="7">No jobs found</td>';
setTimeout(() => loadingRow.remove(), 2000);
}
setTimeout(loadNextCharacter, 100);
})
.catch(error => {
clearTimeout(loadTimeout);
console.error('Error loading character data:', error);
remainingCharacters.shift();
if (loadingRow) {
loadingRow.innerHTML = '<td colspan="7">Error loading data</td>';
setTimeout(() => loadingRow.remove(), 2000);
}
setTimeout(loadNextCharacter, 100);
});
}
// Start loading after a short delay
setTimeout(loadNextCharacter, 500);
// Retry loading a specific character
window.retryLoadCharacter = function(charId) {
const retryRow = document.querySelector(`#jobsTable tbody tr td[colspan="7"]:contains("Error loading data")`).parentNode;
retryRow.innerHTML = `<td colspan="7"><div class="loader"></div> Loading jobs...</td>`;
fetch(`dashboard.php?ajax=1&character_id=${charId}&t=${Date.now()}`)
.then(response => response.json())
.then(data => {
if (data.jobs && data.jobs.length > 0) {
const fragment = document.createDocumentFragment();
data.jobs.forEach(job => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${job.character}</td>
<td>${job.blueprint}</td>
<td>${job.activity}</td>
<td>${job.location}</td>
<td>${job.start_time}</td>
<td>${job.end_time}</td>
<td class="time-left" data-endtime="${job.end_time_unix}">${job.time_left}</td>
`;
fragment.appendChild(row);
});
retryRow.parentNode.replaceChild(fragment, retryRow);
} else {
}
})
.catch(error => {
console.error('Error retrying character data load:', error);
retryRow.innerHTML = `<td colspan="7">Error loading data. <a href="#" onclick="retryLoadCharacter('${charId}'); return false;" style="color: #00aaff;">Retry</a></td>`;
});
};
// Start the update process
updateCountdowns();
// Clean up animation frame when leaving the page
window.addEventListener('beforeunload', () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
});
// Add page reload every 60 minutes for guaranteed fresh data
setInterval(() => {
if (pageVisible) {
console.log("Scheduled page refresh for fresh data");
window.location.reload();
}
}, 60 * 60 * 1000); // 60 minutes
});
</script>
<?php // Increment visit count asynchronously to improve page load time
if (!function_exists("fastcgi_finish_request")) {
// If not using PHP-FPM, just do it inline
$fp = fopen("visits.txt", "c+");
if (flock($fp, LOCK_EX)) {
$count = (int) fread($fp, 100);
rewind($fp);
$count++;
fwrite($fp, $count);
fflush($fp);
flock($fp, LOCK_UN);
}
fclose($fp);
} else {
// Register shutdown function for faster page load
register_shutdown_function(function () {
$fp = fopen("visits.txt", "c+");
if (flock($fp, LOCK_EX)) {
$count = (int) fread($fp, 100);
rewind($fp);
$count++;
fwrite($fp, $count);
fflush($fp);
flock($fp, LOCK_UN);
}
fclose($fp);
});
} ?>
<script>
document.addEventListener("DOMContentLoaded", () => {
function keepSessionAlive() {
// Add cache-busting parameter to prevent browser caching
fetch("keep_alive.php?t=" + Date.now())
.then(response => response.json())
.then(data => {
if (data.status !== "success") {
console.error("Failed to refresh session:", data.message);
// If session keep-alive fails, reload the page
window.location.reload();
} else {
// If tokens were refreshed or cache was cleared, reload the page to get fresh data
if (data.tokens_refreshed || data.cache_cleared) {
console.log("Tokens refreshed or cache cleared, reloading for fresh data");
window.location.reload();
}
}
})
.catch(error => {
console.error("Error in keep-alive request:", error);
// On network errors, try to reload after a delay
setTimeout(() => window.location.reload(), 30000); // 30 seconds
});
}
// Call keepSessionAlive periodically
setInterval(keepSessionAlive, 300000); // Every 5 minutes
// Also call it on user activity after inactivity
let userInactive = false;
let inactivityTimer;
// Set up inactivity detection
const setInactive = () => {
userInactive = true;
inactivityTimer = setTimeout(() => {
// User has been inactive for 10 minutes
console.log("User inactive for 10 minutes");
}, 600000); // 10 minutes
};
const setActive = () => {
// If user was inactive and is now active again
if (userInactive) {
userInactive = false;
clearTimeout(inactivityTimer);
// Call keep-alive to get fresh data
keepSessionAlive();
}
};
// Set inactive after 2 minutes of no activity
setTimeout(setInactive, 120000);
// Reset activity state on user interaction
['mousemove', 'keydown', 'click', 'scroll'].forEach(event => {
document.addEventListener(event, setActive, false);
});
// Initial call to keepSessionAlive
keepSessionAlive();
});
</script>
</body>
</html>