commit 43988423e00eab1b1687c3a3602f9fb82fa6e698 Author: clay Date: Thu May 22 18:02:27 2025 -0400 Initial commit of EVE Industry Tracker diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b1d311 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Cache and config files +cache/* +config.php +.env +.env.* +.DS_Store +*.cache + +# Logs and databases +*.log +*.sql +*.sqlite +logs/ + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +*.swn +*.bak +*~ + +# Composer dependencies +vendor/ +composer.lock + +# Node modules and logs +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Operating System Files +Thumbs.db +.DS_Store +._* +.Spotlight-V100 +.Trashes + +# Backup files +*.bak +*.backup +*.old +*.tmp + +# Session files +sess_* +sessions/ + +# Local development files +*.local +*.dev +*.development + +# Build files +*.min.css +*.min.js +!assets/*.min.css +!assets/*.min.js \ No newline at end of file diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..90f18de --- /dev/null +++ b/.htaccess @@ -0,0 +1,72 @@ +# Enable URL rewriting +RewriteEngine On + +# Force HTTPS +RewriteCond %{HTTPS} off +RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + +# Remove trailing slashes +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)/$ /$1 [L,R=301] + +# Remove .php extension +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME}.php -f +RewriteRule ^(.*)$ $1.php [L] + +# Browser caching for static resources + + ExpiresActive On + ExpiresByType image/jpg "access plus 1 year" + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType image/gif "access plus 1 year" + ExpiresByType text/css "access plus 1 month" + ExpiresByType application/javascript "access plus 1 month" + + +# Enable compression + + AddOutputFilterByType DEFLATE text/plain + AddOutputFilterByType DEFLATE text/html + AddOutputFilterByType DEFLATE text/xml + AddOutputFilterByType DEFLATE text/css + AddOutputFilterByType DEFLATE application/xml + AddOutputFilterByType DEFLATE application/xhtml+xml + AddOutputFilterByType DEFLATE application/rss+xml + AddOutputFilterByType DEFLATE application/javascript + AddOutputFilterByType DEFLATE application/x-javascript + + +# Security headers + + Header set X-Content-Type-Options "nosniff" + Header set X-XSS-Protection "1; mode=block" + Header set X-Frame-Options "SAMEORIGIN" + Header set Referrer-Policy "strict-origin-when-cross-origin" + + +# Prevent directory listing +Options -Indexes + +# Protect sensitive files + + Order allow,deny + Deny from all + + + + Order allow,deny + Deny from all + + +# Allow access to visits.json specifically + + Order allow,deny + Allow from all + + +# Custom error pages +ErrorDocument 404 /404.php +ErrorDocument 403 /403.php +ErrorDocument 500 /500.php \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100755 index 0000000..e69de29 diff --git a/404.php b/404.php new file mode 100644 index 0000000..3ca901b --- /dev/null +++ b/404.php @@ -0,0 +1,65 @@ + + + + + + + Page Not Found - EVE Industry Tracker + + + + + + + +
+ EVE Jobs Tracker Logo + +
+

404 - Page Not Found

+

Sorry, the page you're looking for doesn't exist.

+ +
+

Looking for something specific? Here are some tips:

+
    +
  • Check that the URL is spelled correctly
  • +
  • Try navigating from our homepage
  • +
  • If you're logged in, try refreshing your session
  • +
+
+ +
+ + + +
+ +
or
+ +
+

πŸ“‚ Import a Saved Session

+
+ + +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9722128 --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# EVE Industry Tracker + +A lightweight PHP application for tracking EVE Online industry jobs across multiple characters in a clean, simple dashboard. No database required, built on ESI (EVE Swagger Interface). + +## Features + +### Core Functionality +- Monitor all industry jobs in real-time +- Multi-character support +- Blueprint inventory tracking +- Auto-refreshing countdown timers +- Session export/import system +- Mobile-responsive design + +### Technical Features +- EVE SSO Authentication +- ESI token management & auto-refresh +- Local caching system +- Session-based storage +- Zero database requirements + +## Requirements + +- PHP 7.4+ +- SSL certificate (for EVE SSO) +- Web server (Apache/Nginx) +- Required PHP extensions: + - curl + - json + - session + +## Installation + +1. Register at EVE Developers Portal + - Create application at https://developers.eveonline.com + - Set callback URL to `https://your-domain/callback.php` + - Required scopes: + - `esi-industry.read_character_jobs.v1` + - `esi-characters.read_blueprints.v1` + +2. Server Setup + ```bash + # Create cache directory + mkdir cache + chmod 755 cache + chown www-data:www-data cache + + # Create config file + cp config.example.php config.php + ``` + +3. Configure Application + ```php + // config.php + define('CLIENT_ID', 'your-eve-client-id'); + define('CLIENT_SECRET', 'your-eve-client-secret'); + define('CALLBACK_URL', 'https://your-domain/callback.php'); + ``` + +4. Web Server Configuration + + Example Nginx configuration: + ```nginx + server { + listen 443 ssl; + server_name your-domain; + root /path/to/eveindy; + index index.php; + + # SSL Configuration + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + # PHP Processing + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php-fpm.sock; + } + + # Static files + location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { + expires 1y; + add_header Cache-Control "public, no-transform"; + } + } + ``` + +## Project Structure + +``` +eveindy/ +β”œβ”€β”€ assets/ # Static files (CSS, images) +β”œβ”€β”€ cache/ # Cache directory +β”œβ”€β”€ config.php # Configuration +β”œβ”€β”€ functions.php # Core functions +β”œβ”€β”€ callback.php # EVE SSO handling +β”œβ”€β”€ dashboard.php # Main dashboard +β”œβ”€β”€ index.php # Entry point +└── session_bootstrap.php +``` + +## Core Functions + +### Authentication +- `refresh_token()`: Token refresh handling +- `is_token_expired()`: Token validation + +### Data Retrieval +- `fetch_character_jobs()`: Get industry jobs +- `fetch_character_blueprints()`: Get blueprints +- `fetch_type_names()`: Get item information +- `fetch_system_names()`: Get location data + +### Caching +- `populate_blueprint_cache()`: Update blueprint cache +- `clean_cache()`: Maintain cache +- `get_cache_data()`: Read cache +- `set_cache_data()`: Write cache + +## Security Features + +- EVE SSO authentication +- Session-based data storage +- No permanent data storage +- Secure cookie handling +- SSL/TLS required + +## Session Management + +- 14-day session lifetime +- Exportable session data +- Cross-device compatibility +- Zero server-side storage + +## Contributing + +1. Fork the repository +2. Create feature branch +3. Commit changes +4. Push to branch +5. Create Pull Request + +## License + +This project is licensed under the MIT License. + +## Credits + +EVE Online and the EVE logo are the registered trademarks of CCP hf. All rights reserved. Used with permission. \ No newline at end of file diff --git a/assets/EIJT-FAVICON.png b/assets/EIJT-FAVICON.png new file mode 100755 index 0000000..82265e3 Binary files /dev/null and b/assets/EIJT-FAVICON.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100755 index 0000000..fa3ccde Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/styles.css b/assets/styles.css new file mode 100755 index 0000000..bbb5a85 --- /dev/null +++ b/assets/styles.css @@ -0,0 +1,378 @@ +/* Base styles */ +body { + font-family: Arial, sans-serif; + background: #1b1b1b; + color: #d4d4d4; + margin: 0; + padding: 0; +} + +/* Loader spinner for dashboard and tables */ +.loader { + border: 4px solid #333 !important; + border-top: 4px solid #00ffcc !important; + border-radius: 50% !important; + width: 20px !important; + height: 20px !important; + animation: spin 1s linear infinite !important; + display: inline-block !important; + margin-left: 10px !important; + vertical-align: middle !important; +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +footer { + text-align: center; + margin-top: 20px; + padding: 10px 0; + font-size: 0.9em; + color: #888; +} + +footer .fa, +footer .fab, +footer .fas { + margin: 0 10px; + color: #00aaff; + transition: + color 0.3s ease, + transform 0.3s ease; +} + +footer .fa:hover, +footer .fab:hover, +footer .fas:hover { + color: #00ccff; + transform: scale(1.2); +} + +a { + color: #00aaff; + text-decoration: none; +} +a:hover { + color: #00ccff; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +button { + background-color: #333; + color: #fff; + padding: 10px 20px; + border: none; + cursor: pointer; + margin: 5px; + border-radius: 5px; + transition: + background-color 0.2s ease, + box-shadow 0.2s ease; +} + +.dashboard-toggle button { + background-color: #333; + color: #fff; + padding: 10px 20px; + border: none; + cursor: pointer; + margin: 5px; + border-radius: 5px; + transition: + background-color 0.2s ease, + box-shadow 0.2s ease; +} + +.dashboard-toggle button.active { + background-color: #00ffcc; + color: #000; +} +button:hover { + background-color: #444; + box-shadow: 0 0 8px rgba(0, 255, 255, 0.1); +} + +/* Job cards */ +.job { + background: #222; + margin: 10px; + padding: 10px; + border-radius: 8px; +} +.job-header { + font-weight: bold; +} +.job-details { + font-size: 0.9em; + color: #aaa; +} + +/* Dashboard Table */ +#jobsTable { + width: 100%; + border-collapse: collapse; + margin-top: 20px; + table-layout: auto; /* Let columns adjust based on content */ +} + +#jobsTable th, +#jobsTable td, +.jobsTable th, +.jobsTable td { + padding: 10px; + text-align: left; + border-bottom: 1px solid #333; + white-space: nowrap; /* Prevent text wrapping */ + overflow: hidden; /* Ensure text stays within the cell */ + text-overflow: ellipsis; /* Shorten overflow text with "..." */ + min-width: 100px; /* Ensure minimum width for columns */ +} + +#jobsTable th, +.jobsTable th { + background-color: #2a2a2a; + color: #00ffcc; + cursor: pointer; + position: sticky; + top: 0; + z-index: 1; +} + +#jobsTable tr:nth-child(even), +.jobsTable tr:nth-child(even) { + background-color: #1e1e1e; +} + +#jobsTable tr:hover, +.jobsTable tr:hover { + background-color: #2c2c2c; +} + +/* Top Button Row */ +.buttons { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; + position: relative; +} + +/* Import Form Styling */ +.import-form { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 10px; +} + +.import-form input[type="file"] { + margin-bottom: 10px; + padding: 8px; + background-color: #333; + color: #ddd; + border: 1px solid #444; + border-radius: 5px; +} + +.import-form button { + background-color: #00ff9d; + color: #000; + padding: 10px 20px; + border: none; + border-radius: 5px; + font-weight: bold; + transition: background-color 0.2s ease; +} +.import-form button:hover { + background-color: #00b379; +} + +/* Section Block Styles (Login/Import panels) */ +.section { + max-width: 500px; + margin: 2em auto; + padding: 2em; + border: 1px solid #2a2a2a; + border-radius: 10px; + background: linear-gradient(145deg, #1e1e1e, #2a2a2a); + box-shadow: + 0 0 20px rgba(0, 255, 200, 0.08), + 0 0 40px rgba(0, 150, 255, 0.05); + transition: + background 0.3s ease, + box-shadow 0.3s ease; +} +.section:hover { + background: linear-gradient(145deg, #222, #333); + box-shadow: 0 0 25px rgba(0, 200, 255, 0.15); +} + +/* Divider Styling */ +.divider { + margin: 3em auto; + text-align: center; + position: relative; +} +.divider::before, +.divider::after { + content: ""; + display: inline-block; + width: 30%; + height: 1px; + background: #444; + vertical-align: middle; + margin: 0 1em; +} +.divider span { + color: #888; + font-size: 0.8em; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Button flash animation */ +@keyframes flashButton { + 0%, + 100% { + background-color: #333; + } + 50% { + background-color: #00ffcc; + color: #000; + } +} + +.export-flash { + animation: flashButton 0.8s ease-in-out 3; +} + +/* Responsive tweaks */ + +.no-jobs-message { + color: #d4d4d4; + background-color: #222; + border-left: 3px solid #00ffcc; + padding: 10px; + margin: 20px 0; + font-style: italic; + text-align: center; +} +@media (max-width: 768px) { + .buttons { + flex-direction: column; + align-items: flex-start; + } + + #jobsTable th, + #jobsTable td { + font-size: 0.9em; + } +} + +th.sorted-asc::after { + content: " β–²"; +} +th.sorted-desc::after { + content: " β–Ό"; +} +.soon { + background-color: #1e2b38 !important; /* Darker blue-gray for dark theme */ + color: #00ffcc !important; /* Make text pop */ + font-weight: bold !important; + border-left: 2px solid #00ffcc !important; /* Accent highlight */ +} +.character-list { + margin-top: 20px; +} +.character-list ul { + list-style-type: none; + padding: 0; +} +.character-list li { + font-size: 1.1em; + margin: 5px 0; +} + +/* Stats Table Styles */ +.jobsTable { + width: 100%; + margin-top: 20px; + border-collapse: collapse; +} + +.jobsTable th, +.jobsTable td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #444; +} + +.jobsTable th { + background-color: #2a2a2a; + color: #00ffcc; + cursor: pointer; + position: relative; +} + +.jobsTable th.sorted-asc::after { + content: " β–²"; +} + +.jobsTable th.sorted-desc::after { + content: " β–Ό"; +} + +.jobsTable tr:nth-child(even) { + background-color: #1e1e1e; +} + +.jobsTable tr:hover { + background-color: #2c2c2c; +} + +/* Responsive tables */ +@media (max-width: 768px) { + .jobsTable, + .jobsTable thead, + .jobsTable tbody, + .jobsTable th, + .jobsTable td, + .jobsTable tr { + display: block; + } + + .jobsTable thead tr { + position: absolute; + top: -9999px; + left: -9999px; + } + + .jobsTable tr { + border: 1px solid #444; + margin-bottom: 1em; + } + + .jobsTable td { + border: none; + position: relative; + padding-left: 50%; + } + + .jobsTable td:before { + position: absolute; + left: 6px; + width: 45%; + white-space: nowrap; + font-weight: bold; + } +} diff --git a/assets/styles.min.css b/assets/styles.min.css new file mode 100644 index 0000000..747d6fb --- /dev/null +++ b/assets/styles.min.css @@ -0,0 +1 @@ +body{font-family:Arial,sans-serif;background:#1b1b1b;color:#d4d4d4;margin:0;padding:0}@keyframes flashButton{0%,100%{background-color:#333}50%{background-color:#00ffcc;color:#000}}.export-flash{animation:flashButton .8s ease-in-out 3}.no-jobs-message{color:#00ffcc;background-color:#444;padding:10px;margin:20px 0;text-align:center;border-radius:5px;font-style:italic}footer{text-align:center;margin-top:20px;padding:10px 0;font-size:.9em;color:#888}footer .fa,footer .fab,footer .fas{margin:0 10px;color:#00aaff;transition:color .3s ease,transform .3s ease}footer .fa:hover,footer .fab:hover,footer .fas:hover{color:#00ccff;transform:scale(1.2)}a{color:#00aaff;text-decoration:none}a:hover{color:#00ccff}.container{max-width:1200px;margin:0 auto;padding:20px}button{background-color:#333;color:#fff;padding:10px 20px;border:none;cursor:pointer;margin:5px;border-radius:5px;transition:background-color .2s ease,box-shadow .2s ease}.dashboard-toggle button{background-color:#333;color:#fff;padding:10px 20px;border:none;cursor:pointer;margin:5px;border-radius:5px;transition:background-color .2s ease,box-shadow .2s ease}.dashboard-toggle button.active{background-color:#00ffcc;color:#000}button:hover{background-color:#444;box-shadow:0 0 8px rgba(0,255,255,.1)}.job{background:#222;margin:10px;padding:10px;border-radius:8px}.job-header{font-weight:700}.job-details{font-size:.9em;color:#aaa}#jobsTable,.jobsTable{width:100%;border-collapse:collapse;margin-top:20px;table-layout:auto}#jobsTable td,#jobsTable th,.jobsTable td,.jobsTable th{padding:10px;text-align:left;border-bottom:1px solid #333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:100px}#jobsTable th,.jobsTable th{background-color:#2a2a2a;color:#00ffcc;cursor:pointer;position:sticky;top:0;z-index:1}#jobsTable tr:nth-child(even),.jobsTable tr:nth-child(even){background-color:#1e1e1e}#jobsTable tr:hover,.jobsTable tr:hover{background-color:#2c2c2c}.buttons{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:20px;position:relative}.import-form{display:flex;flex-direction:column;align-items:center;margin-top:10px}.import-form input[type=file]{margin-bottom:10px;padding:8px;background-color:#333;color:#ddd;border:1px solid #444;border-radius:5px}.import-form button{background-color:#00ff9d;color:#000;padding:10px 20px;border:none;border-radius:5px;font-weight:700;transition:background-color .2s ease}.import-form button:hover{background-color:#00b379}.section{max-width:500px;margin:2em auto;padding:2em;border:1px solid #2a2a2a;border-radius:10px;background:linear-gradient(145deg,#1e1e1e,#2a2a2a);box-shadow:0 0 20px rgba(0,255,200,.08),0 0 40px rgba(0,150,255,.05);transition:background .3s ease,box-shadow .3s ease}.section:hover{background:linear-gradient(145deg,#222,#333);box-shadow:0 0 25px rgba(0,200,255,.15)}.divider{margin:3em auto;text-align:center;position:relative}.divider::after,.divider::before{content:"";display:inline-block;width:30%;height:1px;background:#444;vertical-align:middle;margin:0 1em}.divider span{color:#888;font-size:.8em;text-transform:uppercase;letter-spacing:1px}@media (max-width:768px){.buttons{flex-direction:column;align-items:flex-start}#jobsTable td,#jobsTable th{font-size:.9em}.jobsTable,.jobsTable tbody,.jobsTable td,.jobsTable th,.jobsTable thead,.jobsTable tr{display:block}.jobsTable thead tr{position:absolute;top:-9999px;left:-9999px}.jobsTable tr{border:1px solid #444;margin-bottom:1em}.jobsTable td{border:none;position:relative;padding-left:50%}.jobsTable td:before{position:absolute;left:6px;width:45%;white-space:nowrap;font-weight:700}}th.sorted-asc::after{content:" β–²"}th.sorted-desc::after{content:" β–Ό"}.soon{background-color:#1e2b38!important;color:#00ffcc!important;font-weight:700!important;border-left:2px solid #00ffcc!important}.character-list{margin-top:20px}.character-list ul{list-style-type:none;padding:0}.character-list li{font-size:1.1em;margin:5px 0}.jobsTable th.sorted-asc::after{content:" β–²"}.jobsTable th.sorted-desc::after{content:" β–Ό"} diff --git a/assets/table-sort.js b/assets/table-sort.js new file mode 100644 index 0000000..4121a4f --- /dev/null +++ b/assets/table-sort.js @@ -0,0 +1,56 @@ +let sortDirection = {}; + +function sortTable(tableId, col) { + const table = document.getElementById(tableId); + if (!table) return; + + // Initialize this table's sort direction if not set + if (!sortDirection[tableId]) { + sortDirection[tableId] = {}; + } + + const dir = sortDirection[tableId][col] = !sortDirection[tableId][col]; + const rows = Array.from(table.tBodies[0].rows); + + rows.sort((a, b) => { + const aCell = a.cells[col]; + const bCell = b.cells[col]; + + if (col === 1) { // If sorting by visit count (numeric column) + const aValue = parseInt(aCell.innerText.trim()) || 0; + const bValue = parseInt(bCell.innerText.trim()) || 0; + return dir ? bValue - aValue : aValue - bValue; + } else { // Sorting by text (browser/os/other columns) + const aText = aCell.innerText.trim(); + const bText = bCell.innerText.trim(); + return dir ? bText.localeCompare(aText) : aText.localeCompare(bText); + } + }); + + // Re-attach the sorted rows + rows.forEach(row => table.tBodies[0].appendChild(row)); + + // Update sorting indicators + 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'); + } + }); +} + +// Initialize table sorting when the document is loaded +document.addEventListener('DOMContentLoaded', function() { + // Apply sorting to tables with sortable class + const tables = document.querySelectorAll('.jobsTable'); + tables.forEach(table => { + const tableId = table.id; + const headers = table.querySelectorAll('th'); + + headers.forEach((header, idx) => { + header.addEventListener('click', function() { + sortTable(tableId, idx); + }); + }); + }); +}); \ No newline at end of file diff --git a/blueprints_dashboard.php b/blueprints_dashboard.php new file mode 100755 index 0000000..59ad775 --- /dev/null +++ b/blueprints_dashboard.php @@ -0,0 +1,286 @@ + 300) { + foreach ($_SESSION["characters"] as $character_id => &$charData) { + if (isset($charData["refresh_token"])) { + if ( + !isset($charData["access_token"]) || + is_token_expired($charData["access_token"]) + ) { + $new_tokens = refresh_token($charData["refresh_token"]); + if (!empty($new_tokens["access_token"])) { + $charData["access_token"] = $new_tokens["access_token"]; + } else { + error_log( + "Failed to refresh token for character ID $character_id" + ); + } + } + } + } + unset($charData); + $_SESSION['last_token_refresh'] = $page_load_time; + } +} + +if (!isset($_SESSION["characters"]) || empty($_SESSION["characters"])) { + echo "No characters logged in. Go back"; + exit(); +} + +// Check if we have a cached blueprint list +$cache_key = "all_character_blueprints"; +$cached_data = get_cache_data($cache_key); + +if ($cached_data !== null) { + $blueprints = $cached_data; +} else { + // Fetch blueprints for all characters + $blueprints = []; + $blueprint_ids = []; + + foreach ($_SESSION["characters"] as $character_id => $charData) { + $access_token = $charData["access_token"] ?? null; + if (!$access_token) { + continue; + } + + $character_blueprints = fetch_character_blueprints( + $character_id, + $access_token + ); + foreach ($character_blueprints as $bp) { + $key = + $bp["blueprint_type_id"] . + "-" . + $bp["material_efficiency"] . + "-" . + $bp["time_efficiency"] . + "-" . + $bp["runs"]; + $blueprints[$key] = $bp; + $blueprint_ids[] = $bp["blueprint_type_id"]; + } + } + + $blueprint_names = fetch_type_names(array_unique($blueprint_ids)); + foreach ($blueprints as &$bp) { + $bp["blueprint_name"] = + $blueprint_names[$bp["blueprint_type_id"]] ?? "Unknown Blueprint"; + } + unset($bp); + + // Cache the results for 10 minutes + set_cache_data($cache_key, $blueprints, 600); +} +?> + + + + + Blueprints + + + +

Blueprints

+ + + + + + + + + + + + + + + + + + + + + + + +
Blueprint NameMaterial EfficiencyTime EfficiencyNumber of RunsRuns Remaining
%%
+ + + + diff --git a/callback.php b/callback.php new file mode 100755 index 0000000..7100400 --- /dev/null +++ b/callback.php @@ -0,0 +1,82 @@ + "authorization_code", + "code" => $code, + "redirect_uri" => $redirect_uri, + ]) +); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Basic " . base64_encode($client_id . ":" . $client_secret), + "Content-Type: application/x-www-form-urlencoded", +]); + +$response = curl_exec($ch); +$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +$token_data = json_decode($response, true); + +// Fail gracefully if token fetch fails +if (!is_array($token_data) || !isset($token_data["access_token"])) { + error_log("Token exchange failed: $response"); + exit("Failed to retrieve access token"); +} + +// Use access token to get character info +$ch = curl_init("https://login.eveonline.com/oauth/verify"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Bearer " . $token_data["access_token"], +]); +$user_data = json_decode(curl_exec($ch), true); +curl_close($ch); + +// Validate user info +if (!is_array($user_data) || !isset($user_data["CharacterID"])) { + error_log("Failed to verify character: " . json_encode($user_data)); + exit("Character verification failed"); +} + +$character_id = $user_data["CharacterID"]; +$character_name = $user_data["CharacterName"]; + +// Save access & refresh tokens in session +if (!isset($_SESSION["characters"][$character_id])) { + $_SESSION["characters"][$character_id] = [ + "name" => $character_name, + "access_token" => $token_data["access_token"], + "refresh_token" => $token_data["refresh_token"] ?? null, + ]; +} else { + error_log("Character data already exists for ID: $character_id. Skipping overwrite."); +} + +// Redirect back to main page +header("Location: index.php"); +exit(); diff --git a/dashboard.php b/dashboard.php new file mode 100755 index 0000000..959b9d3 --- /dev/null +++ b/dashboard.php @@ -0,0 +1,625 @@ + 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. Go back"; + 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(); +} +?> + + + + + + + Industry Jobs Dashboard + + + +

Industry Jobs

+ + + + + + + + + + + + + + +
CharacterBlueprintActivityLocationStart TimeEnd TimeTime Left
No jobs found for " . htmlspecialchars($charData["name"]) . "
"; + } 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 ""; + echo "" . + htmlspecialchars($job["character"]) . + ""; + echo "" . + htmlspecialchars($job["blueprint"]) . + ""; + echo "" . + htmlspecialchars($job["activity"]) . + ""; + echo "" . + htmlspecialchars($job["location"]) . + ""; + echo "" . + htmlspecialchars($job["start_time"]) . + ""; + echo "" . + htmlspecialchars($job["end_time"]) . + ""; + echo "" . + $time_left_display . + ""; + echo ""; + } + } + } + + // 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 ""; + echo "Loading jobs for " . + htmlspecialchars($char_name) . + "
"; + echo ""; + } + } + } ?> + + + +
+

Logged-in Characters

+ + + + + + + + + $charData + ): ?> + + + + + + +
NameCharacter ID
+
+ + + + + + + + + + + + + + + + diff --git a/eveindy.claytonia.net b/eveindy.claytonia.net new file mode 100644 index 0000000..21f0583 --- /dev/null +++ b/eveindy.claytonia.net @@ -0,0 +1,97 @@ +server { + server_name eveindy.claytonia.net; + root /home/eveindy; + index index.php index.html index.htm; + + # Compression + gzip on; + gzip_vary on; + gzip_min_length 1000; + gzip_comp_level 6; + gzip_types text/plain text/css application/javascript image/x-icon application/json + text/xml application/xml application/xml+rss text/javascript; + + # Logs + access_log /var/log/nginx/eveindy_access.log; + error_log /var/log/nginx/eveindy_error.log; + + # Error pages + error_page 404 /404.php; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Allow visits.json (must be before the json block) + location = /visits.json { + allow all; + } + + # Static files with caching + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ { + expires 1y; + add_header Cache-Control "public, no-transform"; + access_log off; + try_files $uri =404; + } + + # PHP handling + location ~ \.php$ { + try_files $uri =404; + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + + # Preload critical assets + add_header Link "; rel=preload; as=style" always; + add_header Link "; rel=preload; as=image" always; + } + + # Block sensitive files + location ~ \.(json|txt|log|cache)$ { + deny all; + access_log off; + } + + # Protect dot files + location ~ /\. { + deny all; + access_log off; + } + + # Default location + location / { + try_files $uri $uri/ $uri.php =404; + } + + # Remove www + if ($host ~* ^www\.(.*)) { + return 301 https://$1$request_uri; + } + + # Remove trailing slashes + rewrite ^/(.*)/$ /$1 permanent; + + # SSL Configuration + listen 443 ssl; + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # SSL optimizations + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_prefer_server_ciphers on; +} + +# HTTP to HTTPS redirect +server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; +} \ No newline at end of file diff --git a/export.php b/export.php new file mode 100755 index 0000000..ac07553 --- /dev/null +++ b/export.php @@ -0,0 +1,39 @@ + "No session data available to export"]); + exit(); +} + +// Clean up the session data before export +if (isset($_SESSION["characters"])) { + // Remove any entries with empty character IDs + if (isset($_SESSION["characters"][""])) { + unset($_SESSION["characters"][""]); + } + + // Create a clean copy for export (don't modify the actual session) + $export_data = $_SESSION; +} else { + $export_data = $_SESSION; +} + +// Generate descriptive filename with timestamp for user download +$date_time = date("Y-m-d"); +$download_filename = "eve_indy_job_tracker_backup_{$date_time}.json"; + +// Set headers for download +header("Content-Type: application/json"); +header('Content-Disposition: attachment; filename="' . $download_filename . '"'); +header("Cache-Control: no-store, no-cache, must-revalidate"); +header("Pragma: no-cache"); + +// Output session data as pretty-printed JSON +echo json_encode($export_data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); +error_log("Export completed successfully for session download."); +exit(); +?> \ No newline at end of file diff --git a/functions.php b/functions.php new file mode 100755 index 0000000..d873224 --- /dev/null +++ b/functions.php @@ -0,0 +1,795 @@ + "Manufacturing", + 3 => "TE Research", + 4 => "ME Research", + 5 => "Copying", + 7 => "Reverse Engineering", + 8 => "Invention", + 9 => "Reaction", + 11 => "Reaction", + ]; + + // Process blueprint and system IDs in smaller batches for better performance + if (!empty($blueprint_ids)) { + $blueprint_names = fetch_type_names($blueprint_ids); + } + + if (!empty($system_ids)) { + $system_names = fetch_system_names($system_ids); + } + + $results = []; + $current_time = time(); + + foreach ($all_jobs as $job) { + $start_time = format_time($job["start_date"]); + $end_time = isset($job["end_date"]) + ? format_time($job["end_date"]) + : ""; + + $end_timestamp = isset($job["end_date"]) + ? strtotime($job["end_date"]) + : null; + + $time_left = + $end_timestamp && $job["status"] !== "delivered" + ? max(0, $end_timestamp - $current_time) + : "Completed"; + + $system_name = $system_names[$job["system_id"]] ?? "Private Structure"; + $blueprint_name = + $blueprint_names[$job["blueprint_type_id"]] ?? + $job["blueprint_type_id"]; + + $results[] = [ + "character" => + $character_name . + (isset($job["installer_id"]) && + $job["installer_id"] != $character_id + ? " (Corp)" + : ""), + "blueprint" => $blueprint_name, + "activity" => + $activity_map[$job["activity_id"]] ?? + "Activity {$job["activity_id"]}", + "status" => $job["status"], + "location" => $system_name, + "start_time" => $start_time, + "end_time" => $end_time, + "end_time_unix" => $end_timestamp, + "time_left" => is_numeric($time_left) + ? gmdate("H:i:s", $time_left) + : $time_left, + ]; + } + + // Cache results with dynamic TTL based on the nearest job completion + $min_time_left = PHP_INT_MAX; + foreach ($results as $job) { + if (is_numeric($job["end_time_unix"]) && $job["end_time_unix"] > $current_time) { + $time_until_completion = $job["end_time_unix"] - $current_time; + if ($time_until_completion < $min_time_left) { + $min_time_left = $time_until_completion; + } + } + } + + // Cache until the next job completes (min 60 seconds, max 300 seconds) + $cache_ttl = ($min_time_left < PHP_INT_MAX) + ? min(max(60, $min_time_left), 300) + : 300; + + set_cache_data($cache_key, $results, $cache_ttl); + + return $results; +} + +function fetch_corporation_jobs($corporation_id, $access_token) +{ + // Check cache first + $cache_key = "corporation_jobs_{$corporation_id}"; + $cache = get_cache_data($cache_key); + + if ($cache !== null) { + return $cache; + } + + $esi_url = "https://esi.evetech.net/latest/corporations/{$corporation_id}/industry/jobs/?include_completed=false"; + $headers = [ + "Authorization: Bearer $access_token", + "Accept: application/json", + ]; + + $response = esi_call($esi_url, $headers); + + if ($response["http_code"] !== 200) { + error_log("Corp job fetch failed: " . $response["body"]); + return []; + } + + $jobs = json_decode($response["body"], true); + $results = is_array($jobs) ? $jobs : []; + + // Cache the results for 5 minutes + set_cache_data($cache_key, $results, 300); + + return $results; +} + +function fetch_character_info($character_id, $access_token) +{ + // Check cache first + $cache_key = "character_info_{$character_id}"; + $cache = get_cache_data($cache_key); + + if ($cache !== null) { + return $cache; + } + + $url = "https://esi.evetech.net/latest/characters/{$character_id}/"; + $headers = [ + "Authorization: Bearer $access_token", + "Accept: application/json", + ]; + + $response = esi_call($url, $headers); + $info = json_decode($response["body"], true); + + if (is_array($info)) { + // Cache character info for 1 hour (rarely changes) + set_cache_data($cache_key, $info, 3600); + } + + return $info; +} + +function format_time($iso_string) +{ + static $cache = []; + + // Use cached result if available + if (isset($cache[$iso_string])) { + return $cache[$iso_string]; + } + + $dt = DateTime::createFromFormat(DateTime::ATOM, $iso_string); + $result = $dt ? $dt->format("M j, Y H:i") : $iso_string; + + // Cache the result + $cache[$iso_string] = $result; + + return $result; +} + +function fetch_type_names($type_ids) { + if (empty($type_ids)) { + return []; + } + + // Check if cleanup is needed + $last_cleanup = get_last_cleanup_time(); + $thirty_days_ago = strtotime("-30 days"); + if (!$last_cleanup || strtotime($last_cleanup) < $thirty_days_ago) { + // Call populate_cache.php to refresh the cache + $populate_cache_script = __DIR__ . "/populate_cache.php"; + if (file_exists($populate_cache_script)) { + exec("php " . escapeshellarg($populate_cache_script)); + } + update_last_cleanup_time(); // Update the last cleanup timestamp + } + + // Load cache + $cache_file = __DIR__ . "/cache/blueprint_cache.json"; + $cache = file_exists($cache_file) ? json_decode(file_get_contents($cache_file), true) : []; + + // Check cache first + $cached_names = []; + $ids_to_fetch = []; + foreach ($type_ids as $id) { + if (isset($cache[$id])) { + $cached_names[$id] = $cache[$id]; + } else { + $ids_to_fetch[] = $id; + } + } + + // Fetch missing IDs from API + if (!empty($ids_to_fetch)) { + $url = "https://esi.evetech.net/latest/universe/names/"; + $chunks = array_chunk($ids_to_fetch, 1000); // Split into chunks of 1000 IDs + foreach ($chunks as $chunk) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array_values($chunk))); + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($http_code === 200) { + $data = json_decode($response, true); + foreach ($data as $entry) { + if (isset($entry["id"], $entry["name"])) { + $cached_names[$entry["id"]] = $entry["name"]; + $cache[$entry["id"]] = $entry["name"]; // Update cache + } + } + } else { + error_log("Failed to fetch type names. HTTP Code: $http_code"); + } + } + } + + // Save updated cache + file_put_contents($cache_file, json_encode($cache, JSON_PRETTY_PRINT)); + + return $cached_names; +} + +function populate_blueprint_cache() { + echo "Fetching all type IDs...\n"; + $all_type_ids = get_all_type_ids(); + + if (empty($all_type_ids)) { + echo "No type IDs retrieved.\n"; + return; + } + + echo "Filtering blueprint IDs...\n"; + $blueprint_ids = filter_blueprint_ids($all_type_ids); + + if (empty($blueprint_ids)) { + echo "No blueprint IDs found.\n"; + return; + } + + echo "Fetching blueprint names...\n"; + $cached_data = []; + $chunks = array_chunk($blueprint_ids, 1000); + foreach ($chunks as $chunk) { + $batch_names = fetch_type_names($chunk); + foreach ($batch_names as $id => $name) { + $cached_data[$id] = $name; + } + usleep(250000); + } + + // Save to JSON cache file + $cache_dir = __DIR__ . "/cache"; + if (!is_dir($cache_dir)) { + mkdir($cache_dir, 0775, true); + } + $cache_file = $cache_dir . "/blueprint_cache.json"; + file_put_contents( + $cache_file, + json_encode($cached_data, JSON_PRETTY_PRINT) + ); + + echo "Cache populated with " . count($cached_data) . " blueprint names.\n"; +} + +function get_all_type_ids() { + $all_ids = []; + $page = 1; + + do { + $url = "https://esi.evetech.net/latest/universe/types/?page=$page"; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($http_code !== 200) { + echo "Failed to fetch type IDs on page $page. HTTP Code: $http_code\n"; + break; + } + + $ids = json_decode($response, true); + if (empty($ids)) { + break; + } + + $all_ids = array_merge($all_ids, $ids); + $page++; + usleep(250000); // Avoid API rate limit + } while (true); + + return $all_ids; +} + +function filter_blueprint_ids($ids) { + $blueprint_ids = []; + $chunks = array_chunk($ids, 1000); + + foreach ($chunks as $chunk) { + $names = fetch_type_names($chunk); + foreach ($names as $id => $name) { + if (str_ends_with($name, "Blueprint")) { + $blueprint_ids[] = $id; + } + } + usleep(250000); + } + + return $blueprint_ids; +} + +function fetch_system_names($system_ids) +{ + if (empty($system_ids)) { + return []; + } + + // Generate a cache key based on the sorted system IDs + sort($system_ids); + $cache_key = "system_names_" . md5(json_encode($system_ids)); + $cache = get_cache_data($cache_key); + + if ($cache !== null) { + return $cache; + } + + // Check if we already have some of these system names cached individually + $names = []; + $ids_to_fetch = []; + + foreach ($system_ids as $id) { + $individual_cache_key = "system_name_$id"; + $cached_name = get_cache_data($individual_cache_key); + + if ($cached_name !== null) { + $names[$id] = $cached_name; + } else { + $ids_to_fetch[] = $id; + } + } + + // If we have all system names from individual caches, return them + if (empty($ids_to_fetch)) { + // Still cache the combined result + set_cache_data($cache_key, $names, 86400); + return $names; + } + + // Only fetch the IDs we don't already have cached + $url = "https://esi.evetech.net/latest/universe/names/"; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]); + curl_setopt( + $ch, + CURLOPT_POSTFIELDS, + json_encode(array_values($ids_to_fetch)) + ); + $response = curl_exec($ch); + curl_close($ch); + + $data = json_decode($response, true); + + if (is_array($data)) { + foreach ($data as $entry) { + if (isset($entry["id"]) && isset($entry["name"])) { + $names[$entry["id"]] = $entry["name"]; + + // Cache individual system names + $individual_cache_key = "system_name_{$entry["id"]}"; + set_cache_data($individual_cache_key, $entry["name"], 86400 * 7); // Cache for a week + } + } + } + + // Cache system names for 24 hours (they don't change) + set_cache_data($cache_key, $names, 86400); + + return $names; +} + +function refresh_token($refresh_token) +{ + // Initialize cURL + $ch = curl_init("https://login.eveonline.com/v2/oauth/token"); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: Basic " . base64_encode(ESI_CLIENT_ID . ":" . ESI_CLIENT_SECRET), + "Content-Type: application/x-www-form-urlencoded", + ]); + curl_setopt( + $ch, + CURLOPT_POSTFIELDS, + http_build_query([ + "grant_type" => "refresh_token", + "refresh_token" => $refresh_token, + ]) + ); + + // Execute the request + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Get HTTP status code + $curl_error = curl_error($ch); // Capture cURL error if any + curl_close($ch); + + // Handle cURL errors + if ($response === false) { + error_log("cURL error: $curl_error"); + return []; + } + + // Handle HTTP errors + if ($http_code !== 200) { + error_log("ERROR: Token refresh failed. HTTP $http_code. Response: $response"); + + // Specific handling for common HTTP errors + if ($http_code === 400) { + error_log("Bad Request: Check the refresh token or request parameters."); + } elseif ($http_code === 401) { + error_log("Unauthorized: Check the client ID and secret."); + } elseif ($http_code === 429) { + error_log("Rate limit exceeded. Retry after delay."); + } elseif ($http_code >= 500) { + error_log("Server error. Retry after a delay."); + } + + return []; + } + + // Decode the response + $data = json_decode($response, true); + + // Validate the response + if (!isset($data["access_token"])) { + error_log("ERROR: Token refresh failed. Invalid response: $response"); + return []; + } + + // Save the new refresh token if provided + if (isset($data["refresh_token"])) { + $_SESSION["characters"][$character_id]["refresh_token"] = $data["refresh_token"]; + } + + return $data; +} + +function esi_call($url, $headers) +{ + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_HEADER, true); + $response = curl_exec($ch); + $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $body = substr($response, $header_size); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return [ + "http_code" => $http_code, + "body" => $body, + ]; +} + +function is_token_expired($access_token, $buffer_time = 300) +{ + // Split the token into its three parts (header, payload, signature) + $parts = explode(".", $access_token); + + // Validate that the token has exactly three parts + if (count($parts) !== 3) { + error_log("Invalid JWT format: $access_token"); + return true; // Assume expired if the format is invalid + } + + // Decode the payload (second part of the JWT) + $payload = json_decode(base64_decode($parts[1]), true); + + // Validate that the payload is a valid JSON object + if (!is_array($payload)) { + error_log("Malformed JWT payload: " . base64_decode($parts[1])); + return true; // Assume expired if the payload is malformed + } + + // Check if the "exp" (expiration) claim exists + if (!isset($payload["exp"])) { + error_log("JWT missing 'exp' claim: " . json_encode($payload)); + return true; // Assume expired if the expiration claim is missing + } + + // Compare the expiration time with the current time, considering the buffer time + // This prevents tokens from expiring during a user session by refreshing them early + return $payload["exp"] < (time() + $buffer_time); +} +function fetch_character_blueprints($character_id, $access_token) +{ + // Check cache first + $cache_key = "character_blueprints_{$character_id}"; + $cache = get_cache_data($cache_key); + + if ($cache !== null) { + return $cache; + } + + $esi_url = "https://esi.evetech.net/latest/characters/{$character_id}/blueprints/"; + $headers = [ + "Authorization: Bearer $access_token", + "Accept: application/json", + ]; + + $response = esi_call($esi_url, $headers); + + if ($response["http_code"] !== 200) { + error_log( + "Blueprint fetch failed for character ID $character_id: " . + $response["body"] + ); + return []; + } + + $blueprints = json_decode($response["body"], true); + $results = []; + + foreach ($blueprints as $bp) { + $results[] = [ + "blueprint_type_id" => $bp["type_id"], + "blueprint_name" => $bp["type_id"], // Placeholder, replace with actual name lookup if needed + "material_efficiency" => $bp["material_efficiency"], + "time_efficiency" => $bp["time_efficiency"], + "runs" => $bp["runs"], + "quantity" => $bp["quantity"] ?? 1, + ]; + } + + // Cache the results for 10 minutes (blueprints change less frequently) + set_cache_data($cache_key, $results, 600); + + return $results; +} +function get_cached_name($id) { + $cache_file = __DIR__ . "/cache/blueprint_cache.json"; + if (!file_exists($cache_file)) { + return null; + } + + $cache = json_decode(file_get_contents($cache_file), true); + return $cache[$id]["name"] ?? null; +} + +function cache_name($id, $name) { + $cache_file = __DIR__ . "/cache/blueprint_cache.json"; + $cache = file_exists($cache_file) ? json_decode(file_get_contents($cache_file), true) : []; + $cache[$id] = [ + "name" => $name, + "last_updated" => gmdate("Y-m-d\TH:i:s\Z") + ]; + file_put_contents($cache_file, json_encode($cache, JSON_PRETTY_PRINT)); +} + +function clean_cache($expiry_days = 30) { + $cache_file = __DIR__ . "/cache/blueprint_cache.json"; + if (!file_exists($cache_file)) { + return; + } + + $cache = json_decode(file_get_contents($cache_file), true); + $expiry_time = strtotime("-$expiry_days days"); + + foreach ($cache as $id => $entry) { + if (strtotime($entry["last_updated"]) < $expiry_time) { + unset($cache[$id]); + } + } + + file_put_contents($cache_file, json_encode($cache, JSON_PRETTY_PRINT)); +} +function get_last_cleanup_time() { + $file = __DIR__ . "/cache/last_cleanup.json"; + if (!file_exists($file)) { + return null; + } + + $data = json_decode(file_get_contents($file), true); + return $data["last_cleanup"] ?? null; +} + +function update_last_cleanup_time() { + $file = __DIR__ . "/cache/last_cleanup.json"; + $data = ["last_cleanup" => gmdate("Y-m-d\TH:i:s\Z")]; + file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); +} +// Cache functions +function get_cache_data($key) { + static $cache_data = null; + static $loaded = false; + + $cache_dir = __DIR__ . "/cache"; + $cache_file = $cache_dir . "/api_cache.json"; + + // Load cache data only once per request + if (!$loaded) { + if (file_exists($cache_file)) { + $cache_data = json_decode(file_get_contents($cache_file), true) ?: []; + } else { + $cache_data = []; + } + $loaded = true; + } + + if (!isset($cache_data[$key]) || $cache_data[$key]['expires'] < time()) { + return null; + } + + return $cache_data[$key]['data']; +} + +function set_cache_data($key, $data, $ttl = 300) { + static $cache_data = null; + static $cache_modified = false; + static $loaded = false; + + $cache_dir = __DIR__ . "/cache"; + + if (!is_dir($cache_dir)) { + mkdir($cache_dir, 0755, true); + } + + $cache_file = $cache_dir . "/api_cache.json"; + + // Load cache data only once per request + if (!$loaded) { + if (file_exists($cache_file)) { + $cache_data = json_decode(file_get_contents($cache_file), true) ?: []; + } else { + $cache_data = []; + } + $loaded = true; + } + + if (!is_array($cache_data)) { + $cache_data = []; + } + + // Clean expired cache entries (only occasionally to improve performance) + if (rand(1, 10) === 1) { + foreach ($cache_data as $cache_key => $cache_entry) { + if ($cache_entry['expires'] < time()) { + unset($cache_data[$cache_key]); + $cache_modified = true; + } + } + } + + // Update cache with new data + $cache_data[$key] = [ + 'data' => $data, + 'expires' => time() + $ttl + ]; + $cache_modified = true; + + // Use register_shutdown_function to write cache only once at the end of the request + if ($cache_modified && !isset($GLOBALS['cache_shutdown_registered'])) { + $GLOBALS['cache_shutdown_registered'] = true; + register_shutdown_function(function() use ($cache_file) { + global $cache_data; + if (is_array($cache_data)) { + file_put_contents($cache_file, json_encode($cache_data)); + } + }); + } +} +?> diff --git a/import.php b/import.php new file mode 100755 index 0000000..5a2b4fb --- /dev/null +++ b/import.php @@ -0,0 +1,88 @@ + "Invalid JSON session file"]); + exit(); + } + + // Debug: Log the original import data structure + error_log("Import data structure: " . print_r(array_keys($imported), true)); + if (isset($imported["characters"])) { + error_log("Characters in import: " . print_r(array_keys($imported["characters"]), true)); + } + + // Thorough cleaning of the imported data + if (isset($imported["characters"]) && is_array($imported["characters"])) { + foreach (array_keys($imported["characters"]) as $charID) { + // Remove any entries with empty or non-numeric character IDs + if (empty($charID) || $charID === "" || !is_numeric($charID)) { + error_log("Removing invalid character ID: '$charID'"); + unset($imported["characters"][$charID]); + } + } + } + + // Only assign cleaned data to session + $_SESSION = $imported; + + // Refresh tokens for each character if needed + if (isset($_SESSION["characters"]) && is_array($_SESSION["characters"])) { + // Extra validation check + foreach (array_keys($_SESSION["characters"]) as $charID) { + if (empty($charID) || $charID === "") { + error_log("Removing empty character ID from session"); + unset($_SESSION["characters"][$charID]); + } + } + + foreach ($_SESSION["characters"] as $charID => &$char) { + if ( + empty($char["access_token"]) || + !empty($char["refresh_token"]) // Always refresh on import + ) { + error_log("Refreshing token for character $charID"); + $new_tokens = refresh_token($char["refresh_token"]); + if (!empty($new_tokens["access_token"])) { + $char["access_token"] = $new_tokens["access_token"]; + if (!empty($new_tokens["refresh_token"])) { + $char["refresh_token"] = $new_tokens["refresh_token"]; + } + } else { + error_log("Failed to refresh token for character $charID"); + } + } + } + unset($char); // Break the reference to avoid unexpected behavior + + // Final validation to ensure no empty entries + if (isset($_SESSION["characters"][""])) { + error_log("Final cleanup: Removing empty character ID"); + unset($_SESSION["characters"][""]); + } + } + + // Log the final session structure + error_log("Final character count: " . count($_SESSION["characters"])); + error_log("Character IDs after import: " . implode(", ", array_keys($_SESSION["characters"]))); + + header("Content-Type: application/json"); + echo json_encode([ + "status" => "success", + "characters_imported" => count($_SESSION["characters"]), + ]); + // After successful import, redirect to index.php + header("Location: index.php"); + exit(); +} else { + http_response_code(400); + echo json_encode(["error" => "POST with session_file required"]); + exit(); +} diff --git a/index.php b/index.php new file mode 100755 index 0000000..2ecfce2 --- /dev/null +++ b/index.php @@ -0,0 +1,194 @@ + + + + + + + + EVE Industry Tracker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ EVE Jobs Tracker Logo + + + + +
or
+ +
+

πŸ“‚ Import a Saved Session

+
+ +
+ +
+
+ + + +
+ + +
+ +
+ +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/keep_alive.php b/keep_alive.php new file mode 100644 index 0000000..6b1628a --- /dev/null +++ b/keep_alive.php @@ -0,0 +1,89 @@ + 300) { + foreach ($_SESSION["characters"] as $character_id => &$charData) { + // Skip invalid character entries + if (empty($character_id)) continue; + + 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"])) { + $charData["access_token"] = $new_tokens["access_token"]; + $tokens_refreshed = true; + } + } + } + } + unset($charData); // Break reference to avoid unexpected behavior + $_SESSION['last_token_refresh'] = $page_load_time; + + // Only clear cache if tokens were refreshed + if ($tokens_refreshed) { + // Clear specific cache entries + $cache_keys = [ + 'character_jobs_', + 'corporation_jobs_', + 'all_character_blueprints' + ]; + + foreach ($cache_keys as $prefix) { + $cleared = clear_cache_by_prefix($prefix); + if ($cleared) $cache_cleared = true; + } + } + } + } + + echo json_encode([ + "status" => "success", + "message" => "Session refreshed", + "tokens_refreshed" => $tokens_refreshed, + "cache_cleared" => $cache_cleared, + "timestamp" => time() + ]); +} else { + echo json_encode(["status" => "error", "message" => "Session not active"]); +} + +// Function to clear cache entries by prefix +function clear_cache_by_prefix($prefix) { + $cache_dir = __DIR__ . "/cache"; + $cache_file = $cache_dir . "/api_cache.json"; + + if (!file_exists($cache_file)) { + return false; + } + + $cache = json_decode(file_get_contents($cache_file), true) ?: []; + $cleared = false; + + foreach ($cache as $key => $value) { + if (strpos($key, $prefix) === 0) { + unset($cache[$key]); + $cleared = true; + } + } + + if ($cleared) { + file_put_contents($cache_file, json_encode($cache)); + } + + return $cleared; +} +?> \ No newline at end of file diff --git a/login.php b/login.php new file mode 100755 index 0000000..337d095 --- /dev/null +++ b/login.php @@ -0,0 +1,31 @@ + "code", + "redirect_uri" => $redirect_uri, + "client_id" => $client_id, + "scope" => $scope, + "state" => $state, + ]); + +header("Location: $url"); +exit(); diff --git a/logout.php b/logout.php new file mode 100755 index 0000000..57ab647 --- /dev/null +++ b/logout.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/populate_cache.php b/populate_cache.php new file mode 100644 index 0000000..ce83a03 --- /dev/null +++ b/populate_cache.php @@ -0,0 +1,103 @@ + $name) { + if (str_ends_with($name, "Blueprint")) { + $blueprint_ids[] = $id; + } + } + usleep(250000); + } + + return $blueprint_ids; +} + +// Batch fetch type names using /universe/names/ + + +// Populate the blueprint cache +function populate_blueprint_cache() +{ + echo "Fetching all type IDs...\n"; + $all_type_ids = get_all_type_ids(); + + if (empty($all_type_ids)) { + echo "No type IDs retrieved.\n"; + return; + } + + echo "Filtering blueprint IDs...\n"; + $blueprint_ids = filter_blueprint_ids($all_type_ids); + + if (empty($blueprint_ids)) { + echo "No blueprint IDs found.\n"; + return; + } + + echo "Fetching blueprint names...\n"; + $cached_data = []; + $chunks = array_chunk($blueprint_ids, 1000); + foreach ($chunks as $chunk) { + $batch_names = fetch_type_names($chunk); + foreach ($batch_names as $id => $name) { + $cached_data[$id] = $name; + } + usleep(250000); + } + + // Save to JSON cache file + $cache_dir = __DIR__ . "/cache"; + if (!is_dir($cache_dir)) { + mkdir($cache_dir, 0775, true); + } + $cache_file = $cache_dir . "/blueprint_cache.json"; + file_put_contents( + $cache_file, + json_encode($cached_data, JSON_PRETTY_PRINT) + ); + + echo "Cache populated with " . count($cached_data) . " blueprint names.\n"; +} + +// Run it +populate_blueprint_cache(); diff --git a/project_description.md b/project_description.md new file mode 100644 index 0000000..ac53b50 --- /dev/null +++ b/project_description.md @@ -0,0 +1,29 @@ +# 🏭 EVE Industry Tracker + +A simple dashboard to track all your EVE Online industry jobs in one place. + +## ✨ Features +- View all industry jobs across multiple characters +- Auto-refreshing status and countdown timers +- Sort and filter jobs easily +- Track blueprint inventory and research +- Export/Import session data for backups +- Works great on mobile devices + +## πŸš€ Getting Started +1. Visit https://eveindy.claytonia.net +2. Log in with EVE SSO +3. Add more characters if needed +4. Track your industry empire! + +## πŸ”’ Privacy +- Uses official EVE SSO +- No data stored on servers +- Export data locally +- Secure token handling + +## πŸ’« Support +If you find this tool useful, send ISK to `Clay's Accountant` + +--- +*EVE Online and the EVE logo are the registered trademarks of CCP hf. All rights reserved. Used with permission.* \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..d0283bc --- /dev/null +++ b/robots.txt @@ -0,0 +1,24 @@ +User-agent: * +Allow: / +Allow: /index.php +Allow: /dashboard.php +Allow: /blueprints_dashboard.php +Allow: /login.php +Allow: /assets/ +Disallow: /cache/ +Disallow: /exports/ +Disallow: /api/ +Disallow: /.local/ +Disallow: /.zed/ +Disallow: /views.php +Disallow: /track_visits.php +Disallow: /session_bootstrap.php +Disallow: /populate_cache.php +Disallow: /logout.php +Disallow: /keep_alive.php +Disallow: /import.php +Disallow: /functions.php +Disallow: /export.php +Disallow: /callback.php + +Sitemap: https://eveindy.claytonia.net/sitemap.xml \ No newline at end of file diff --git a/session_bootstrap.php b/session_bootstrap.php new file mode 100755 index 0000000..65b7c45 --- /dev/null +++ b/session_bootstrap.php @@ -0,0 +1,31 @@ + $lifetime, + "path" => "/", + "secure" => isset($_SERVER["HTTPS"]), + "httponly" => true, + "samesite" => "Lax", +]); + +session_start(); + +// Basic session cleanup - only remove empty character IDs +if (isset($_SESSION["characters"]) && is_array($_SESSION["characters"])) { + // Remove any empty character IDs + if (isset($_SESSION["characters"][""])) { + unset($_SESSION["characters"][""]); + } + + // Remove any null or false entries + foreach ($_SESSION["characters"] as $charID => $char) { + if (empty($charID) || $charID === false || $charID === null) { + unset($_SESSION["characters"][$charID]); + } + } +} +?> \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..f71a90d --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,27 @@ + + + + https://eveindy.claytonia.net/ + 2024-01-09 + daily + 1.0 + + + https://eveindy.claytonia.net/login.php + 2024-01-09 + monthly + 0.8 + + + https://eveindy.claytonia.net/dashboard.php + 2024-01-09 + daily + 0.9 + + + https://eveindy.claytonia.net/blueprints_dashboard.php + 2024-01-09 + daily + 0.9 + + \ No newline at end of file diff --git a/track_visits.php b/track_visits.php new file mode 100644 index 0000000..0e8ed80 --- /dev/null +++ b/track_visits.php @@ -0,0 +1,119 @@ + 0, + "unique_visits" => 0, + "last_visit" => null, + "monthly" => [], + "weekly" => [], + "browsers" => [], + "operating_systems" => [], + ]; + } + + // Increment the total visit count + $stats["total_visits"]++; + + // If it's a new visitor, increase the unique visit count + if ($firstVisit) { + $stats["unique_visits"]++; + setcookie($cookieName, "1", time() + $cookieLifetime, "/"); // Set cookie for this user + } + + // Update the last visit timestamp + $timestamp = time(); + $stats["last_visit"] = $timestamp; + + // Track by week and month + $week = date("W", $timestamp); // Week number (ISO 8601 format) + $month = date("Y-m", $timestamp); // Month in YYYY-MM format + + // Increment weekly stats + if (!isset($stats["weekly"][$week])) { + $stats["weekly"][$week] = 0; + } + $stats["weekly"][$week]++; + + // Increment monthly stats + if (!isset($stats["monthly"][$month])) { + $stats["monthly"][$month] = 0; + } + $stats["monthly"][$month]++; + + // Track browser type + if (!isset($stats["browsers"][$browser])) { + $stats["browsers"][$browser] = 0; + } + $stats["browsers"][$browser]++; + + // Track operating system + if (!isset($stats["operating_systems"][$os])) { + $stats["operating_systems"][$os] = 0; + } + $stats["operating_systems"][$os]++; + + // Write the updated stats back to the file + fseek($fp, 0); + fwrite($fp, json_encode($stats)); + fflush($fp); + + // Unlock the file + flock($fp, LOCK_UN); +} + +// Close the file +fclose($fp); +?> \ No newline at end of file diff --git a/views.php b/views.php new file mode 100755 index 0000000..1414603 --- /dev/null +++ b/views.php @@ -0,0 +1,184 @@ + 0, + "unique_visits" => 0, + "last_visit" => time(), + "monthly" => [], + "weekly" => [], + "browsers" => [], + "operating_systems" => [], + ]; + } + + // Read the stats from the file + $fp = fopen($visitsFile, "r"); + if ($fp === false) { + die("Error opening the file."); + } + + $data = fgets($fp); + fclose($fp); + + $stats = json_decode($data, true); + + // Check if JSON decode failed + if ($stats === null) { + error_log("Failed to decode visits.json: " . json_last_error_msg()); + return [ + "total_visits" => 0, + "unique_visits" => 0, + "last_visit" => time(), + "monthly" => [], + "weekly" => [], + "browsers" => [], + "operating_systems" => [], + ]; + } + + return $stats; +} + +$stats = getVisitStats(); +?> + + + + + + Website Stats + + + + + +
+

Website Stats

+ +
+

General Stats

+

Total Visits:

+

Unique Visits:

+

Last Visit Timestamp:

+
+ + +
+

Monthly Visits

+ + + + + + + + + $count): ?> + + + + + + +
MonthVisits
+
+ + +
+

Weekly Visits

+ + + + + + + + + $count): ?> + + + + + + +
WeekVisits
Week
+
+ + +
+

Browser Stats

+ + + + + + + + + $count): ?> + + + + + + +
BrowserVisits
+
+ + +
+

Operating System Stats

+ + + + + + + + + $count + ): ?> + + + + + + +
Operating SystemVisits
+
+
+
+

Just a Quick Note

+

We know, we know… this isn’t the most sophisticated way to track visitors. No fancy servers or big data here, just some humble tracking behind the scenes. But hey, it’s privacy-friendly! We’re not tracking your every move or creating a digital dossier, just counting visits in a way that doesn’t require selling your personal info to the highest bidder. So enjoy the stats, and know that your privacy is safe with us!

+
+ + + diff --git a/visits.json b/visits.json new file mode 100755 index 0000000..eb66cd3 --- /dev/null +++ b/visits.json @@ -0,0 +1 @@ +{"total_visits":521,"unique_visits":43,"last_visit":1747923537,"monthly":{"2025-05":471},"weekly":{"20":81,"21":390},"browsers":{"Firefox":439,"Unknown Browser":19,"Chrome":11,"Safari":2},"operating_systems":{"Linux":322,"Unknown OS":20,"Windows":99,"iOS":2,"Android":26,"macOS":2}} \ No newline at end of file diff --git a/visits.txt b/visits.txt new file mode 100755 index 0000000..1edbdba --- /dev/null +++ b/visits.txt @@ -0,0 +1 @@ +185 \ No newline at end of file