625 lines
26 KiB
PHP
Executable file
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>
|