Initial commit of EVE Industry Tracker
This commit is contained in:
		
						commit
						43988423e0
					
				
					 30 changed files with 3627 additions and 0 deletions
				
			
		
							
								
								
									
										60
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
							
								
								
									
										72
									
								
								.htaccess
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								.htaccess
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
<IfModule mod_expires.c>
 | 
			
		||||
    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"
 | 
			
		||||
</IfModule>
 | 
			
		||||
 | 
			
		||||
# Enable compression
 | 
			
		||||
<IfModule mod_deflate.c>
 | 
			
		||||
    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
 | 
			
		||||
</IfModule>
 | 
			
		||||
 | 
			
		||||
# Security headers
 | 
			
		||||
<IfModule mod_headers.c>
 | 
			
		||||
    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"
 | 
			
		||||
</IfModule>
 | 
			
		||||
 | 
			
		||||
# Prevent directory listing
 | 
			
		||||
Options -Indexes
 | 
			
		||||
 | 
			
		||||
# Protect sensitive files
 | 
			
		||||
<FilesMatch "^\.">
 | 
			
		||||
    Order allow,deny
 | 
			
		||||
    Deny from all
 | 
			
		||||
</FilesMatch>
 | 
			
		||||
 | 
			
		||||
<FilesMatch "\.(json|txt|log|cache)$">
 | 
			
		||||
    Order allow,deny
 | 
			
		||||
    Deny from all
 | 
			
		||||
</FilesMatch>
 | 
			
		||||
 | 
			
		||||
# Allow access to visits.json specifically
 | 
			
		||||
<Files "visits.json">
 | 
			
		||||
    Order allow,deny
 | 
			
		||||
    Allow from all
 | 
			
		||||
</Files>
 | 
			
		||||
 | 
			
		||||
# Custom error pages
 | 
			
		||||
ErrorDocument 404 /404.php
 | 
			
		||||
ErrorDocument 403 /403.php
 | 
			
		||||
ErrorDocument 500 /500.php
 | 
			
		||||
							
								
								
									
										0
									
								
								.zed/settings.json
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										0
									
								
								.zed/settings.json
									
										
									
									
									
										Executable file
									
								
							
							
								
								
									
										65
									
								
								404.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								404.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
<?php 
 | 
			
		||||
require_once __DIR__ . "/session_bootstrap.php";
 | 
			
		||||
http_response_code(404);
 | 
			
		||||
?>
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="UTF-8">
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
        <title>Page Not Found - EVE Industry Tracker</title>
 | 
			
		||||
        <meta name="description" content="Page not found - EVE Industry Tracker">
 | 
			
		||||
        <meta name="robots" content="noindex,follow">
 | 
			
		||||
        <link rel="canonical" href="/404">
 | 
			
		||||
        <link rel="stylesheet" href="assets/styles.min.css">
 | 
			
		||||
        <link rel="icon" href="assets/EIJT-FAVICON.png" type="image/png">
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <div class="container" style="text-align: center;">
 | 
			
		||||
            <img src="assets/logo.png" 
 | 
			
		||||
                 width="30%" 
 | 
			
		||||
                 style="max-width: 300px; margin-bottom: 2em;" 
 | 
			
		||||
                 alt="EVE Jobs Tracker Logo" 
 | 
			
		||||
                 loading="eager">
 | 
			
		||||
 | 
			
		||||
            <div class="section">
 | 
			
		||||
                <h1 style="color: #00ffcc;">404 - Page Not Found</h1>
 | 
			
		||||
                <p style="margin-bottom: 2em;">Sorry, the page you're looking for doesn't exist.</p>
 | 
			
		||||
 | 
			
		||||
                <div class="no-jobs-message" style="text-align: left; max-width: 400px; margin: 2em auto;">
 | 
			
		||||
                    <p style="color: #00ffcc; margin-bottom: 1em;">Looking for something specific? Here are some tips:</p>
 | 
			
		||||
                    <ul style="list-style-type: disc; padding-left: 2em; margin-bottom: 2em;">
 | 
			
		||||
                        <li>Check that the URL is spelled correctly</li>
 | 
			
		||||
                        <li>Try navigating from our homepage</li>
 | 
			
		||||
                        <li>If you're logged in, try refreshing your session</li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div style="display: flex; justify-content: center; margin: 2em 0;">
 | 
			
		||||
                    <a href="login.php">
 | 
			
		||||
                        <button>Log In</button>
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="divider"><span>or</span></div>
 | 
			
		||||
 | 
			
		||||
                <div class="import-form">
 | 
			
		||||
                    <h3 style="color: #00ffcc; margin-bottom: 1em;">📂 Import a Saved Session</h3>
 | 
			
		||||
                    <form action="import.php" method="POST" enctype="multipart/form-data">
 | 
			
		||||
                        <input type="file" 
 | 
			
		||||
                               name="session_file" 
 | 
			
		||||
                               accept=".json" 
 | 
			
		||||
                               required>
 | 
			
		||||
                        <button type="submit">Import Session</button>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <footer>
 | 
			
		||||
            <p>Need help? Send ISK to <a href="https://evewho.com/character/95770276" target="_blank" rel="noopener">Clay's Accountant</a></p>
 | 
			
		||||
        </footer>
 | 
			
		||||
 | 
			
		||||
        <?php require_once "track_visits.php"; ?>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										149
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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.
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/EIJT-FAVICON.png
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/EIJT-FAVICON.png
									
										
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 713 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/logo.png
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/logo.png
									
										
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 101 KiB  | 
							
								
								
									
										378
									
								
								assets/styles.css
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										378
									
								
								assets/styles.css
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								assets/styles.min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/styles.min.css
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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:" ▼"}
 | 
			
		||||
							
								
								
									
										56
									
								
								assets/table-sort.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								assets/table-sort.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										286
									
								
								blueprints_dashboard.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										286
									
								
								blueprints_dashboard.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,286 @@
 | 
			
		|||
<?php
 | 
			
		||||
require_once __DIR__ . "/session_bootstrap.php";
 | 
			
		||||
require_once "functions.php";
 | 
			
		||||
 | 
			
		||||
// Set a timestamp for when the page was loaded
 | 
			
		||||
$page_load_time = time();
 | 
			
		||||
 | 
			
		||||
// 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"])) {
 | 
			
		||||
                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. <a href='index.php'>Go back</a>";
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
?>
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <title>Blueprints</title>
 | 
			
		||||
    <style>
 | 
			
		||||
        th.sorted-asc::after { content: " ▲"; }
 | 
			
		||||
        th.sorted-desc::after { content: " ▼"; }
 | 
			
		||||
        #blueprintsTable {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            border-collapse: collapse;
 | 
			
		||||
            margin-top: 20px;
 | 
			
		||||
            table-layout: auto;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #blueprintsTable th,
 | 
			
		||||
        #blueprintsTable td {
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            text-align: left;
 | 
			
		||||
            border-bottom: 1px solid #333;
 | 
			
		||||
            white-space: nowrap;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            text-overflow: ellipsis;
 | 
			
		||||
            min-width: 100px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #blueprintsTable th {
 | 
			
		||||
            background-color: #2a2a2a;
 | 
			
		||||
            color: #00ffcc;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            position: sticky;
 | 
			
		||||
            top: 0;
 | 
			
		||||
            z-index: 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #blueprintsTable tr:nth-child(even) {
 | 
			
		||||
            background-color: #1e1e1e;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #blueprintsTable tr:hover {
 | 
			
		||||
            background-color: #2c2c2c;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <h1>Blueprints</h1>
 | 
			
		||||
    <table id="blueprintsTable" class="dashboard-table">
 | 
			
		||||
        <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>Blueprint Name</th>
 | 
			
		||||
                <th>Material Efficiency</th>
 | 
			
		||||
                <th>Time Efficiency</th>
 | 
			
		||||
                <th>Number of Runs</th>
 | 
			
		||||
                <th>Runs Remaining</th>
 | 
			
		||||
 | 
			
		||||
            </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
            <?php foreach ($blueprints as $bp): ?>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td><?php echo htmlspecialchars(
 | 
			
		||||
                        $bp["blueprint_name"]
 | 
			
		||||
                    ); ?></td>
 | 
			
		||||
                    <td><?php echo htmlspecialchars(
 | 
			
		||||
                        $bp["material_efficiency"]
 | 
			
		||||
                    ); ?>%</td>
 | 
			
		||||
                    <td><?php echo htmlspecialchars(
 | 
			
		||||
                        $bp["time_efficiency"]
 | 
			
		||||
                    ); ?>%</td>
 | 
			
		||||
                    <td><?php echo htmlspecialchars(
 | 
			
		||||
                        $bp["runs"] == -1 ? "Original" : $bp["runs"]
 | 
			
		||||
                    ); ?></td>
 | 
			
		||||
                    <td><?php echo htmlspecialchars(
 | 
			
		||||
                        $bp["runs"] == -1 ? "∞" : $bp["runs"]
 | 
			
		||||
                    ); ?></td>
 | 
			
		||||
 | 
			
		||||
                </tr>
 | 
			
		||||
            <?php endforeach; ?>
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
    <script>
 | 
			
		||||
        document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
            let sortDirection = [];
 | 
			
		||||
 | 
			
		||||
            document.getElementById('blueprintsTable').querySelector('thead').addEventListener('click', (event) => {
 | 
			
		||||
                const th = event.target;
 | 
			
		||||
                if (th.tagName === 'TH') {
 | 
			
		||||
                    const col = Array.from(th.parentNode.children).indexOf(th);
 | 
			
		||||
                    sortTable(col);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            function sortTable(col) {
 | 
			
		||||
                const table = document.getElementById("blueprintsTable");
 | 
			
		||||
                const rows = Array.from(table.tBodies[0].rows);
 | 
			
		||||
                const dir = (sortDirection[col] = !sortDirection[col]);
 | 
			
		||||
 | 
			
		||||
                // Create a document fragment for better performance
 | 
			
		||||
                const fragment = document.createDocumentFragment();
 | 
			
		||||
 | 
			
		||||
                rows.sort((a, b) => {
 | 
			
		||||
                    const aCell = a.cells[col];
 | 
			
		||||
                    const bCell = b.cells[col];
 | 
			
		||||
                    const aText = aCell.textContent.trim();
 | 
			
		||||
                    const bText = bCell.textContent.trim();
 | 
			
		||||
 | 
			
		||||
                    if (!isNaN(aText) && !isNaN(bText)) {
 | 
			
		||||
                        return dir ? bText - aText : aText - bText;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return dir ? bText.localeCompare(aText) : aText.localeCompare(bText);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Add rows to fragment
 | 
			
		||||
                rows.forEach((row) => fragment.appendChild(row));
 | 
			
		||||
                
 | 
			
		||||
                // Clear and append in a single operation
 | 
			
		||||
                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");
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
    <script>
 | 
			
		||||
        document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
            let sortDirection = [];
 | 
			
		||||
 | 
			
		||||
            document.getElementById('blueprintsTable').querySelector('thead').addEventListener('click', (event) => {
 | 
			
		||||
                const th = event.target;
 | 
			
		||||
                if (th.tagName === 'TH') {
 | 
			
		||||
                    const col = Array.from(th.parentNode.children).indexOf(th);
 | 
			
		||||
                    sortTable(col);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            function sortTable(col) {
 | 
			
		||||
                const table = document.getElementById("blueprintsTable");
 | 
			
		||||
                const rows = Array.from(table.tBodies[0].rows);
 | 
			
		||||
                const dir = (sortDirection[col] = !sortDirection[col]);
 | 
			
		||||
 | 
			
		||||
                rows.sort((a, b) => {
 | 
			
		||||
                    const aCell = a.cells[col];
 | 
			
		||||
                    const bCell = b.cells[col];
 | 
			
		||||
                    const aText = aCell.textContent.trim();
 | 
			
		||||
                    const bText = bCell.textContent.trim();
 | 
			
		||||
 | 
			
		||||
                    if (!isNaN(aText) && !isNaN(bText)) {
 | 
			
		||||
                        return dir ? bText - aText : aText - bText;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return dir ? bText.localeCompare(aText) : aText.localeCompare(bText);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                rows.forEach((row) => table.tBodies[0].appendChild(row));
 | 
			
		||||
 | 
			
		||||
                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");
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Session keep-alive mechanism
 | 
			
		||||
            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);
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                    .catch(error => console.error("Error in keep-alive request:", error));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Call keepSessionAlive only if the user is active
 | 
			
		||||
            let sessionTimer;
 | 
			
		||||
            const resetSessionTimer = () => {
 | 
			
		||||
                clearTimeout(sessionTimer);
 | 
			
		||||
                sessionTimer = setTimeout(keepSessionAlive, 300000); // 5 minutes
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Reset timer on user activity
 | 
			
		||||
            ['mousemove', 'keydown', 'click', 'scroll'].forEach(event => {
 | 
			
		||||
                document.addEventListener(event, resetSessionTimer, false);
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // Initial setup
 | 
			
		||||
            resetSessionTimer();
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										82
									
								
								callback.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										82
									
								
								callback.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
<?php
 | 
			
		||||
require_once __DIR__ . "/session_bootstrap.php";
 | 
			
		||||
 | 
			
		||||
$client_id = "YOUR-EVE-CLIENT-ID";
 | 
			
		||||
// Replace with your ESI client secret from EVE Developer Portal
 | 
			
		||||
$client_secret = "YOUR-EVE-CLIENT-SECRET";
 | 
			
		||||
$redirect_uri = "YOUR-CALLBACK-URL"; // Example: https://your-domain.com/callback.php
 | 
			
		||||
 | 
			
		||||
// Validate the OAuth state and code
 | 
			
		||||
if (
 | 
			
		||||
    !isset($_GET["code"]) ||
 | 
			
		||||
    !isset($_GET["state"]) ||
 | 
			
		||||
    $_GET["state"] !== $_SESSION["oauth2state"]
 | 
			
		||||
) {
 | 
			
		||||
    exit("Invalid state or code");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$code = $_GET["code"];
 | 
			
		||||
$token_url = "https://login.eveonline.com/v2/oauth/token";
 | 
			
		||||
 | 
			
		||||
// Get the access and refresh tokens
 | 
			
		||||
$ch = curl_init($token_url);
 | 
			
		||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 | 
			
		||||
curl_setopt($ch, CURLOPT_POST, true);
 | 
			
		||||
curl_setopt(
 | 
			
		||||
    $ch,
 | 
			
		||||
    CURLOPT_POSTFIELDS,
 | 
			
		||||
    http_build_query([
 | 
			
		||||
        "grant_type" => "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();
 | 
			
		||||
							
								
								
									
										625
									
								
								dashboard.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										625
									
								
								dashboard.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,625 @@
 | 
			
		|||
<?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>
 | 
			
		||||
							
								
								
									
										97
									
								
								eveindy.claytonia.net
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								eveindy.claytonia.net
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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 "</assets/styles.min.css>; rel=preload; as=style" always;
 | 
			
		||||
        add_header Link "</assets/logo.png>; 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								export.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										39
									
								
								export.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
<?php
 | 
			
		||||
session_start();
 | 
			
		||||
 | 
			
		||||
// Make sure the session contains data to export
 | 
			
		||||
if (empty($_SESSION)) {
 | 
			
		||||
    http_response_code(400);
 | 
			
		||||
    header("Content-Type: application/json");
 | 
			
		||||
    echo json_encode(["error" => "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();
 | 
			
		||||
?>
 | 
			
		||||
							
								
								
									
										795
									
								
								functions.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										795
									
								
								functions.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,795 @@
 | 
			
		|||
<?php
 | 
			
		||||
require_once __DIR__ . "/session_bootstrap.php";
 | 
			
		||||
 | 
			
		||||
define("ESI_CLIENT_ID", "YOUR-EVE-CLIENT-ID");
 | 
			
		||||
// Replace with your ESI client secret from EVE Developer Portal
 | 
			
		||||
define("ESI_CLIENT_SECRET", "YOUR-EVE-CLIENT-SECRET");
 | 
			
		||||
 | 
			
		||||
function fetch_character_jobs($character_id, &$access_token)
 | 
			
		||||
{
 | 
			
		||||
    // Check cache first
 | 
			
		||||
    $cache_key = "character_jobs_{$character_id}";
 | 
			
		||||
    $cache = get_cache_data($cache_key);
 | 
			
		||||
    
 | 
			
		||||
    if ($cache !== null) {
 | 
			
		||||
        return $cache;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $esi_url = "https://esi.evetech.net/latest/characters/{$character_id}/industry/jobs/?include_completed=false";
 | 
			
		||||
    $headers = [
 | 
			
		||||
        "Authorization: Bearer $access_token",
 | 
			
		||||
        "Accept: application/json",
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $response = esi_call($esi_url, $headers);
 | 
			
		||||
 | 
			
		||||
    // Token expired? Try to refresh it.
 | 
			
		||||
    if (
 | 
			
		||||
        $response["http_code"] === 401 &&
 | 
			
		||||
        isset($_SESSION["characters"][$character_id]["refresh_token"])
 | 
			
		||||
    ) {
 | 
			
		||||
        $new_tokens = refresh_token(
 | 
			
		||||
            $_SESSION["characters"][$character_id]["refresh_token"]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (!empty($new_tokens["access_token"])) {
 | 
			
		||||
            // Save refreshed token
 | 
			
		||||
            $_SESSION["characters"][$character_id]["access_token"] =
 | 
			
		||||
                $new_tokens["access_token"];
 | 
			
		||||
            $access_token = $new_tokens["access_token"];
 | 
			
		||||
            $headers[0] = "Authorization: Bearer $access_token";
 | 
			
		||||
 | 
			
		||||
            // Retry the API call
 | 
			
		||||
            $response = esi_call($esi_url, $headers);
 | 
			
		||||
        } else {
 | 
			
		||||
            error_log(
 | 
			
		||||
                "ERROR: Token refresh failed for character ID $character_id."
 | 
			
		||||
            );
 | 
			
		||||
            return []; // Avoid crashing page
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Still bad? Bail out
 | 
			
		||||
    if ($response["http_code"] !== 200) {
 | 
			
		||||
        error_log(
 | 
			
		||||
            "ERROR: ESI call failed after token refresh for character ID $character_id. HTTP {$response["http_code"]}"
 | 
			
		||||
        );
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $char_jobs = json_decode($response["body"], true);
 | 
			
		||||
    if (!is_array($char_jobs)) {
 | 
			
		||||
        $char_jobs = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Skip corporation jobs if no character jobs (faster load)
 | 
			
		||||
    if (empty($char_jobs)) {
 | 
			
		||||
        // Cache empty results for 60 seconds (shorter time for empty results)
 | 
			
		||||
        set_cache_data($cache_key, [], 60);
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Fetch corporation jobs if possible
 | 
			
		||||
    $corp_jobs = [];
 | 
			
		||||
    $character_info = fetch_character_info($character_id, $access_token);
 | 
			
		||||
    if (isset($character_info["corporation_id"])) {
 | 
			
		||||
        $corp_jobs = fetch_corporation_jobs(
 | 
			
		||||
            $character_info["corporation_id"],
 | 
			
		||||
            $access_token
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Filter out corp jobs where character_id is not in the $_SESSION["characters"]
 | 
			
		||||
    $filtered_corp_jobs = [];
 | 
			
		||||
    foreach ($corp_jobs as $job) {
 | 
			
		||||
        if (
 | 
			
		||||
            isset($job["installer_id"]) &&
 | 
			
		||||
            $job["installer_id"] == $character_id
 | 
			
		||||
        ) {
 | 
			
		||||
            $filtered_corp_jobs[] = $job;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $all_jobs = array_merge($char_jobs, $filtered_corp_jobs);
 | 
			
		||||
 | 
			
		||||
    // If still no jobs after merging, return empty results
 | 
			
		||||
    if (empty($all_jobs)) {
 | 
			
		||||
        // Cache empty results for 60 seconds (shorter time for empty results)
 | 
			
		||||
        set_cache_data($cache_key, [], 60);
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Extract IDs and fetch names in batch
 | 
			
		||||
    $blueprint_ids = array_unique(array_column($all_jobs, "blueprint_type_id"));
 | 
			
		||||
    $system_ids = array_unique(array_column($all_jobs, "system_id"));
 | 
			
		||||
    
 | 
			
		||||
    // Fetch names in parallel using promises
 | 
			
		||||
    $blueprint_names = [];
 | 
			
		||||
    $system_names = [];
 | 
			
		||||
    
 | 
			
		||||
    // Optimize by caching character name
 | 
			
		||||
    $character_name = $_SESSION["characters"][$character_id]["name"] ?? "Unknown";
 | 
			
		||||
 | 
			
		||||
    // Pre-define activity mapping
 | 
			
		||||
    $activity_map = [
 | 
			
		||||
        1 => "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));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
?>
 | 
			
		||||
							
								
								
									
										88
									
								
								import.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										88
									
								
								import.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,88 @@
 | 
			
		|||
<?php
 | 
			
		||||
require_once __DIR__ . "/session_bootstrap.php";
 | 
			
		||||
require_once "functions.php"; // Include your existing functions
 | 
			
		||||
 | 
			
		||||
// Handle POST with file upload
 | 
			
		||||
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_FILES["session_file"])) {
 | 
			
		||||
    $raw = file_get_contents($_FILES["session_file"]["tmp_name"]);
 | 
			
		||||
    $imported = json_decode($raw, true);
 | 
			
		||||
 | 
			
		||||
    if (!is_array($imported)) {
 | 
			
		||||
        http_response_code(400);
 | 
			
		||||
        echo json_encode(["error" => "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();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										194
									
								
								index.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										194
									
								
								index.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,194 @@
 | 
			
		|||
<?php require_once __DIR__ . "/session_bootstrap.php"; ?>
 | 
			
		||||
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="UTF-8">
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
        <title>EVE Industry Tracker</title>
 | 
			
		||||
 | 
			
		||||
        <!-- Styles -->
 | 
			
		||||
        <link rel="stylesheet" href="assets/styles.min.css">
 | 
			
		||||
 | 
			
		||||
        <!-- Meta Description -->
 | 
			
		||||
        <meta name="description" content="Track and manage your EVE Online industry jobs efficiently. Monitor manufacturing, research, and industry activities across multiple characters in one dashboard.">
 | 
			
		||||
 | 
			
		||||
        <!-- Keywords -->
 | 
			
		||||
        <meta name="keywords" content="EVE Online, industry tracker, manufacturing, research, industry jobs, EVE Online tools">
 | 
			
		||||
 | 
			
		||||
        <!-- Favicon -->
 | 
			
		||||
        <link rel="icon" href="/assets/EIJT-FAVICON.png" type="image/png">
 | 
			
		||||
 | 
			
		||||
        <!-- Preload -->
 | 
			
		||||
        <link rel="preload" href="assets/logo.png" as="image">
 | 
			
		||||
 | 
			
		||||
        <!-- Open Graph (for sharing links) -->
 | 
			
		||||
        <meta property="og:title" content="EVE Industry Tracker - Manage Your EVE Online Industry">
 | 
			
		||||
        <meta property="og:description" content="Track and optimize your EVE Online industry jobs across multiple characters. Free tool for manufacturing, research, and blueprint management.">
 | 
			
		||||
        <meta property="og:image" content="assets/EIJT-FAVICON.png">
 | 
			
		||||
        <meta property="og:type" content="website">
 | 
			
		||||
        <meta property="og:url" content="https://your-domain/">
 | 
			
		||||
        <meta property="og:site_name" content="EVE Industry Tracker">
 | 
			
		||||
        <meta property="og:locale" content="en_US">
 | 
			
		||||
 | 
			
		||||
        <!-- Canonical URL -->
 | 
			
		||||
        <link rel="canonical" href="/">
 | 
			
		||||
 | 
			
		||||
        <!-- Twitter Cards -->
 | 
			
		||||
        <meta name="twitter:card" content="summary_large_image">
 | 
			
		||||
        <meta name="twitter:title" content="EVE Industry Tracker - EVE Online Industry Management">
 | 
			
		||||
        <meta name="twitter:description" content="Free tool to track and optimize your EVE Online industry jobs. Manage manufacturing, research, and blueprints across multiple characters.">
 | 
			
		||||
        <meta name="twitter:image" content="assets/logo.png">
 | 
			
		||||
        <meta name="twitter:site" content="@your-twitter-handle">
 | 
			
		||||
        <meta name="twitter:creator" content="@your-twitter-handle">
 | 
			
		||||
        <link rel="preload" href="assets/styles.min.css" as="style">
 | 
			
		||||
 | 
			
		||||
        <!-- Mobile & Theming -->
 | 
			
		||||
        <meta name="theme-color" content="#1b1b1b">
 | 
			
		||||
        <meta name="apple-mobile-web-app-capable" content="yes">
 | 
			
		||||
 | 
			
		||||
        <!-- Basic Security -->
 | 
			
		||||
        <meta http-equiv="X-Content-Type-Options" content="nosniff">
 | 
			
		||||
        <meta http-equiv="Referrer-Policy" content="no-referrer">
 | 
			
		||||
        <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';">
 | 
			
		||||
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" media="print" onload="this.media='all'">
 | 
			
		||||
        <style>
 | 
			
		||||
            /* Styles moved to styles.css */
 | 
			
		||||
        </style>
 | 
			
		||||
 | 
			
		||||
        <!-- Schema.org markup for rich snippets -->
 | 
			
		||||
        <script type="application/ld+json">
 | 
			
		||||
        {
 | 
			
		||||
            "@context": "http://schema.org",
 | 
			
		||||
            "@type": "WebApplication",
 | 
			
		||||
            "name": "EVE Industry Tracker",
 | 
			
		||||
            "applicationCategory": "Game Tool",
 | 
			
		||||
            "operatingSystem": "Any",
 | 
			
		||||
            "url": "https://your-domain/",
 | 
			
		||||
            "offers": {
 | 
			
		||||
                "@type": "Offer",
 | 
			
		||||
                "price": "0",
 | 
			
		||||
                "priceCurrency": "USD"
 | 
			
		||||
            },
 | 
			
		||||
            "description": "Track and manage your EVE Online industry jobs efficiently. Monitor manufacturing, research, and industry activities across multiple characters in one dashboard.",
 | 
			
		||||
            "browserRequirements": "Requires JavaScript. Any modern web browser.",
 | 
			
		||||
            "softwareVersion": "1.0",
 | 
			
		||||
            "applicationSubCategory": "EVE Online Tools",
 | 
			
		||||
            "featureList": [
 | 
			
		||||
                "Industry jobs tracking",
 | 
			
		||||
                "Multi-character support",
 | 
			
		||||
                "Blueprint management",
 | 
			
		||||
                "Real-time updates",
 | 
			
		||||
                "Session export/import"
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
        </script>
 | 
			
		||||
    </head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="container" style="text-align: center; padding: 2em;">
 | 
			
		||||
        <img src="assets/logo.png" width="50%" style="max-width: 600px; margin-bottom: 2em;" alt="EVE Jobs Tracker Logo" loading="eager">
 | 
			
		||||
 | 
			
		||||
        <?php if (
 | 
			
		||||
            !isset($_SESSION["characters"]) ||
 | 
			
		||||
            count($_SESSION["characters"]) === 0
 | 
			
		||||
        ): ?>
 | 
			
		||||
            <div class="section">
 | 
			
		||||
                <a href="login.php">
 | 
			
		||||
                    <button style="padding: 0.75em 1.5em; font-size: 1.1em;">Log In</button>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="divider"><span>or</span></div>
 | 
			
		||||
 | 
			
		||||
            <div class="section">
 | 
			
		||||
                <h3>📂 Import a Saved Session</h3>
 | 
			
		||||
                <form action="import.php" method="POST" enctype="multipart/form-data" style="margin-top: 1em;">
 | 
			
		||||
                    <input type="file" name="session_file" accept=".json" required style="margin-bottom: 1em;">
 | 
			
		||||
                    <br>
 | 
			
		||||
                    <button type="submit" style="padding: 0.5em 1.2em;">Import Session</button>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
        <?php else: ?>
 | 
			
		||||
            <div class="buttons" style="margin-top: 0.1em;">
 | 
			
		||||
                <a href="login.php"><button>Add Another Character</button></a>
 | 
			
		||||
                <a href="logout.php"><button>Logout All</button></a>
 | 
			
		||||
                <a href="export.php"><button id="exportButton">Export Save</button></a>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
             <div class="dashboard-toggle" style="margin-top: 1em;">
 | 
			
		||||
                <button id="jobsDashboardButton" class="active">Industry Jobs</button>
 | 
			
		||||
                <button id="blueprintsDashboardButton">Blueprints</button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div id="jobsDashboard" style="margin-top: 1em;">
 | 
			
		||||
                <?php include "dashboard.php"; ?>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div id="blueprintsDashboard" style="margin-top: 1em; display: none;">
 | 
			
		||||
                <?php include "blueprints_dashboard.php"; ?>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <script>
 | 
			
		||||
                document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
                    // Make export button flash only on first page load
 | 
			
		||||
                    const exportButton = document.getElementById("exportButton");
 | 
			
		||||
                    if (exportButton && !sessionStorage.getItem('exportButtonFlashed')) {
 | 
			
		||||
                        exportButton.classList.add('export-flash');
 | 
			
		||||
                        sessionStorage.setItem('exportButtonFlashed', 'true');
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const jobsButton = document.getElementById("jobsDashboardButton");
 | 
			
		||||
                    const blueprintsButton = document.getElementById("blueprintsDashboardButton");
 | 
			
		||||
                    const jobsDashboard = document.getElementById("jobsDashboard");
 | 
			
		||||
                    const blueprintsDashboard = document.getElementById("blueprintsDashboard");
 | 
			
		||||
 | 
			
		||||
                    jobsButton.addEventListener("click", () => {
 | 
			
		||||
                        jobsDashboard.style.display = "block";
 | 
			
		||||
                        blueprintsDashboard.style.display = "none";
 | 
			
		||||
                        jobsButton.classList.add("active");
 | 
			
		||||
                        blueprintsButton.classList.remove("active");
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    blueprintsButton.addEventListener("click", () => {
 | 
			
		||||
                        jobsDashboard.style.display = "none";
 | 
			
		||||
                        blueprintsDashboard.style.display = "block";
 | 
			
		||||
                        blueprintsButton.classList.add("active");
 | 
			
		||||
                        jobsButton.classList.remove("active");
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            </script>
 | 
			
		||||
        <?php endif; ?>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Footer -->
 | 
			
		||||
    <footer style="text-align: center; padding: 2em 0; background-color: #222; color: #d4d4d4;">
 | 
			
		||||
        <p>If you find this tool useful, please consider donating in-game!</p>
 | 
			
		||||
        <p>Send ISK to <a href="https://evewho.com/character/95770276" target="_blank" style="color: #00aaff;">Clay's Accountant</a>.</p>
 | 
			
		||||
        <p>Thank you for your support! <a href="views.php">🙏</a></p>
 | 
			
		||||
        <p style="text-align: right; font-size: 0.7em; margin-top: 1em; color: #666;">EVE Online and all related materials are trademarks or intellectual property of CCP Games. </p>
 | 
			
		||||
 | 
			
		||||
    </footer>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <!-- stats -->
 | 
			
		||||
    <?php require_once "track_visits.php"; ?>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										89
									
								
								keep_alive.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								keep_alive.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,89 @@
 | 
			
		|||
<?php
 | 
			
		||||
require_once __DIR__ . "/session_bootstrap.php";
 | 
			
		||||
require_once __DIR__ . "/functions.php";
 | 
			
		||||
 | 
			
		||||
// Refresh session and tokens
 | 
			
		||||
if (session_status() === PHP_SESSION_ACTIVE) {
 | 
			
		||||
    $tokens_refreshed = false;
 | 
			
		||||
    $cache_cleared = false;
 | 
			
		||||
    
 | 
			
		||||
    // Check if we need to refresh tokens
 | 
			
		||||
    if (isset($_SESSION["characters"]) && !empty($_SESSION["characters"])) {
 | 
			
		||||
        $page_load_time = time();
 | 
			
		||||
        $last_token_refresh = $_SESSION['last_token_refresh'] ?? 0;
 | 
			
		||||
        
 | 
			
		||||
        // Only refresh tokens if it's been at least 5 minutes since the last refresh
 | 
			
		||||
        if ($page_load_time - $last_token_refresh > 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;
 | 
			
		||||
}
 | 
			
		||||
?>
 | 
			
		||||
							
								
								
									
										31
									
								
								login.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										31
									
								
								login.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<?php
 | 
			
		||||
require_once __DIR__ . "/session_bootstrap.php";
 | 
			
		||||
 | 
			
		||||
$client_id = "990297a28f9c49cabba56aa7ad2704a2";
 | 
			
		||||
$redirect_uri = "https://eveindy.claytonia.net/callback.php";
 | 
			
		||||
 | 
			
		||||
// Full recommended scope list for industry dashboard
 | 
			
		||||
$scope = implode(" ", [
 | 
			
		||||
    "esi-industry.read_character_jobs.v1",
 | 
			
		||||
    "esi-industry.read_corporation_jobs.v1",
 | 
			
		||||
    "esi-characters.read_blueprints.v1",
 | 
			
		||||
    "esi-corporations.read_blueprints.v1",
 | 
			
		||||
    "esi-assets.read_corporation_assets.v1",
 | 
			
		||||
    "esi-universe.read_structures.v1",
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
$state = bin2hex(random_bytes(12));
 | 
			
		||||
$_SESSION["oauth2state"] = $state;
 | 
			
		||||
 | 
			
		||||
$url =
 | 
			
		||||
    "https://login.eveonline.com/v2/oauth/authorize/?" .
 | 
			
		||||
    http_build_query([
 | 
			
		||||
        "response_type" => "code",
 | 
			
		||||
        "redirect_uri" => $redirect_uri,
 | 
			
		||||
        "client_id" => $client_id,
 | 
			
		||||
        "scope" => $scope,
 | 
			
		||||
        "state" => $state,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
header("Location: $url");
 | 
			
		||||
exit();
 | 
			
		||||
							
								
								
									
										1
									
								
								logout.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								logout.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<?php session_start(); session_destroy(); header('Location: index.php'); exit; ?>
 | 
			
		||||
							
								
								
									
										103
									
								
								populate_cache.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								populate_cache.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,103 @@
 | 
			
		|||
<?php
 | 
			
		||||
require_once __DIR__ . "/functions.php"; // Ensure this has no conflicts
 | 
			
		||||
 | 
			
		||||
// Fetch all type IDs from ESI using pagination
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Filter IDs to include only blueprints by checking for names ending in "Blueprint"
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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();
 | 
			
		||||
							
								
								
									
										29
									
								
								project_description.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								project_description.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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.*
 | 
			
		||||
							
								
								
									
										24
									
								
								robots.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								robots.txt
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
							
								
								
									
										31
									
								
								session_bootstrap.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										31
									
								
								session_bootstrap.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<?php
 | 
			
		||||
$lifetime = 60 * 60 * 24 * 14; // 2 weeks
 | 
			
		||||
 | 
			
		||||
ini_set("session.gc_maxlifetime", $lifetime);
 | 
			
		||||
ini_set("session.cookie_lifetime", $lifetime);
 | 
			
		||||
 | 
			
		||||
session_set_cookie_params([
 | 
			
		||||
    "lifetime" => $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]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
?>
 | 
			
		||||
							
								
								
									
										27
									
								
								sitemap.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								sitemap.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
 | 
			
		||||
    <url>
 | 
			
		||||
        <loc>https://eveindy.claytonia.net/</loc>
 | 
			
		||||
        <lastmod>2024-01-09</lastmod>
 | 
			
		||||
        <changefreq>daily</changefreq>
 | 
			
		||||
        <priority>1.0</priority>
 | 
			
		||||
    </url>
 | 
			
		||||
    <url>
 | 
			
		||||
        <loc>https://eveindy.claytonia.net/login.php</loc>
 | 
			
		||||
        <lastmod>2024-01-09</lastmod>
 | 
			
		||||
        <changefreq>monthly</changefreq>
 | 
			
		||||
        <priority>0.8</priority>
 | 
			
		||||
    </url>
 | 
			
		||||
    <url>
 | 
			
		||||
        <loc>https://eveindy.claytonia.net/dashboard.php</loc>
 | 
			
		||||
        <lastmod>2024-01-09</lastmod>
 | 
			
		||||
        <changefreq>daily</changefreq>
 | 
			
		||||
        <priority>0.9</priority>
 | 
			
		||||
    </url>
 | 
			
		||||
    <url>
 | 
			
		||||
        <loc>https://eveindy.claytonia.net/blueprints_dashboard.php</loc>
 | 
			
		||||
        <lastmod>2024-01-09</lastmod>
 | 
			
		||||
        <changefreq>daily</changefreq>
 | 
			
		||||
        <priority>0.9</priority>
 | 
			
		||||
    </url>
 | 
			
		||||
</urlset>
 | 
			
		||||
							
								
								
									
										119
									
								
								track_visits.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								track_visits.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,119 @@
 | 
			
		|||
<?php
 | 
			
		||||
$visitsFile = "visits.json";
 | 
			
		||||
 | 
			
		||||
// Check if the cookie exists to track if the visitor has been here before
 | 
			
		||||
$cookieName = "EIJT";
 | 
			
		||||
$cookieLifetime = 60 * 60 * 24 * 30; // Cookie valid for 30 days
 | 
			
		||||
$firstVisit = !isset($_COOKIE[$cookieName]);
 | 
			
		||||
 | 
			
		||||
// Get user-agent for browser and OS info
 | 
			
		||||
$userAgent = $_SERVER["HTTP_USER_AGENT"];
 | 
			
		||||
$browser = "Unknown Browser";
 | 
			
		||||
$os = "Unknown OS";
 | 
			
		||||
 | 
			
		||||
// Detect browser using simple substr checks
 | 
			
		||||
if (strpos($userAgent, "Firefox") !== false) {
 | 
			
		||||
    $browser = "Firefox";
 | 
			
		||||
} elseif (strpos($userAgent, "Chrome") !== false) {
 | 
			
		||||
    $browser = "Chrome";
 | 
			
		||||
} elseif (strpos($userAgent, "Safari") !== false) {
 | 
			
		||||
    $browser = "Safari";
 | 
			
		||||
} elseif (strpos($userAgent, "Edge") !== false) {
 | 
			
		||||
    $browser = "Edge";
 | 
			
		||||
} elseif (
 | 
			
		||||
    strpos($userAgent, "MSIE") !== false ||
 | 
			
		||||
    strpos($userAgent, "Trident") !== false
 | 
			
		||||
) {
 | 
			
		||||
    $browser = "Internet Explorer";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Detect operating system
 | 
			
		||||
if (strpos($userAgent, "Windows NT") !== false) {
 | 
			
		||||
    $os = "Windows";
 | 
			
		||||
} elseif (strpos($userAgent, "Macintosh") !== false) {
 | 
			
		||||
    $os = "macOS";
 | 
			
		||||
} elseif (strpos($userAgent, "Linux") !== false) {
 | 
			
		||||
    $os = "Linux";
 | 
			
		||||
} elseif (strpos($userAgent, "Android") !== false) {
 | 
			
		||||
    $os = "Android";
 | 
			
		||||
} elseif (strpos($userAgent, "iPhone") !== false) {
 | 
			
		||||
    $os = "iOS";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Open the file in read/write mode, creating it if it doesn't exist
 | 
			
		||||
$fp = fopen($visitsFile, "c+");
 | 
			
		||||
if ($fp === false) {
 | 
			
		||||
    die("Error opening the file.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lock the file to avoid race conditions
 | 
			
		||||
if (flock($fp, LOCK_EX)) {
 | 
			
		||||
    // Read the file content to get the stats
 | 
			
		||||
    $data = fgets($fp); // Read the entire file content (assuming one line per file)
 | 
			
		||||
    if ($data) {
 | 
			
		||||
        $stats = json_decode($data, true);
 | 
			
		||||
    } else {
 | 
			
		||||
        $stats = [
 | 
			
		||||
            "total_visits" => 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);
 | 
			
		||||
?>
 | 
			
		||||
							
								
								
									
										184
									
								
								views.php
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										184
									
								
								views.php
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,184 @@
 | 
			
		|||
<?php
 | 
			
		||||
// Use a function to get stats safely
 | 
			
		||||
function getVisitStats() {
 | 
			
		||||
    $visitsFile = "visits.json";
 | 
			
		||||
    
 | 
			
		||||
    if (!file_exists($visitsFile)) {
 | 
			
		||||
        return [
 | 
			
		||||
            "total_visits" => 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();
 | 
			
		||||
?>
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>Website Stats</title>
 | 
			
		||||
    <link rel="stylesheet" href="assets/styles.css"> <!-- Linking your provided CSS -->
 | 
			
		||||
    <style>
 | 
			
		||||
    /* Specific labels for each table in responsive mode */
 | 
			
		||||
    @media (max-width: 768px) {
 | 
			
		||||
        #monthlyTable td:nth-of-type(1):before { content: "Month"; }
 | 
			
		||||
        #monthlyTable td:nth-of-type(2):before { content: "Visits"; }
 | 
			
		||||
        
 | 
			
		||||
        #weeklyTable td:nth-of-type(1):before { content: "Week"; }
 | 
			
		||||
        #weeklyTable td:nth-of-type(2):before { content: "Visits"; }
 | 
			
		||||
        
 | 
			
		||||
        #browserTable td:nth-of-type(1):before { content: "Browser"; }
 | 
			
		||||
        #browserTable td:nth-of-type(2):before { content: "Visits"; }
 | 
			
		||||
        
 | 
			
		||||
        #osTable td:nth-of-type(1):before { content: "OS"; }
 | 
			
		||||
        #osTable td:nth-of-type(2):before { content: "Visits"; }
 | 
			
		||||
    }
 | 
			
		||||
    </style>
 | 
			
		||||
    <script src="assets/table-sort.js"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <h1>Website Stats</h1>
 | 
			
		||||
 | 
			
		||||
        <section class="section">
 | 
			
		||||
            <h2>General Stats</h2>
 | 
			
		||||
            <p><strong>Total Visits:</strong> <?php echo $stats[
 | 
			
		||||
                "total_visits"
 | 
			
		||||
            ]; ?></p>
 | 
			
		||||
            <p><strong>Unique Visits:</strong> <?php echo $stats[
 | 
			
		||||
                "unique_visits"
 | 
			
		||||
            ]; ?></p>
 | 
			
		||||
            <p><strong>Last Visit Timestamp:</strong> <?php echo date(
 | 
			
		||||
                "Y-m-d H:i:s",
 | 
			
		||||
                $stats["last_visit"]
 | 
			
		||||
            ); ?></p>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
        <!-- Monthly Visits Table -->
 | 
			
		||||
        <section class="section">
 | 
			
		||||
            <h2>Monthly Visits</h2>
 | 
			
		||||
            <table id="monthlyTable" class="jobsTable">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th>Month</th>
 | 
			
		||||
                        <th>Visits</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    <?php foreach ($stats["monthly"] as $month => $count): ?>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td><?php echo $month; ?></td>
 | 
			
		||||
                            <td><?php echo $count; ?></td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    <?php endforeach; ?>
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
        <!-- Weekly Visits Table -->
 | 
			
		||||
        <section class="section">
 | 
			
		||||
            <h2>Weekly Visits</h2>
 | 
			
		||||
            <table id="weeklyTable" class="jobsTable">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th>Week</th>
 | 
			
		||||
                        <th>Visits</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    <?php foreach ($stats["weekly"] as $week => $count): ?>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>Week <?php echo $week; ?></td>
 | 
			
		||||
                            <td><?php echo $count; ?></td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    <?php endforeach; ?>
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
        <!-- Browser Stats Table -->
 | 
			
		||||
        <section class="section">
 | 
			
		||||
            <h2>Browser Stats</h2>
 | 
			
		||||
            <table id="browserTable" class="jobsTable">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th>Browser</th>
 | 
			
		||||
                        <th>Visits</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    <?php foreach ($stats["browsers"] as $browser => $count): ?>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td><?php echo $browser; ?></td>
 | 
			
		||||
                            <td><?php echo $count; ?></td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    <?php endforeach; ?>
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
        <!-- Operating System Stats Table -->
 | 
			
		||||
        <section class="section">
 | 
			
		||||
            <h2>Operating System Stats</h2>
 | 
			
		||||
            <table id="osTable" class="jobsTable">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th>Operating System</th>
 | 
			
		||||
                        <th>Visits</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    <?php foreach (
 | 
			
		||||
                        $stats["operating_systems"]
 | 
			
		||||
                        as $os => $count
 | 
			
		||||
                    ): ?>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td><?php echo $os; ?></td>
 | 
			
		||||
                            <td><?php echo $count; ?></td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    <?php endforeach; ?>
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
        </section>
 | 
			
		||||
    </div>
 | 
			
		||||
    <section class="section">
 | 
			
		||||
        <h2>Just a Quick Note</h2>
 | 
			
		||||
        <p><em>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!</em></p>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										1
									
								
								visits.json
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								visits.json
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -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}}
 | 
			
		||||
							
								
								
									
										1
									
								
								visits.txt
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								visits.txt
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
185
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue