No nearby Wikipedia articles found.
'; } } catch (error) { console.error('Error fetching Wikipedia data:', error); document.getElementById('wikipediaInfo').innerHTML = 'Error loading Wikipedia data.
'; } } // Function to initialize the radar display function initializeRadar() { const canvas = document.getElementById('radarDisplay'); if (canvas) { radarContext = canvas.getContext('2d'); // Initial draw updateWindage(0, null, 0, 0); } } // Function to update windage on the radar display function updateWindage(vehicleSpeed, vehicleHeading, windSpeed, windDirection) { const canvas = radarContext.canvas; const centerX = canvas.width / 2; const centerY = canvas.height / 2; const radius = Math.min(centerX, centerY) - 8; // Clear canvas with transparent background radarContext.clearRect(0, 0, canvas.width, canvas.height); // Draw circular background radarContext.beginPath(); radarContext.arc(centerX, centerY, radius, 0, 2 * Math.PI); radarContext.strokeStyle = '#666'; radarContext.lineWidth = 1; radarContext.stroke(); // Draw concentric circles with speed labels for (let i = 1; i <= 4; i++) { const currentRadius = (radius * i) / 4; radarContext.beginPath(); radarContext.arc(centerX, centerY, currentRadius, 0, 2 * Math.PI); radarContext.strokeStyle = '#666'; radarContext.setLineDash([2, 2]); radarContext.stroke(); radarContext.setLineDash([]); // Add speed label const speedLabel = Math.round((MAX_SPEED * i) / 4); radarContext.fillStyle = '#666'; radarContext.font = '10px Inter'; radarContext.textAlign = 'right'; radarContext.fillText(speedLabel, centerX - 5, centerY - currentRadius + 12); } // Draw cardinal direction lines radarContext.beginPath(); radarContext.moveTo(centerX, centerY - radius); radarContext.lineTo(centerX, centerY + radius); radarContext.moveTo(centerX - radius, centerY); radarContext.lineTo(centerX + radius, centerY); radarContext.strokeStyle = '#666'; radarContext.stroke(); // Draw direction labels with dark gray background for visibility radarContext.fillStyle = '#666'; radarContext.font = '12px Inter'; radarContext.textAlign = 'center'; radarContext.textBaseline = 'middle'; // Position labels with proper spacing and background const labelOffset = radius - 5; function drawLabel(text, x, y) { const padding = 4; const metrics = radarContext.measureText(text); radarContext.fillStyle = '#666'; radarContext.fillText(text, x, y); } drawLabel('FWD', centerX, centerY - labelOffset); drawLabel('AFT', centerX, centerY + labelOffset); drawLabel('RT', centerX + labelOffset, centerY); drawLabel('LT', centerX - labelOffset, centerY); // Get the Tesla blue color from CSS const teslaBlue = getComputedStyle(document.documentElement).getPropertyValue('--tesla-blue').trim(); // Helper function to draw arrow function drawArrow(fromX, fromY, toX, toY, color, headLength = 9) { const angle = Math.atan2(toY - fromY, toX - fromX); const headAngle = Math.PI / 6; // 30 degrees radarContext.beginPath(); radarContext.moveTo(fromX, fromY); radarContext.lineTo(toX, toY); // Draw the arrow head radarContext.lineTo( toX - headLength * Math.cos(angle - headAngle), toY - headLength * Math.sin(angle - headAngle) ); radarContext.moveTo(toX, toY); radarContext.lineTo( toX - headLength * Math.cos(angle + headAngle), toY - headLength * Math.sin(angle + headAngle) ); radarContext.strokeStyle = color; radarContext.lineWidth = 3; radarContext.stroke(); } // Calculate headwind and crosswind components and display on radar let headWind = null; let crossWind = null; if (vehicleHeading && windDirection) { // Threshold for meaningful motion const windAngle = windDirection - vehicleHeading; // car frame const windAngleRad = (90 - windAngle) * Math.PI / 180; // Wind vector components in global frame const windX = windSpeed * Math.cos(windAngleRad); const windY = windSpeed * Math.sin(windAngleRad); // Sum the vectors to get relative wind (for radar plot) const relativeWindX = windX; const relativeWindY = windY; headWind = -windY; // Will be negative if a tailwind crossWind = windX; // Will be positive if from the left const windScale = radius / MAX_SPEED; const relativeWindXPlot = centerX + relativeWindX * windScale; const relativeWindYPlot = centerY - relativeWindY * windScale; drawArrow(centerX, centerY, relativeWindXPlot, relativeWindYPlot, teslaBlue); } // Update the wind component displays with proper units if (headWind !== null) { if (!settings || settings["imperial-units"]) { document.getElementById('headwind').innerText = Math.abs(Math.round(headWind)); } else { // Convert mph to m/s (1 mph ≈ 0.44704 m/s) document.getElementById('headwind').innerText = Math.abs(Math.round(headWind * 0.44704)); } document.getElementById('headwind-arrow').innerHTML = (headWind > 0 ? '▼' : '▲'); // down/up filled triangles // Change the label to TAILWIND when headWind is negative and use appropriate units if (!settings || settings["imperial-units"]) { document.getElementById('headwind-label').innerText = (headWind < 0) ? "TAILWIND (MPH)" : "HEADWIND (MPH)"; } else { document.getElementById('headwind-label').innerText = (headWind < 0) ? "TAILWIND (M/S)" : "HEADWIND (M/S)"; } } else { document.getElementById('headwind').innerText = '--'; document.getElementById('headwind-arrow').innerHTML = ''; // Set label with appropriate units if (!settings || settings["imperial-units"]) { document.getElementById('headwind-label').innerText = "HEADWIND (MPH)"; } else { document.getElementById('headwind-label').innerText = "HEADWIND (M/S)"; } } if (crossWind !== null) { if (!settings || settings["imperial-units"]) { document.getElementById('crosswind').innerText = Math.abs(Math.round(crossWind)); } else { // Convert mph to m/s document.getElementById('crosswind').innerText = Math.abs(Math.round(crossWind * 0.44704)); } document.getElementById('crosswind-arrow').innerHTML = (crossWind >= 0 ? '▶' : '◀'); // right/left triangles } else { document.getElementById('crosswind').innerText = '--'; document.getElementById('crosswind-arrow').innerHTML = ''; } // Set label with appropriate units if (!settings || settings["imperial-units"]) { document.getElementById('crosswind-label').innerText = "CROSSWIND (MPH)"; } else { document.getElementById('crosswind-label').innerText = "CROSSWIND (M/S)"; } } // Function to update location-dependent data async function updateLocationData(lat, long) { customLog('Updating location dependent data for (', lat, ', ', long, ')'); neverUpdatedLocation = false; // Fire off API requests for external data // updateTimeZone(lat, long); fetchCityData(lat, long); // Update connectivity data iff the Network section is visible // const networkSection = document.getElementById("network"); // if (networkSection.style.display === "block") { // customLog('Updating connectivity data...'); // updateNetworkInfo(); // } // Update Wikipedia data iff the Landmarks section is visible const locationSection = document.getElementById("landmarks"); if (locationSection.style.display === "block") { customLog('Updating Wikipedia data...'); fetchWikipediaData(lat, long); } } // Function to determine if short-range data should be updated function shouldUpdateShortRangeData() { if (neverUpdatedLocation || !lastUpdateLat || !lastUpdateLong) { return true; } const now = Date.now(); const timeSinceLastUpdate = (now - lastUpdate) / (1000 * 60); // Convert to minutes const distance = calculateDistance(lat, long, lastUpdateLat, lastUpdateLong); return distance >= UPDATE_DISTANCE_THRESHOLD || timeSinceLastUpdate >= UPDATE_TIME_THRESHOLD; } // Function to determine if long-range data should be updated function shouldUpdateLongRangeData() { // Check if we've never updated weather data if (lastWxUpdate === 0 || lastWxUpdateLat === null || lastWxUpdateLong === null) { return true; } // Check time threshold using WX_TIME_THRESHOLD constant const now = Date.now(); const timeSinceLastUpdate = now - lastWxUpdate; if (timeSinceLastUpdate >= WX_TIME_THRESHOLD * 60 * 1000) { // Convert minutes to milliseconds return true; } // Check distance threshold using WX_DISTANCE_THRESHOLD constant if (lat !== null && long !== null) { const distance = calculateDistance(lat, long, lastWxUpdateLat, lastWxUpdateLong); if (distance >= WX_DISTANCE_THRESHOLD) { // Use constant for meters return true; } } // No need to update weather data return false; } // Function to handle position updates from GPS function handlePositionUpdate(position) { lat = position.coords.latitude; long = position.coords.longitude; alt = position.coords.altitude; acc = position.coords.accuracy; speed = position.coords.speed / 0.44704; // Convert m/s to mph if (position.coords.heading) { lastKnownHeading = position.coords.heading; } // Update GPS status indicator with color gradient based on accuracy const gpsStatusElement = document.getElementById('gps-status'); if (gpsStatusElement) { if (lat === null || long === null) { // Use CSS variable for unavailable GPS gpsStatusElement.style.color = 'var(--status-unavailable)'; gpsStatusElement.title = 'GPS Unavailable'; } else { // Interpolate between yellow and green based on accuracy const maxAccuracy = 50; // Yellow threshold const minAccuracy = 1; // Green threshold // Clamp accuracy between min and max thresholds const clampedAcc = Math.min(Math.max(acc, minAccuracy), maxAccuracy); // Calculate interpolation factor (0 = yellow, 1 = green) const factor = 1 - (clampedAcc - minAccuracy) / (maxAccuracy - minAccuracy); if (factor < 0.5) { gpsStatusElement.style.color = 'var(--status-poor)'; } else { gpsStatusElement.style.color = 'var(--status-good)'; } gpsStatusElement.title = `GPS Accuracy: ${Math.round(acc)}m`; } } // Update radar display with current speed and heading if nav section is visible const navigationSection = document.getElementById("navigation"); if (navigationSection.style.display === "block") { // Update heading displays if (lastKnownHeading) { document.getElementById('heading').innerText = Math.round(lastKnownHeading) + '°'; if (weatherData) { const windSpeedMPH = Math.min((weatherData.windSpeed * 2.237), MAX_SPEED); const windDir = weatherData.windDirection; updateWindage(speed, lastKnownHeading, windSpeedMPH, windDir); } else { updateWindage(speed, lastKnownHeading, 0, 0); } } else { document.getElementById('heading').innerText = '--'; updateWindage(0, null, 0, 0); } // Update display values with proper units if (alt) { if (!settings || settings["imperial-units"]) { // Convert meters to feet document.getElementById('altitude').innerText = Math.round(alt * 3.28084); document.getElementById('altitude-unit').innerText = 'FT'; } else { document.getElementById('altitude').innerText = Math.round(alt); document.getElementById('altitude-unit').innerText = 'M'; } } else { document.getElementById('altitude').innerText = '--'; } document.getElementById('accuracy').innerText = acc ? Math.round(acc) + ' m' : '--'; // Update headwind/crosswind labels if (!settings || settings["imperial-units"]) { document.getElementById('headwind-label').innerText = document.getElementById('headwind-label').innerText.replace("(MPH)", "(MPH)"); document.querySelector('.stat-box:nth-child(4) .stat-label').innerText = document.querySelector('.stat-box:nth-child(4) .stat-label').innerText.replace("(MPH)", "(MPH)"); } else { document.getElementById('headwind-label').innerText = document.getElementById('headwind-label').innerText.replace("(MPH)", "(M/S)"); document.querySelector('.stat-box:nth-child(4) .stat-label').innerText = document.querySelector('.stat-box:nth-child(4) .stat-label').innerText.replace("(MPH)", "(M/S)"); } } // Short distance updates if (shouldUpdateShortRangeData()) { updateLocationData(lat, long); lastUpdateLat = lat; lastUpdateLong = long; lastUpdate = Date.now(); } // Long distance updates if (shouldUpdateLongRangeData()) { updateTimeZone(lat, long); fetchWeatherData(lat, long); lastWxUpdateLat = lat; lastWxUpdateLong = long; lastWxUpdate = Date.now(); } } // Function to update GPS data function updateGPS() { if (!testMode) { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(handlePositionUpdate); } else { customLog('Geolocation is not supported by this browser.'); return false; } } else { // testing handlePositionUpdate(positionSimulator.getPosition()); } return true; } // Function to throttle GPS updates function throttledUpdateGPS() { const now = Date.now(); if (now - lastGPSUpdate >= MIN_GPS_UPDATE_INTERVAL) { lastGPSUpdate = now; updateGPS(); } else { customLog('Skipping rapid GPS update'); } } // Function to start the GPS updates function startGPSUpdates() { if (!gpsIntervalId) { if (updateGPS()) { // Call immediately and check if browser supports gpsIntervalId = setInterval(throttledUpdateGPS, 1000 * LATLON_UPDATE_INTERVAL); customLog('GPS updates started'); } } } // Function to stop the GPS updates function stopGPSUpdates() { if (gpsIntervalId) { clearInterval(gpsIntervalId); gpsIntervalId = null; customLog('GPS updates paused'); } } // Check for NOTE file and display if present function updateServerNote() { fetch('NOTE') .then(response => { if (!response.ok) { throw new Error('NOTE file not found'); } return response.text(); }) .then(content => { // Sanitize the content to prevent XSS const sanitizedContent = document.createElement('div'); sanitizedContent.textContent = content; // Update the note paragraph with the sanitized content in italic const noteElement = document.getElementById('note'); noteElement.innerHTML = sanitizedContent.innerHTML; // Show the announcement section const announcementSection = document.getElementById('announcement'); if (announcementSection) { announcementSection.style.display = 'block'; } // Add notification dot to About section if it's not the current section const aboutSection = document.getElementById('about'); if (aboutSection && aboutSection.style.display !== 'block') { const aboutButton = document.querySelector('.section-button[onclick="showSection(\'about\')"]'); if (aboutButton) { aboutButton.classList.add('has-notification'); } } }) .catch(error => { customLog('No NOTE file available: ', error); }); } // Show git version from vers.php function updateVersion() { const versionElement = document.getElementById('version'); if (versionElement) { fetch('vers.php') .then(response => response.json()) .then(data => { const versionText = `${data.branch || 'unknown'}-${data.commit || 'unknown'}`; versionElement.innerHTML = versionText; }) .catch(error => { console.error('Error fetching version:', error); versionElement.innerHTML = 'Error loading version'; }); } } // Function to load an external URL in a new tab or frame window.loadExternalUrl = function (url, inFrame = false) { // Open external links in a new tab if (!inFrame) { window.open(url, '_blank'); return; } // Load external content in the right frame const rightFrame = document.getElementById('rightFrame'); // Store current content if (!rightFrame.hasAttribute('data-original-content')) { rightFrame.setAttribute('data-original-content', rightFrame.innerHTML); } // Create and load iframe rightFrame.innerHTML = ''; rightFrame.classList.add('external'); const iframe = document.createElement('iframe'); iframe.setAttribute('allow', 'geolocation; fullscreen'); iframe.src = url; rightFrame.appendChild(iframe); // Deactivate current section button const activeButton = document.querySelector('.section-button.active'); if (activeButton) { activeButton.classList.remove('active'); } } // Show a specific section and update URL - defined directly on window object window.showSection = function (sectionId) { // Log the clicked section customLog(`Showing section: ${sectionId}`); // Update URL without page reload const url = new URL(window.location); url.searchParams.set('section', sectionId); window.history.pushState({}, '', url); // First, restore original content if we're in external mode const rightFrame = document.getElementById('rightFrame'); if (rightFrame.classList.contains('external')) { rightFrame.innerHTML = rightFrame.getAttribute('data-original-content'); rightFrame.removeAttribute('data-original-content'); rightFrame.classList.remove('external'); } // If switching to news section, clear the notification dot if (sectionId === 'news') { setUserHasSeenLatestNews(true); const newsButton = document.querySelector('.section-button[onclick="showSection(\'news\')"]'); if (newsButton) { newsButton.classList.remove('has-notification'); } } // If switching to about section, clear the notification dot if (sectionId === 'about') { const aboutButton = document.querySelector('.section-button[onclick="showSection(\'about\')"]'); if (aboutButton) { aboutButton.classList.remove('has-notification'); } } // Clear "new" markers from news items when switching to a different section if (sectionId !== 'news') { const newNewsItems = document.querySelectorAll('.news-new'); newNewsItems.forEach(item => { item.classList.remove('news-new'); }); } // Then get a fresh reference to sections after DOM is restored const sections = document.querySelectorAll('.section'); sections.forEach(section => { section.style.display = 'none'; }); // Deactivate all buttons const buttons = document.querySelectorAll('.section-button'); buttons.forEach(button => { button.classList.remove('active'); }); // Show the selected section const section = document.getElementById(sectionId); if (section) { section.style.display = 'block'; if (sectionId === 'navigation' && testMode) { // In test mode, replace TeslaWaze iframe with "TESTING MODE" message const teslaWazeContainer = document.querySelector('.teslawaze-container'); if (teslaWazeContainer) { const iframe = teslaWazeContainer.querySelector('iframe'); if (iframe) { iframe.style.display = 'none'; // Check if our test mode message already exists let testModeMsg = teslaWazeContainer.querySelector('.test-mode-message'); if (!testModeMsg) { // Create and add the test mode message testModeMsg = document.createElement('div'); testModeMsg.className = 'test-mode-message'; testModeMsg.style.cssText = 'display: flex; justify-content: center; align-items: center; height: 100%; font-size: 32px; font-weight: bold;'; testModeMsg.textContent = 'TESTING MODE'; teslaWazeContainer.appendChild(testModeMsg); } else { testModeMsg.style.display = 'flex'; } } } } else if (sectionId === 'navigation') { // Normal mode - ensure iframe is visible and test mode message is hidden const teslaWazeContainer = document.querySelector('.teslawaze-container'); if (teslaWazeContainer) { const iframe = teslaWazeContainer.querySelector('iframe'); const testModeMsg = teslaWazeContainer.querySelector('.test-mode-message'); if (iframe) iframe.style.display = ''; if (testModeMsg) testModeMsg.style.display = 'none'; } } else if (sectionId === 'satellite') { // Load weather image when satellite section is shown const weatherImage = document.getElementById('weather-image'); weatherImage.src = SAT_URLS.latest; } else { // Remove weather img src to force reload when switching back const weatherImage = document.getElementById('weather-image'); if (weatherImage) { weatherImage.src = ''; } } if (sectionId === 'network') { updateNetworkInfo(); } if (sectionId === 'landmarks') { if (lat !== null && long !== null) { fetchWikipediaData(lat, long); } else { customLog('Location not available for Wikipedia data.'); } } } // Activate the clicked button const button = document.querySelector(`.section-button[onclick="showSection('${sectionId}')"]`); if (button) { button.classList.add('active'); } }; // ***** Main code ***** // Console logging customLog('*** app.js top level code ***'); // Update link click event listener document.addEventListener('click', function (e) { if (e.target.tagName === 'A' && !e.target.closest('.section-buttons')) { e.preventDefault(); const inFrame = e.target.hasAttribute('data-frame'); loadExternalUrl(e.target.href, inFrame); } }); // Handle browser back/forward buttons window.addEventListener('popstate', () => { showSection(getInitialSection()); }); // Handle page visibility changes document.addEventListener('visibilitychange', () => { if (document.hidden) { stopGPSUpdates(); pauseNewsUpdates(); pausePingTest(); } else { startGPSUpdates(); resumeNewsUpdates(); resumePingTest(); } }); // Event listeners and initialization after DOM content is loaded document.addEventListener('DOMContentLoaded', async function () { // Log customLog('DOM fully loaded and parsed...'); // Attempt login from URL parameter or cookie await attemptLogin(); updateLoginState(); // Check for NOTE file and display if present updateServerNote(); // Initialize radar display initializeRadar(); // Start location services startGPSUpdates(); // Start news updates resumeNewsUpdates(); // Begin network sensing startPingTest(); // Get version from vers.php asyncly updateVersion(); // Add event listeners for login modal document.getElementById('login-cancel').addEventListener('click', closeLoginModal); document.getElementById('login-submit').addEventListener('click', handleLogin); // Handle Enter key in login form document.getElementById('user-id').addEventListener('keyup', function (event) { if (event.key === 'Enter') { handleLogin(); } }); // Show the initial section from URL parameter const urlParams = new URLSearchParams(window.location.search); const initialSection = urlParams.get('section') || 'news'; showSection(initialSection); }); PKOS=s =s PK- ~Z Tesla/common.js SV // Imports import { settings } from './settings.js'; // Global variables const GEONAMES_USERNAME = 'birgefuller'; let locationTimeZone = browserTimeZone(); let testMode = null; // Set to true if test parameter exists // Exports export { locationTimeZone, testMode, GEONAMES_USERNAME } // Set time zone based on location export async function updateTimeZone(lat, long) { try { const response = await fetch(`https://secure.geonames.org/timezoneJSON?lat=${lat}&lng=${long}&username=${GEONAMES_USERNAME}`); const tzData = await response.json(); if (!tzData || !tzData.timezoneId) { throw new Error('Timezone not returned from server.'); } customLog('Timezone: ', tzData.timezoneId); return tzData.timezoneId; } catch (error) { console.error('Error fetching timezone: ', error); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; customLog('Fallback timezone: ', tz); return tz; } } // Custom log function that prepends the current time export function customLog(...args) { const now = new Date(); const timeString = now.toLocaleTimeString(); console.log(`[${timeString}] `, ...args); } // Update element with a change-dependent highlight effect export function highlightUpdate(id, content = null) { const element = document.getElementById(id); if (content !== null) { if (element.innerHTML === content) { return; // Exit if content is the same } element.innerHTML = content; } const highlightColor = getComputedStyle(document.documentElement).getPropertyValue('--tesla-blue').trim(); // const originalFontWeight = getComputedStyle(element).fontWeight; element.style.transition = 'color 0.5s, font-weight 0.5s'; element.style.color = highlightColor; // element.style.fontWeight = '800'; setTimeout(() => { element.style.transition = 'color 2s, font-weight 2s'; element.style.color = ''; // Reset to default color // element.style.fontWeight = ''; // Reset to original font weight }, 2000); } // Helper function to format time according to user settings export function formatTime(date, options = {}) { // Default options const defaultOptions = { hour: 'numeric', minute: '2-digit', timeZone: locationTimeZone }; // Merge provided options with defaults const timeOptions = {...defaultOptions, ...options}; // Check if 24-hour format is enabled in settings if (settings && settings['24-hour-time']) { timeOptions.hour12 = false; } return date.toLocaleTimeString('en-US', timeOptions); } // Return time zone based on browser settings function browserTimeZone() { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; customLog('Browser timezone: ', tz); return tz; } // ***** Initialization ***** // URL parameters const urlParams = new URLSearchParams(window.location.search); const testParam = urlParams.get('test'); testMode = testParam === 'true'; if (testMode) { customLog('##### TEST MODE #####'); } PKǞ@E E PK- ~Z Tesla/favicon.svg SV PK= PK- ~Z Tesla/git_info.php SV 'unknown', 'branch' => null, 'tag' => null ]; // Get commit hash from refs file $gitRefsFile = __DIR__ . '/.git/info/refs'; if (file_exists($gitRefsFile)) { $headContent = file($gitRefsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (!empty($headContent)) { $gitInfo['commit'] = substr(trim($headContent[0]), 0, 8); // Truncate to 8 digits } } // Get branch name $gitHeadFile = __DIR__ . '/.git/HEAD'; if (file_exists($gitHeadFile)) { $headContent = file($gitHeadFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (!empty($headContent)) { if (strpos($headContent[0], 'ref:') === 0) { $gitInfo['branch'] = trim(str_replace('ref: refs/heads/', '', $headContent[0])); } } } // Check for tag name $tagsOutput = []; exec('git describe --tags --exact-match 2>/dev/null', $tagsOutput); if (!empty($tagsOutput)) { $gitInfo['tag'] = $tagsOutput[0]; } return $gitInfo; }PK PK- ~Z Tesla/icons.svg SV PK*mc/ PK- ~Z Tesla/index.html SVHeadlines...
This service was created by Jonathan Birge and Alex Birge as an educational father-son project. It is in no way affiliated with Tesla, Inc. and no warranty is made for its utility.
For questions or feedback, please email admin@teslas.cloud or submit an issue to the GitHub repository. Even a quick note letting us know you're using this would be motivation to keep improving it.
Apologies to those outside of the US; right now some functionality is limited to CONUS, such as the weather satellite feed. If you're using this from another region, please let us know where (via the contact email above) and we'll see what we can do to improve support for your region with news and weather maps.
--
No headlines available
'; } } } // If there were no new items and the container is empty or only contains a spinner, show a message if (!hasNewItems && (!newsContainer.innerHTML || newsContainer.innerHTML.includes('') || newsContainer.innerHTML.includes('spinner-container'))) { newsContainer.innerHTML = 'No new headlines available
'; } } catch (error) { console.error('Error fetching news:', error); customLog('Error fetching news:', error); const newsContainer = document.getElementById('newsHeadlines'); // Make sure to remove the spinner even in case of an error if (newsContainer) { const spinnerContainer = newsContainer.querySelector('.spinner-container'); if (spinnerContainer) { spinnerContainer.remove(); } newsContainer.innerHTML = 'Error loading headlines
'; } } } // Pauses the automatic news updates window.pauseNewsUpdates = function () { if (newsUpdateInterval) { clearInterval(newsUpdateInterval); newsUpdateInterval = null; customLog('News updates paused'); } } // Resumes the automatic news updates if paused window.resumeNewsUpdates = function () { if (!newsUpdateInterval) { updateNews(); // Call immediately newsUpdateInterval = setInterval(updateNews, 60000 * NEWS_REFRESH_INTERVAL); customLog('News updates resumed'); } } PKq_ PK- ~Z Tesla/ping.php SV PKe e PK- ~Z Tesla/proxy.php SV ]*http-equiv=["\']X-Frame-Options["\'][^>]*>/i', '', $content); $content = preg_replace('/]*http-equiv=["\']Content-Security-Policy["\'][^>]*>/i', '', $content); // Add base tag $content = preg_replace('//i', 'Failed to load content (HTTP $httpCode). Please try visiting the page directly.
"; } ?>PK7 PK- ~Z Tesla/rss.php SV 'https://feeds.content.dowjones.io/public/rss/RSSWorldNews', 'nyt' => 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 'wapo' => 'https://feeds.washingtonpost.com/rss/national', 'latimes' => 'https://www.latimes.com/business/rss2.0.xml', 'bos' => 'https://www.boston.com/tag/local-news/feed', 'bloomberg' => 'https://feeds.bloomberg.com/news.rss', 'bloomberg-tech' => 'https://feeds.bloomberg.com/technology/news.rss', 'bbc' => 'http://feeds.bbci.co.uk/news/world/rss.xml', 'telegraph' => 'https://www.telegraph.co.uk/news/rss.xml', 'economist' => 'https://www.economist.com/latest/rss.xml', 'lemonde' => 'https://www.lemonde.fr/rss/une.xml', 'derspiegel' => 'https://www.spiegel.de/international/index.rss', 'notateslaapp' => 'https://www.notateslaapp.com/rss', 'teslarati' => 'https://www.teslarati.com/feed/', 'insideevs' => 'https://insideevs.com/rss/articles/all/', 'electrek' => 'https://electrek.co/feed/', 'thedrive' => 'https://www.thedrive.com/feed', 'jalopnik' => 'https://jalopnik.com/rss', 'arstechnica' => 'https://feeds.arstechnica.com/arstechnica/index', 'engadget' => 'https://www.engadget.com/rss.xml', 'gizmodo' => 'https://gizmodo.com/rss', 'theverge' => 'https://www.theverge.com/rss/index.xml', 'defensenews' => 'https://www.defensenews.com/arc/outboundfeeds/rss/?outputType=xml' ]; // Set up error logging - clear log file on each run file_put_contents('/tmp/rss-php-errors.log', ''); // Empty the file ini_set('log_errors', 1); ini_set('error_log', '/tmp/rss-php-errors.log'); // Custom error handler to capture all types of errors set_error_handler(function($errno, $errstr, $errfile, $errline) { $message = date('[Y-m-d H:i:s] ') . "Error ($errno): $errstr in $errfile on line $errline\n"; error_log($message); return false; // Let PHP handle the error as well }); // Register shutdown function to catch fatal errors register_shutdown_function(function() { $error = error_get_last(); if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { $message = date('[Y-m-d H:i:s] ') . "FATAL Error: {$error['message']} in {$error['file']} on line {$error['line']}\n"; error_log($message); } }); // Create empty log file file_put_contents($logFile, 'rss.php started...' . "\n"); // Set the content type and add headers to prevent caching header('Cache-Control: no-cache, no-store, must-revalidate'); header('Expires: 0'); header('Content-Type: application/json'); // Check if we're receiving a POST request with excluded feeds $excludedFeeds = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Get the request body $requestBody = file_get_contents('php://input'); $requestData = json_decode($requestBody, true); // Check if excludedFeeds is set in the request if (isset($requestData['excludedFeeds']) && is_array($requestData['excludedFeeds'])) { $excludedFeeds = $requestData['excludedFeeds']; logMessage("Received excluded feeds: " . implode(', ', $excludedFeeds)); } } // Check if reload parameter is set to bypass cache $forceReload = isset($_GET['reload']) || isset($_GET['n']); // Check if serial fetching is requested $useSerialFetch = isset($_GET['serial']); // Get number of stories to return $numStories = isset($_GET['n']) ? intval($_GET['n']) : $maxStories; $numStories = max(1, min($maxStories, $numStories)); // Cache logic if (!$forceReload && file_exists($cacheFile) && file_exists($cacheTimestampFile)) { $timestamp = file_get_contents($cacheTimestampFile); if ((time() - $timestamp) < $cacheDuration) { logMessage("Using cached data, last updated: " . date('Y-m-d H:i:s', $timestamp)); $useCache = true; } else { logMessage("Cache expired, fetching new data..."); $useCache = false; } } else { logMessage("Cache not found or expired, fetching new data..."); $useCache = false; } // Get items from cache or from external sources $allItems = []; if ($useCache) { $allItems = json_decode(file_get_contents($cacheFile), true); } else { if ($useSerialFetch) { // Serial fetching mode foreach ($feeds as $source => $url) { $xml = fetchRSS($url); if ($xml !== false) { $items = parseRSS($xml, $source); $allItems = array_merge($allItems, $items); } } } else { // Parallel fetching mode (default) $feedResults = fetchRSSParallel($feeds); // Process the results foreach ($feedResults as $source => $xml) { if ($xml !== false) { $items = parseRSS($xml, $source); $allItems = array_merge($allItems, $items); } } } // Sort by date, newest first usort($allItems, function($a, $b) { return $b['date'] - $a['date']; }); // Cache the results - the cache contains ALL items file_put_contents($cacheFile, json_encode($allItems)); file_put_contents($cacheTimestampFile, time()); } // Apply exclusion filters to cached data $outputItems = applyExclusionFilters($allItems, $excludedFeeds); // Limit number of stories if needed $outputItems = array_slice($outputItems, 0, $numStories); // Return filtered cached content echo json_encode($outputItems); // ***** Utility functions ***** function fetchRSSParallel($feedUrls) { $multiHandle = curl_multi_init(); $curlHandles = []; $results = []; // Initialize all curl handles and add them to multi handle foreach ($feedUrls as $source => $url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; RSS Reader/1.0)'); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_PRIVATE, $source); // Store the source as private data curl_multi_add_handle($multiHandle, $ch); $curlHandles[] = $ch; $results[$source] = false; // Initialize with false for error checking later } // Execute all queries simultaneously $active = null; do { $mrc = curl_multi_exec($multiHandle, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); while ($active && $mrc == CURLM_OK) { if (curl_multi_select($multiHandle) != -1) { do { $mrc = curl_multi_exec($multiHandle, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); } } // Process the results foreach ($curlHandles as $ch) { $source = curl_getinfo($ch, CURLINFO_PRIVATE); $content = curl_multi_getcontent($ch); if (curl_errno($ch)) { error_log("RSS Feed Error: " . curl_error($ch) . " - URL: " . $feedUrls[$source]); } else { $results[$source] = $content; } curl_multi_remove_handle($multiHandle, $ch); curl_close($ch); } curl_multi_close($multiHandle); return $results; } function fetchRSS($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; RSS Reader/1.0)'); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $response = curl_exec($ch); if (curl_errno($ch)) { error_log("RSS Feed Error: " . curl_error($ch) . " - URL: " . $url); curl_close($ch); return false; } curl_close($ch); return $response; } function parseRSS($xml, $source) { global $maxSingleSource; try { $feed = simplexml_load_string($xml); if (!$feed) { error_log("RSS Parse Error: Failed to parse XML feed from source: {$source}"); return []; } } catch (Exception $e) { error_log("RSS Parse Exception for source {$source}: " . $e->getMessage()); return []; } $items = []; // Handle different RSS feed structures $feedItems = null; if (isset($feed->channel) && isset($feed->channel->item)) { $feedItems = $feed->channel->item; // Standard RSS format } elseif (isset($feed->entry)) { $feedItems = $feed->entry; // Atom format } elseif (isset($feed->item)) { $feedItems = $feed->item; // Some non-standard RSS variants } if (!$feedItems) return []; foreach ($feedItems as $item) { // Try to find the publication date in various formats $pubDate = null; $dateString = null; // Check for different date fields if (isset($item->pubDate)) { $dateString = (string)$item->pubDate; } elseif (isset($item->published)) { $dateString = (string)$item->published; } elseif (isset($item->updated)) { $dateString = (string)$item->updated; } elseif (isset($item->children('dc', true)->date)) { $dateString = (string)$item->children('dc', true)->date; } if ($dateString) { // Try to parse the date $pubDate = strtotime($dateString); // If parsing failed, try to reformat common date patterns if ($pubDate === false) { // Try ISO 8601 format (remove milliseconds if present) if (preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $dateString)) { $cleaned = preg_replace('/\.\d+/', '', $dateString); $pubDate = strtotime($cleaned); } // Try common RFC formats with missing timezone if ($pubDate === false && preg_match('/^\w+, \d+ \w+ \d+$/', $dateString)) { $pubDate = strtotime($dateString . " 00:00:00 +0000"); } // Last resort: use current time if ($pubDate === false) { error_log("Failed to parse date: {$dateString} from source: {$source}"); $pubDate = time(); } } } else { // If no date is found, use current time (FIX: bad idea) $pubDate = time(); } // Find the link (which could be in different formats) $link = ""; if (isset($item->link)) { if (is_object($item->link) && isset($item->link['href'])) { $link = (string)$item->link['href']; // Atom format } else { $link = (string)$item->link; // RSS format } } // Find the title $title = isset($item->title) ? (string)$item->title : "No Title"; $items[] = [ 'title' => $title, 'link' => $link, 'date' => $pubDate, 'source' => $source ]; // Limit number from single source if (count($items) > $maxSingleSource) { logMessage("Limiting number of stories from source: {$source}"); break; } } logMessage("Fetched " . count($items) . " stories from source: {$source}"); return $items; } // Function to apply exclusion filters to items function applyExclusionFilters($items, $excludedFeeds) { if (empty($excludedFeeds)) { return $items; } logMessage("Filtering out excluded feeds: " . implode(', ', $excludedFeeds)); $filteredItems = array_filter($items, function($item) use ($excludedFeeds) { return !in_array($item['source'], $excludedFeeds); }); // Re-index array after filtering $filteredItems = array_values($filteredItems); logMessage("After filtering: " . count($filteredItems) . " items remain"); return $filteredItems; } // Function to write timestamped log messages to the end of the log file function logMessage($message) { global $logFile; file_put_contents($logFile, date('[Y-m-d H:i:s] ') . $message . "\n", FILE_APPEND); } PK2 2 PK- ~Z Tesla/settings.js SV // Imports import { updateNews } from './news.js'; import { customLog } from './common.js'; import { updateChartAxisColors } from './net.js'; // Global variables let isLoggedIn = false; let currentUser = null; let hashedUser = null; // Store the hashed version of the user ID let settings = {}; // Initialize settings object // Export settings object so it's accessible to other modules export { settings, currentUser, isLoggedIn }; // Default settings that will be used when no user is logged in const defaultSettings = { // General settings "auto-dark-mode": true, "dark-mode": false, "24-hour-time": false, "imperial-units": true, // News source settings "rss-wsj": true, "rss-nyt": true, "rss-wapo": true, "rss-latimes": false, "rss-bos": false, "rss-bloomberg": false, "rss-bloomberg-tech": false, "rss-bbc": true, "rss-telegraph": false, "rss-economist": false, "rss-lemonde": false, "rss-derspiegel": false, "rss-notateslaapp": true, "rss-teslarati": true, "rss-insideevs": true, "rss-electrek": false, "rss-thedrive": false, "rss-techcrunch": true, "rss-jalopnik": false, "rss-theverge": false, "rss-arstechnica": true, "rss-engadget": false, "rss-gizmodo": false, "rss-defensenews": false, }; // Function to initialize with defaults function initializeSettings() { customLog('Using default settings (no login)'); settings = { ...defaultSettings }; initializeToggleStates(); customLog('Settings initialized: ', settings); } // Turn on dark mode export function turnOnDarkMode() { document.body.classList.add('dark-mode'); document.getElementById('darkModeToggle').checked = true; toggleSetting('dark-mode', true); updateDarkModeDependants(); } // Turn off dark mode export function turnOffDarkMode() { document.body.classList.remove('dark-mode'); document.getElementById('darkModeToggle').checked = false; toggleSetting('dark-mode', false); updateDarkModeDependants(); } // Update things that depend on dark mode function updateDarkModeDependants() { updateChartAxisColors(); } // Function to hash a user ID using SHA-256 async function hashUserId(userId) { // Use the SubtleCrypto API to create a SHA-256 hash const encoder = new TextEncoder(); const data = encoder.encode(userId); const hashBuffer = await crypto.subtle.digest('SHA-256', data); // Convert the hash buffer to a hexadecimal string const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // Return only the first 16 characters (64 bits) of the hash return hashHex.substring(0, 16); } // Function to validate user ID, and if valid, set environment variables async function validateUserId(userId) { // Check for minimum length (9 characters) if (userId.length < 9) { return { valid: false, message: 'User ID must be at least 9 characters long.' }; } // Check for standard characters (letters, numbers, underscore, hyphen) const validFormat = /^[a-zA-Z0-9_-]+$/; if (!validFormat.test(userId)) { return { valid: false, message: 'User ID can only contain letters, numbers, underscore, and hyphen.' }; } try { // Hash the user ID before sending to the server const hashedId = await hashUserId(userId); // Use HEAD request to check if the user exists const response = await fetch(`settings.php/${encodeURIComponent(hashedId)}`, { method: 'HEAD' }); if (response.status === 404) { // User doesn't exist, create default settings const created = await createNewUser(userId, hashedId); if (!created) { return { valid: false, message: 'Failed to create new user.' }; } } else if (!response.ok) { return { valid: false, message: 'Error checking user existence.' }; } // User exists, set environment variables isLoggedIn = true; currentUser = userId; hashedUser = hashedId; customLog('User ID validated and logged in: ', userId, '(hashed: ', hashedId, ')'); return { valid: true }; } catch (error) { console.error('Error validating user ID:', error); return { valid: false, message: 'Network error during validation.' }; } } // Pull all settings for a given user from REST server, creating user account with defaults if needed async function fetchSettings(userId) { const validation = await validateUserId(userId); // returns true if valid user existed or was created if (validation.valid) { try { // Fetch settings using RESTful API customLog('Fetching settings for user: ', hashedUser); const response = await fetch(`settings.php/${encodeURIComponent(hashedUser)}`, { method: 'GET' }); if (response.ok) { // Load settings settings = await response.json(); customLog('Settings loaded: ', settings); // Update UI updateLoginState(); // Activate the settings section button document.getElementById('settings-section').classList.remove('hidden'); // Save user ID in a cookie setCookie('userid', userId); // Initialize toggle states based on settings initializeToggleStates(); // Handle dark mode if (settings['dark-mode']) { turnOnDarkMode(); } else { turnOffDarkMode(); } } else { console.error('Error fetching settings: ', response.statusText); } } catch (error) { console.error('Error fetching settings: ', error); } } else { console.error('Invalid user ID: ', validation.message); } } // Function to create a new user with default settings async function createNewUser(userId, hashedId = null) { try { // If hashedId wasn't provided, generate it if (!hashedId) { hashedId = await hashUserId(userId); } const response = await fetch(`settings.php/${encodeURIComponent(hashedId)}`, { method: 'POST' }); if (response.ok) { customLog('Created new user with default settings:', userId); isLoggedIn = true; currentUser = userId; hashedUser = hashedId; // Go through all values in the settings object and set them to the server for (const [key, value] of Object.entries(settings)) { await toggleSetting(key, value); } return true; } else { customLog('Failed to create new user:', userId); return false; } } catch (error) { console.error('Error creating new user:', error); return false; } } // Function to attempt login from cookie export async function attemptLogin() { const urlParams = new URLSearchParams(window.location.search); let userId = urlParams.get('user'); // If no userid in URL, try to get from cookie if (!userId) { userId = getCookie('userid'); customLog('Checking for userid cookie:', userId ? 'found' : 'not found'); } if (userId) { await fetchSettings(userId); // Fetch settings for the user } else { initializeSettings(); // No user ID found, use default settings } } // Update login/logout button visibility based on state export function updateLoginState() { const loginButton = document.getElementById('login-button'); const logoutButton = document.getElementById('logout-button'); if (isLoggedIn) { loginButton.classList.add('hidden'); logoutButton.classList.remove('hidden'); logoutButton.textContent = `Logout ${currentUser}`; } else { loginButton.classList.remove('hidden'); logoutButton.classList.add('hidden'); logoutButton.textContent = 'Logout'; } } // Function to toggle a setting (updates both local cache and server) export async function toggleSetting(key, value) { // Handle local settings settings[key] = value; // Update toggle state visually updateToggleVisualState(key, value); customLog(`Setting "${key}" updated to ${value} (local)`); // Update server if logged in if (isLoggedIn && currentUser) { try { // Update the local settings cache with boolean value settings[key] = value; // Update toggle state visually updateToggleVisualState(key, value); // Update the server with the boolean value using the RESTful API const response = await fetch(`settings.php/${encodeURIComponent(hashedUser)}/${encodeURIComponent(key)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: value }) }); if (response.ok) { customLog(`Setting "${key}" updated to ${value} (server)`); } else { customLog(`Failed to update setting "${key}" on server`); } } catch (error) { customLog('Error toggling setting:', error); } } } // Update visual state of a toggle function updateToggleVisualState(key, value) { const settingItem = document.querySelector(`.settings-toggle-item[data-setting="${key}"]`); if (settingItem) { const toggle = settingItem.querySelector('input[type="checkbox"]'); if (toggle) { toggle.checked = value; } } } // Initialize all toggle states based on settings function initializeToggleStates() { // Find all settings toggle items with data-setting attributes const toggleItems = document.querySelectorAll('.settings-toggle-item[data-setting]'); toggleItems.forEach(item => { const key = item.dataset.setting; if (!key) return; const value = settings[key] !== undefined ? settings[key] : false; // Default to false if not set const toggle = item.querySelector('input[type="checkbox"]'); if (toggle) { toggle.checked = value; // customLog(`Initialized toggle for ${key}: ${value}`); } }); } // Cookie management functions function setCookie(name, value, days = 36500) { // Default to ~100 years (forever) const d = new Date(); d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); const expires = "expires=" + d.toUTCString(); document.cookie = name + "=" + value + ";" + expires + ";path=/"; customLog(`Cookie set: ${name}=${value}, expires: ${d.toUTCString()}`); } function getCookie(name) { const cookieName = name + "="; const decodedCookie = decodeURIComponent(document.cookie); const cookieArray = decodedCookie.split(';'); // customLog(`All cookies: ${document.cookie}`); for (let i = 0; i < cookieArray.length; i++) { let cookie = cookieArray[i]; while (cookie.charAt(0) === ' ') { cookie = cookie.substring(1); } if (cookie.indexOf(cookieName) === 0) { const value = cookie.substring(cookieName.length, cookie.length); customLog(`Cookie found: ${name}=${value}`); return value; } } customLog(`Cookie not found: ${name}`); return ""; } function deleteCookie(name) { document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; customLog(`Cookie deleted: ${name}`); } // Function to show the login modal window.showLoginModal = function () { const modal = document.getElementById('login-modal'); modal.style.display = 'flex'; document.getElementById('user-id').focus(); document.getElementById('login-error').textContent = ''; // Clear previous errors } // Function to hide the login modal window.closeLoginModal = function () { document.getElementById('login-modal').style.display = 'none'; } // Function to handle logout window.handleLogout = function () { isLoggedIn = false; currentUser = null; hashedUser = null; // Update UI updateLoginState(); // Hide settings section document.getElementById('settings-section').classList.add('hidden'); // If currently in settings section, redirect to news const sections = document.querySelectorAll('.section'); const settingsSection = document.getElementById('settings'); if (settingsSection.style.display === 'block') { showSection('news'); } // Remove the userid cookie deleteCookie('userid'); } // Function to handle login from dialog window.handleLogin = async function () { const userId = document.getElementById('user-id').value.trim(); closeLoginModal(); fetchSettings(userId); updateNews(true); // Update news feed after login } // Manually swap dark/light mode window.toggleMode = function () { toggleSetting('auto-dark-mode', false); document.body.classList.toggle('dark-mode'); const darkMode = document.body.classList.contains('dark-mode'); document.getElementById('darkModeToggle').checked = darkMode; toggleSetting('dark-mode', darkMode); updateDarkModeDependants(); } // Function called by the toggle UI elements window.toggleSettingFrom = function(element) { customLog('Toggle setting from UI element.'); const settingItem = element.closest('.settings-toggle-item'); if (settingItem && settingItem.dataset.setting) { const key = settingItem.dataset.setting; const value = element.checked; toggleSetting(key, value); // If the setting is RSS-related, update the news feed if (key.startsWith('rss-')) { updateNews(true); } } } PK 7 7 PK- ~Z Tesla/settings.php SV date('Y-m-d H:i:s'), "version" => "1" ]; // Load .env variables from a JSON file $envFilePath = __DIR__ . '/.env'; if (file_exists($envFilePath)) { $envContent = file_get_contents($envFilePath); $envVariables = json_decode($envContent, true); if (json_last_error() === JSON_ERROR_NONE) { foreach ($envVariables as $key => $value) { $_ENV[$key] = $value; } } else { error_log("Failed to parse .env file: " . json_last_error_msg()); } } else { error_log(".env file not found at $envFilePath"); } // SQL database configuration $dbName = $_ENV['SQL_DB_NAME'] ?? 'teslacloud'; $dbHost = $_ENV['SQL_HOST'] ?? null; $dbUser = $_ENV['SQL_USER'] ?? null; $dbPass = $_ENV['SQL_PASS'] ?? null; $dbPort = $_ENV['SQL_PORT'] ?? '3306'; // Establish database connection if (!$dbHost || !$dbName || !$dbUser) { logMessage("Missing required database configuration", "ERROR"); http_response_code(500); echo json_encode(['error' => 'Database configuration missing']); exit; } // Connect to database try { $dsn = "mysql:host={$dbHost};port={$dbPort};dbname={$dbName};charset=utf8mb4"; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; $dbConnection = new PDO($dsn, $dbUser, $dbPass, $options); // Check if the required table exists, create it if not $tableCheck = $dbConnection->query("SHOW TABLES LIKE 'user_settings'"); if ($tableCheck->rowCount() == 0) { $sql = "CREATE TABLE user_settings ( user_id VARCHAR(255) NOT NULL, setting_key VARCHAR({$maxKeyLength}) NOT NULL, setting_value TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (user_id, setting_key) )"; $dbConnection->exec($sql); } } catch (PDOException $e) { $errorMessage = "Database connection failed: " . $e->getMessage(); logMessage($errorMessage, "ERROR"); http_response_code(500); echo json_encode(['error' => 'Database connection failed']); exit; } // Parse the request URI to extract user and key $requestUri = $_SERVER['REQUEST_URI']; $uriParts = explode('/', trim(parse_url($requestUri, PHP_URL_PATH), '/')); // Determine which parts of the URL contain our parameters $userId = null; $key = null; // Check if we have enough parts to contain a user ID if (count($uriParts) > 1) { $scriptName = basename(__FILE__); // Should be settings.php $scriptPos = array_search($scriptName, $uriParts); if ($scriptPos !== false && isset($uriParts[$scriptPos + 1])) { $userId = $uriParts[$scriptPos + 1]; // Check if we also have a key if (isset($uriParts[$scriptPos + 2])) { $key = $uriParts[$scriptPos + 2]; } } } // Handle the request based on method $method = $_SERVER['REQUEST_METHOD']; switch ($method) { case 'POST': // POST request - create a new user settings resource if (!$userId || !validateUserId($userId)) { logMessage("Invalid or missing user ID: $userId", "ERROR"); http_response_code(400); exit; } // Check if user settings already exist if (userSettingsExist($userId)) { logMessage("User settings already exist for $userId", "WARNING"); http_response_code(409); // Conflict exit; } // Save the default settings if (saveUserSettings($userId, $defaultSettings)) { logMessage("User settings created successfully for $userId"); http_response_code(201); // Created echo json_encode([ 'success' => true, 'userId' => $userId, 'message' => 'User settings created with default values', 'settings' => $defaultSettings ]); } else { logMessage("Failed to create user settings for $userId", "ERROR"); http_response_code(500); } break; case 'HEAD': // HEAD request - check if user settings exist without returning content if (!$userId || !validateUserId($userId)) { logMessage("Invalid or missing user ID: $userId", "ERROR"); http_response_code(400); exit; } if (!userSettingsExist($userId)) { logMessage("User settings not found for $userId", "WARNING"); http_response_code(404); exit; } // Resource exists, return 200 OK (with no body) http_response_code(200); exit; case 'GET': // GET request - retrieve settings for a user if (!$userId || !validateUserId($userId)) { logMessage("Invalid or missing user ID: $userId", "ERROR"); http_response_code(400); echo json_encode(['error' => 'Invalid or missing user ID in URL path']); exit; } // Check if the user settings exist if (!userSettingsExist($userId)) { logMessage("User settings not found for $userId", "WARNING"); http_response_code(404); exit; } $settings = loadUserSettings($userId); if ($key) { // Return settings where the key starts with the given prefix $filteredSettings = array_filter($settings, function ($k) use ($key) { return strpos($k, $key) === 0; // Check if the key starts with the prefix }, ARRAY_FILTER_USE_KEY); if (!empty($filteredSettings)) { echo json_encode($filteredSettings); } else { logMessage("No settings found with prefix '$key' for user $userId", "WARNING"); http_response_code(404); } } else { // Return all settings if no key is provided echo json_encode($settings); } break; case 'PUT': // PUT request - update or create a setting if (!$userId || !validateUserId($userId)) { logMessage("Invalid or missing user ID: $userId", "ERROR"); http_response_code(400); exit; } if (!$key) { logMessage("Missing key in URL path", "ERROR"); http_response_code(400); exit; } // Validate key length if (strlen($key) > $maxKeyLength) { logMessage("Key too long: $key", "ERROR"); http_response_code(400); exit; } // Parse the input $requestData = json_decode(file_get_contents('php://input'), true); if (!isset($requestData['value'])) { logMessage("Missing value parameter", "ERROR"); http_response_code(400); exit; } $value = $requestData['value']; // Validate value length if (strlen(json_encode($value)) > $maxValueLength) { logMessage("Value too long", "ERROR"); http_response_code(400); echo json_encode(['error' => 'Value too long']); exit; } // Convert value to boolean if it's a boolean string or already boolean if (is_string($value)) { if ($value === 'true') { $value = true; } elseif ($value === 'false') { $value = false; } // Keep other string values as is - this handles non-boolean settings } // Load current settings $settings = loadUserSettings($userId); // Track if this is a creation operation $isCreatingResource = !userSettingsExist($userId); // Update the setting $settings[$key] = $value; // Save updated settings if (saveUserSettings($userId, $settings)) { // Return 201 Created if this was a new resource, otherwise 200 OK if ($isCreatingResource) { http_response_code(201); echo json_encode(['success' => true, 'key' => $key, 'created' => true]); } else { echo json_encode(['success' => true, 'key' => $key]); } } else { logMessage("Failed to save setting $key for user $userId", "ERROR"); http_response_code(500); } break; default: logMessage("Invalid method: $method", "ERROR"); // Method not allowed http_response_code(405); break; } // ***** Utility Functions ***** // Helper function to validate user ID function validateUserId($userId) { $isValid = (strlen($userId) >= 9) && preg_match('/^[a-zA-Z0-9_-]+$/', $userId); logMessage("Validating user ID: $userId - " . ($isValid ? "Valid" : "Invalid")); return $isValid; } // Helper function to check if user settings exist function userSettingsExist($userId) { global $dbConnection; try { logMessage("Checking if user $userId exists in database"); $stmt = $dbConnection->prepare("SELECT 1 FROM user_settings WHERE user_id = ? LIMIT 1"); $stmt->execute([$userId]); $exists = $stmt->rowCount() > 0; logMessage("Database check result: " . ($exists ? "User exists" : "User does not exist")); return $exists; } catch (PDOException $e) { $errorMsg = "Database error checking if user settings exist: " . $e->getMessage(); logMessage($errorMsg, "ERROR"); throw $e; // Rethrow the exception after logging } } // Helper function to load user settings function loadUserSettings($userId) { global $dbConnection, $defaultSettings; logMessage("Loading settings for user $userId"); try { logMessage("Loading settings from database for $userId"); $settings = []; $stmt = $dbConnection->prepare("SELECT setting_key, setting_value FROM user_settings WHERE user_id = ?"); $stmt->execute([$userId]); $rowCount = $stmt->rowCount(); logMessage("Found $rowCount setting(s) in database for user $userId"); if ($rowCount > 0) { while ($row = $stmt->fetch()) { // Parse stored JSON value or use as is if parsing fails $value = json_decode($row['setting_value'], true); $settings[$row['setting_key']] = ($value !== null) ? $value : $row['setting_value']; logMessage("Loaded setting {$row['setting_key']} from database with value type: " . gettype($value)); } return $settings; } // If no settings in DB but this was called, create default settings logMessage("No settings found in database, saving defaults", "WARNING"); saveUserSettings($userId, $defaultSettings); return $defaultSettings; } catch (PDOException $e) { $errorMsg = "Database error loading user settings: " . $e->getMessage(); logMessage($errorMsg, "ERROR"); throw $e; // Rethrow the exception after logging } } // Helper function to save user settings function saveUserSettings($userId, $settings) { global $dbConnection; logMessage("Saving settings for user $userId - " . count($settings) . " setting(s)"); try { logMessage("Saving settings to database"); $dbConnection->beginTransaction(); // Delete existing settings for this user $deleteStmt = $dbConnection->prepare("DELETE FROM user_settings WHERE user_id = ?"); $deleteStmt->execute([$userId]); $deletedCount = $deleteStmt->rowCount(); logMessage("Deleted $deletedCount existing setting(s) for user $userId"); // Insert new settings $insertStmt = $dbConnection->prepare("INSERT INTO user_settings (user_id, setting_key, setting_value) VALUES (?, ?, ?)"); $insertCount = 0; foreach ($settings as $key => $value) { $jsonValue = json_encode($value); $insertStmt->execute([$userId, $key, $jsonValue]); $insertCount++; logMessage("Inserted setting: $key with value type: " . gettype($value)); } $dbConnection->commit(); logMessage("Database transaction committed successfully - inserted $insertCount setting(s)"); return true; } catch (PDOException $e) { $dbConnection->rollBack(); $errorMsg = "Database error saving user settings: " . $e->getMessage(); logMessage($errorMsg, "ERROR"); return false; } } // Simple logging function function logMessage($message, $level = 'INFO') { global $logFile; $timestamp = date('Y-m-d H:i:s'); $formattedMessage = "[$timestamp] [$level] $message" . PHP_EOL; file_put_contents($logFile, $formattedMessage, FILE_APPEND); } PKy~K5 K5 PK- ~Z Tesla/styles.css SV /* Variables */ :root { --bg-color: #efefef; --text-color: #777777; --active-section-bg: white; --separator-color: #cccccc; --button-bg: #dddddd; --button-text: #333333; --info-bg: #d0d0d0; --tesla-blue: #0077ff; --tesla-red: #ff0000; --button-radius: 13px; --weather-switch-slider: #ffffff; --weather-warning-color: #ff7700; --status-poor: #ff9100; /* Yellow for poor connection */ --status-good: #00d000; /* Green for good connection */ --status-unavailable: #888888; /* Gray for unavailable/bad connection */ --heading-font-size: 17pt; --heading-font-weight: 650; } body.dark-mode { --bg-color: #1d1d1d; --text-color: #777777; --active-section-bg: #333333; --separator-color: #444444; --button-bg: #333333; --button-text: #d6d6d6; --info-bg: #302f39; --weather-switch-slider: #222222; --weather-warning-color: #ff9900; --status-poor: #ffcd27; /* Yellow for poor connection */ --status-good: #5eff19; /* Green for good connection */ --status-unavailable: #888888; /* Gray for unavailable/bad connection */ } /* Basic HTML Elements */ * { font-family: "Inter", sans-serif !important; } body { background-color: var(--bg-color); color: var(--text-color); font-size: 16pt; padding: 0; text-align: left; word-spacing: normal; font-weight: 500; font-optical-sizing: auto; font-variant-ligatures: auto; } a { color: var(--tesla-blue); text-decoration: none; } h1 { color: var(--button-text); font-size: 19pt; margin-bottom: 15px; text-align: left; margin-top: 0px; font-weight: 250; } h2 { color: var(--button-text); font-size: var(--heading-font-size); font-weight: var(--heading-font-weight); letter-spacing: 0.01em; margin-bottom: 9px; margin-top: 28px; text-align: left; } p { margin-top: 12px; margin-bottom: 0px; text-align: justify } ul { padding-left: 20px; margin-top: 0px; margin-bottom: 0px; } li { margin: 9px 0; line-height: 1.4; padding-left: 0px; margin-left: 0px; } hr { border: 0; border-top: 2px solid var(--separator-color); margin-top: 28px; margin-bottom: 28px; } /* About */ #about { font-variant-ligatures: all; } .announcement p { background-color: #03a8f433; color: var(--button-text); border-radius: var(--button-radius); padding: 18px; font-style: none; font-weight: 600; text-align: left; } /* Navigation */ .hidden { display: none; } .logout-button { color: var(--button-text); background-color: var(--button-bg); border-radius: var(--button-radius); font-size: 16pt; font-weight: 600; padding: 16px 20px; border: none; cursor: pointer; text-align: center; margin-top: 15px; width: auto; transition: background-color 0.3s; } .logout-button:hover { background-color: var(--active-section-bg); } .sections { padding-bottom: 30px; /* Add padding to the bottom of all sections */ } .section { display: none; padding-bottom: 30px; /* Add padding to the bottom of each section */ max-width: 940px; } .section-buttons { position: sticky; top: 0; padding-right: 20px; display: flex; flex-direction: column; margin-top: 0px; width: 250px; min-width: 250px; } .section-button { color: var(--text-color); border-radius: var(--button-radius); background-color: transparent; font-size: 19pt; font-weight: 675; letter-spacing: 0.02em; padding: 16px 20px; border: none; cursor: pointer; text-align: left; margin-bottom: 5px; display: flex; align-items: center; transition: background-color 0.3s, color 0.3s; } .button-icon { width: 20px; height: 20px; margin-right: 12px; stroke: currentColor; flex-shrink: 0; } .section-button.active { background-color: var(--active-section-bg); color: var(--button-text); } .section-button:hover { background-color: var(--active-section-bg); } /* Notification dot for news section */ .section-button.has-notification { position: relative; } .section-button.has-notification::after { content: ""; position: absolute; top: 50%; /* Center vertically */ left: 5px; /* Moved further left from 10px */ transform: translateY(-50%); /* Perfect vertical centering */ width: 10px; height: 10px; background-color: var(--tesla-blue); border-radius: 50%; animation: pulse 2s 1; /* Changed from 3 pulses to 1 */ animation-fill-mode: forwards; /* Keep final state */ } /* Weather warning notification dot */ .section-button.weather-warning { position: relative; } .section-button.weather-warning::after { content: ""; position: absolute; top: 50%; left: 5px; transform: translateY(-50%); width: 10px; height: 10px; background-color: var(--weather-warning-color); border-radius: 50%; animation: pulse 2s 1; animation-fill-mode: forwards; } @keyframes pulse { 0% { transform: translateY(-50%) scale(0.6); opacity: 0.6; } 50% { transform: translateY(-50%) scale(1.5); /* Larger pulse */ opacity: 1; } 100% { transform: translateY(-50%) scale(1); opacity: 1; } } /* Layout */ .frame-container { display: flex; height: 100vh; width: 100vw; position: fixed; top: 0; left: 0; } .left-frame { background-color: var(--bg-color); width: 290px; min-width: 290px; flex-shrink: 0; height: 100%; overflow-y: auto; padding: 15px 15px 20px 20px; } .right-frame { flex-grow: 1; height: 100%; overflow-y: auto; padding: 20px 3.5% 20px 15px; scrollbar-gutter: stable; scrollbar-width: thin; -ms-overflow-style: -ms-autohiding-scrollbar; } .right-frame.external { padding: 0; overflow: hidden; } .right-frame iframe { width: 100%; height: 100%; border: none; display: block; } /* Link Button Lists */ .button-list { list-style: none; padding: 0; margin-top: 0; display: grid; grid-template-columns: repeat(auto-fill, 280px); gap: 16px; justify-content: start; max-width: 1200px; } .button-list li { margin: 0; } .button-list a { color: var(--button-text); background-color: var(--button-bg); border-radius: var(--button-radius); display: flex; padding-top: 24px; padding-bottom: 24px; padding-left: 9px; padding-right: 9px; text-decoration: none; transition: background-color 0.3s; width: auto; font-weight: 600; height: 40px; /* Adjust the height as needed */ align-items: center; justify-content: center; gap: 10px; text-align: center; /* Add this line to center the text */ } .button-list a img { height: 32px; width: auto; } /* Enhanced Image Visibility Classes */ .img-adaptive, .img-adaptive-light-invert, .img-adaptive-dark-invert { /* Base styling shared by all image adaptive classes */ filter: grayscale(100%) contrast(1.5); transition: filter 0.3s; } /* Invert colors only in light mode */ body:not(.dark-mode) .img-adaptive, body:not(.dark-mode) .img-adaptive-light-invert { filter: invert(100%); } /* Invert colors only in dark mode */ body.dark-mode .img-adaptive-dark-invert { filter: invert(100%); } /* Make white areas in images transparent */ .img-white-transparent { /* Remove the inverting filters */ mix-blend-mode: multiply; /* Keep this to make white transparent */ transition: filter 0.3s; } /* Add specific dark mode handling */ body.dark-mode .img-white-transparent { mix-blend-mode: screen; /* In dark mode, use screen blend mode to make white transparent */ filter: invert(100%) brightness(125%); /* Invert colors and slightly increase brightness for better visibility */ } /* Indicator container and dark-mode toggle */ .control-container { position: fixed; top: -5px; /* Changed from 10px to -5px to move above screen edge */ right: 20px; display: flex; align-items: center; background-color: var(--bg-color); /* Semi-transparent light mode background */ border-radius: 5px; padding: 8px 12px; z-index: 100; /* Ensure it stays above other content */ opacity: 0.75; } .toggle-label { color: var(--text-color); font-size: 15pt; font-weight: 600; margin-right: 10px; } .toggle-switch { position: relative; display: inline-block; width: 48px; /* Reduced by 20% from 60px */ height: 27px; /* Reduced by 20% from 34px */ } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .toggle-slider:before { position: absolute; content: ""; height: 21px; /* Reduced by 20% from 26px */ width: 21px; /* Reduced by 20% from 26px */ left: 3px; /* Adjusted from 4px */ bottom: 3px; /* Adjusted from 4px */ background-color: white; transition: .4s; border-radius: 50%; } input:checked + .toggle-slider { background-color: #2196F3; } input:focus + .toggle-slider { box-shadow: 0 0 1px #2196F3; } input:checked + .toggle-slider:before { transform: translateX(21px); /* Reduced by 20% from 26px */ } /* Data Display */ .data-info, .data-info-column { row-gap: 18px; max-width: 1024px; width: 100%; /* Ensure it spans full width */ } .data-info { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); column-gap: 12px; } .data-info-column { margin-top: 16px; display: flex; flex-direction: column; } .data-info-column .data-item { margin-right: 0; /* Ensure no extra margin */ width: 100%; /* Ensure items span full width */ } .data-item { color: var(--button-text); font-weight: var(--heading-font-weight); font-size: var(--heading-font-size); display: block; margin-right: 0; padding-top: 0px; } .data-item:last-child { margin-right: 0; margin-bottom: 0; } .data-item h2 { color: var(--text-color); margin-top: 0px; margin-bottom: 4px; } /* Stats Display */ .nav-container { display: flex; gap: 50px; margin: 30px 0px -25px 0px; align-items: flex-start; } .nav-stats { display: grid; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, auto); gap: 10px 0 0 0; width: 400px; } .stat-box { display: flex; flex-direction: column; align-items: left; min-width: 200px; height: 125px; /* fixed height added */ } .value-container { display: flex; align-items: center; /* Vertically align items in container */ } .stat-value { color: var(--button-text); font-size: 48pt; font-weight: 600; display: inline; font-variant-numeric: tabular-nums; } .stat-unit { color: var(--text-color); font-size: 24pt; font-weight: 650; font-family: sans-serif; display: inline; margin-left: 5px; /* Add some spacing between value and unit */ } .stat-label { color: var(--text-color); font-size: 13pt; font-weight: 650; margin-top: 0px; display: block; /* Ensure it's on its own line */ clear: both; /* Clear the float to ensure it appears below */ } /* Dashboard Display */ .radar-container { position: relative; padding-top: 30px; /* Added padding to push radar down */ margin-right: 30px; } .radar-title { position: absolute; top: 0px; left: 24px; text-align: center; margin: 0; } #radarDisplay { border-radius: 50%; background-color: rgba(0, 0, 0, 0.1); } .dark-mode #radarDisplay { background-color: rgba(255, 255, 255, 0.1); } /* Waze frame */ .teslawaze-container { height: calc(100vh - 380px); min-height: 280px; margin-top: 20px; width: 100%; margin-bottom: 0; /* Ensure no bottom margin */ position: relative; /* Add positioning context */ flex-grow: 1; /* Allow it to grow to fill available space */ } #teslawaze { border-radius: var(--button-radius); width: 100%; height: 100%; border: none; position: absolute; /* Position absolutely within container */ top: 0; left: 0; bottom: 0; right: 0; } /* News Elements */ .news-headlines { margin: 24px 0; max-width: 900px; } .news-item { background-color: var(--button-bg); border-radius: var(--button-radius); margin-bottom: 10px; /* Increased from 9px */ padding: 10px 10px 10px 54px; /* Increased left padding from 46px to 54px */ transition: background-color 0.3s; border: none; width: 100%; text-align: left; cursor: pointer; position: relative; /* Added for absolute positioning of the favicon */ } /* New item indicator */ .news-new { position: relative; } .news-new::after { content: ""; position: absolute; top: 50%; right: 15px; transform: translateY(-50%); width: 8px; height: 8px; background-color: var(--tesla-blue); border-radius: 50%; animation: news-pulse 1.5s 1; animation-fill-mode: forwards; } @keyframes news-pulse { 0% { opacity: 0.7; transform: translateY(-50%) scale(0.7); } 50% { opacity: 1; transform: translateY(-50%) scale(1.3); } 100% { opacity: 1; transform: translateY(-50%) scale(1); } } .news-item:hover { background-color: var(--button-bg); /* Keep the hover color consistent */ } .news-item:last-child { margin-bottom: 0; } .news-source { color: var(--tesla-blue); font-weight: 675; margin-right: 10px; font-size: 11.5pt; } .news-time, .news-date { color: var(--text-color); font-size: 12.5pt; margin-left: 5px; display: inline-block; font-weight: 350; } .news-title { color: var(--button-text); display: block; margin-top: 4px; font-size: 16pt; font-weight: 550; } .news-favicon { position: absolute; /* Position absolutely within the news-item */ left: 10px; /* Position on the left side with some padding */ top: 50%; /* Center vertically */ transform: translateY(-50%); /* Perfect vertical centering */ width: 32px; /* Slightly increased width */ height: 32px; /* Slightly increased height */ border-radius: 4px; /* Optional: adds a slight rounding to the icons */ opacity: 0.8; } /* Network Status Indicator */ .network-status { margin-right: 22px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; transition: color 0.3s ease; } /* Spinner styling */ .spinner-container { display: flex; justify-content: center; align-items: center; width: 100%; height: 200px; /* Provide adequate height for the spinner */ } .spinner { width: 50px; height: 50px; border: 5px solid rgba(0, 119, 255, 0.2); /* Tesla blue with opacity */ border-radius: 50%; border-top-color: var(--tesla-blue); animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } .network-status svg { width: 24px; height: 24px; } .network-status .network-bar { transition: fill 0.3s ease; } .network-status.unavailable .network-bar { fill: var(--status-unavailable); } .network-status.poor .bar-1 { fill: var(--status-poor); } .network-status.poor .bar-2, .network-status.poor .bar-3, .network-status.poor .bar-4 { fill: var(--status-unavailable); } .network-status.fair .bar-1, .network-status.fair .bar-2 { fill: var(--status-poor); } .network-status.fair .bar-3, .network-status.fair .bar-4 { fill: var(--status-unavailable); } .network-status.good .bar-1, .network-status.good .bar-2, .network-status.good .bar-3 { fill: var(--status-good); } .network-status.good .bar-4 { fill: var(--status-unavailable); } .network-status.excellent .bar-1, .network-status.excellent .bar-2, .network-status.excellent .bar-3, .network-status.excellent .bar-4 { fill: var(--status-good); } /* GPS Status Indicator */ .gps-status { margin-right: 22px; /* Good separation from the Dark Mode text */ width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; transition: color 0.3s ease; } .gps-status.unavailable { color: var(--status-unavailable); /* Using the shared variable */ } .gps-status.poor { color: var(--status-poor); /* Using the shared variable */ } .gps-status.good { color: var(--status-good); /* Using the shared variable */ } /* Login Styles */ .login-button { color: var(--button-text); background-color: var(--button-bg); border-radius: var(--button-radius); font-size: 16pt; font-weight: 600; padding: 16px 20px; border: none; cursor: pointer; text-align: center; margin-top: 15px; width: 100%; transition: background-color 0.3s; } .login-button:hover { background-color: var(--active-section-bg); } /* Modal Login Dialog */ .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); justify-content: center; align-items: center; } .modal-content { background-color: var(--bg-color); border-radius: var(--button-radius); padding: 25px; width: 400px; max-width: 90%; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); } .login-form { margin-top: 20px; } .login-form label { display: block; margin-bottom: 10px; color: var(--button-text); font-weight: 500; } .login-form input { width: 100%; padding: 12px 15px; margin-bottom: 20px; border: 2px solid var(--separator-color); border-radius: 8px; font-size: 16px; background-color: var(--active-section-bg); color: var(--button-text); box-sizing: border-box; /* Add this to fix the width issue */ } .button-container { display: flex; justify-content: flex-end; gap: 10px; } .modal-button { padding: 12px 25px; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: background-color 0.3s; } .modal-button.cancel { background-color: var(--button-bg); color: var(--button-text); } .modal-button.submit { background-color: var(--tesla-blue); color: white; } .error-message { color: var(--tesla-red); margin-bottom: 20px; font-size: 14px; min-height: 20px; } /* Settings Section Styles */ .settings-controls { margin-top: 12px; } .settings-toggle-item { display: flex; align-items: center; justify-content: space-between; background-color: var(--button-bg); border-radius: var(--button-radius); padding: 15px; margin-bottom: 9px; width: auto; font-weight: 550; cursor: pointer; /* Add cursor pointer to indicate it's clickable */ } .settings-toggle-item label { flex-grow: 1; /* Make label take up all available space */ cursor: pointer; /* Add cursor pointer to label */ } .settings-toggle-item input { opacity: 0; width: 0; height: 0; position: absolute; } .settings-toggle-item span.settings-toggle-slider { position: relative; display: inline-block; width: 60px; height: 34px; flex-shrink: 0; /* Prevent slider from shrinking */ } .settings-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .settings-toggle-slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .settings-toggle-slider { background-color: var(--tesla-blue); } input:focus + .settings-toggle-slider { box-shadow: 0 0 1px var(--tesla-blue); } input:checked + .settings-toggle-slider:before { transform: translateX(26px); } /* News Source Grid */ .news-source-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 9px; width: 100%; max-width: 1200px; } .news-toggle-item { padding: 12px 15px; margin-bottom: 0; max-width: none; width: auto; } .news-toggle-item { font-size: 15pt; } PKгP P PK- ~Z Tesla/vers.php SV $gitInfo['commit'], 'branch' => $gitInfo['branch'], 'tag' => $gitInfo['tag'] ]); PKY# PK- ~Z Tesla/wx.css SV :root { --sky-clear-top: #e6f7ff; --sky-clear-bottom: #a8d0f0; --sky-cloudy-top: #eaeaea; --sky-cloudy-bottom: #c5d5e2; --sky-rainy-top: #d6e4f0; --sky-rainy-bottom: #b6b9bd; --sky-storm-top: #c9d6e2; --sky-storm-bottom: #7b8a9a; --sky-snow-top: #f0f5fb; --sky-snow-bottom: #d8dfe6; } body.dark-mode { --sky-clear-top: #0a1020; --sky-clear-bottom: #441a45; --sky-cloudy-top: #262729; --sky-cloudy-bottom: #142236; --sky-rainy-top: #262729; --sky-rainy-bottom: #2a3040; --sky-storm-top: #080e18; --sky-storm-bottom: #292b2e; --sky-snow-top: #3e3e3b; --sky-snow-bottom: #706e6b; } /* Weather Elements */ .weather-switch-container { display: flex; justify-content: flex-start; margin: 20px 0; } .weather-switch { display: flex; background-color: var(--button-bg); border-radius: var(--button-radius); padding: 4px; gap: 4px; position: relative; width: 420px; --slider-position: 0; } .weather-switch button { flex: 1; padding: 16px 24px; border: none; border-radius: calc(var(--button-radius) - 4px); background: transparent; color: var(--text-color); cursor: pointer; font-family: 'Inter', sans-serif; font-size: 16pt; font-weight: 600; position: relative; z-index: 1; transition: color 0.3s ease; } .weather-switch button.active { color: var(--button-text); } .weather-switch::after { content: ''; position: absolute; top: 4px; left: 4px; bottom: 4px; width: calc((100% - 8px) / 3); background-color: var(--weather-switch-slider); border-radius: calc(var(--button-radius) - 4px); transform: translateX(calc(var(--slider-position) * 100%)); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .weather-image { width: 95%; max-width: 1080px; opacity: 0; display: none; border-radius: var(--button-radius); transition: opacity 0.3s ease; } .weather-image.active { display: block; opacity: 1; } /* Weather Forecast */ .forecast-container { display: none; /* Initially hidden */ justify-content: space-between; gap: 15px; margin: 20px 0; max-width: 1024px; } /* Loading spinner */ .forecast-loading { display: flex; /* Initially visible */ justify-content: center; align-items: center; height: 200px; margin: 20px 0; } .spinner { width: 50px; height: 50px; border: 5px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top-color: var(--tesla-blue); animation: spin 1s linear infinite; } body.dark-mode .spinner { border-color: rgba(255, 255, 255, 0.1); border-top-color: var(--tesla-blue); } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .forecast-day { flex: 1; background: linear-gradient(to bottom, var(--sky-gradient-top), var(--sky-gradient-bottom)); border-radius: var(--button-radius); padding: 15px; text-align: center; min-width: 100px; position: relative; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: background 0.3s ease; } /* Weather condition-specific gradients */ .forecast-day.clear { background: linear-gradient(to bottom, var(--sky-clear-top), var(--sky-clear-bottom)); } .forecast-day.clouds { background: linear-gradient(to bottom, var(--sky-cloudy-top), var(--sky-cloudy-bottom)); } .forecast-day.rain { background: linear-gradient(to bottom, var(--sky-rainy-top), var(--sky-rainy-bottom)); } .forecast-day.storm, .forecast-day.thunderstorm { background: linear-gradient(to bottom, var(--sky-storm-top), var(--sky-storm-bottom)); } .forecast-day.snow { background: linear-gradient(to bottom, var(--sky-snow-top), var(--sky-snow-bottom)); } /* Invert snow icon in dark mode */ body.dark-mode .forecast-day.snow img.forecast-icon { filter: invert(1); } /* Invert clear icon in dark mode and make it grayscale */ body.dark-mode .forecast-day.clear img.forecast-icon { filter: invert(1); filter: grayscale(1); } /* Invert rain icon in dark mode and make it grayscale */ body.dark-mode .forecast-day.rain img.forecast-icon { filter: invert(1); filter: grayscale(1); } .forecast-alert { position: absolute; top: 3px; left: 5px; color: #ff9900; font-size: 18px; font-family: "Inter", sans-serif !important; font-weight: 700; } .forecast-date { font-size: 13pt; font-weight: 600; margin-bottom: 8px; color: var(--text-color); } .forecast-icon { width: 48px; height: 48px; margin: 8px 0; } .forecast-temp { font-size: 14pt; font-weight: 750; margin: 8px 0; color: var(--button-text); } .forecast-desc { font-family: "Inter", sans-serif; font-size: 13pt; font-style: italic; font-weight: 600; color: var(--button-text); } .forecast-popup { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--active-section-bg); border-radius: var(--button-radius); padding: 20px; z-index: 1000; max-width: 90%; width: 600px; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .forecast-popup.show { display: block; } .forecast-popup-close { position: absolute; right: 10px; top: 10px; background: rgba(0, 0, 0, 0.1); border: none; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 22px; line-height: 1; font-family: monospace; padding-bottom: 4px; color: #666; transition: all 0.2s ease; } .forecast-popup-close:hover { background: rgba(0, 0, 0, 0.2); color: #333; } .hourly-forecast { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px; } .hourly-item { background: linear-gradient(to bottom, var(--sky-gradient-top), var(--sky-gradient-bottom)); padding: 15px; border-radius: var(--button-radius); text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: background 0.3s ease; } /* Weather condition-specific gradients for hourly items */ .hourly-item.clear { background: linear-gradient(to bottom, var(--sky-clear-top), var(--sky-clear-bottom)); } .hourly-item.clouds { background: linear-gradient(to bottom, var(--sky-cloudy-top), var(--sky-cloudy-bottom)); } .hourly-item.rain { background: linear-gradient(to bottom, var(--sky-rainy-top), var(--sky-rainy-bottom)); } .hourly-item.storm, .hourly-item.thunderstorm { background: linear-gradient(to bottom, var(--sky-storm-top), var(--sky-storm-bottom)); } .hourly-item.snow { background: linear-gradient(to bottom, var(--sky-snow-top), var(--sky-snow-bottom)); } .hourly-time { font-weight: 500; font-size: 14pt;; color: var(--button-text); margin-bottom: 5px; } .hourly-temp { color: var(--button-text); font-size: 14pt; font-weight: 750; margin: 5px 0; } .hourly-desc { color: var(--button-text); font-size: 13pt; font-style: italic; font-weight: 600; } .station-name { font-size: 11pt; text-transform: uppercase; margin-left: 10px; color: var(--text-color); } PK"` PK- ~Z Tesla/wx.js SV // Import required functions from app.js import { customLog, formatTime, highlightUpdate } from './common.js'; import { settings, turnOffDarkMode, turnOnDarkMode } from './settings.js'; // Constants const OPENWX_API_KEY = '6a1b1bcb03b5718a9b3a2b108ce3293d'; // Global variables let weatherData = null; let sunrise = null; let sunset = null; let forecastData = null; let moonPhaseData = null; // Export these variables for use in other modules export { sunrise, sunset, weatherData }; // Helper function to convert temperature based on user settings function formatTemperature(tempF) { if (!settings || settings["imperial-units"]) { return Math.round(tempF) + "°"; } else { // Convert F to C: (F - 32) * 5/9 return Math.round((tempF - 32) * 5/9) + "°"; } } // Helper function to convert wind speed based on user settings function formatWindSpeed(speedMS) { if (!settings || settings["imperial-units"]) { // Convert m/s to mph return Math.round(speedMS * 2.237) + " MPH"; } else { // Keep as m/s return Math.round(speedMS) + " m/s"; } } // Convert numerical phase to human-readable name function getMoonPhaseName(phase) { if (phase === 0 || phase === 1) return "New Moon"; if (phase < 0.25) return "Waxing Crescent"; if (phase === 0.25) return "First Quarter"; if (phase < 0.5) return "Waxing Gibbous"; if (phase === 0.5) return "Full Moon"; if (phase < 0.75) return "Waning Gibbous"; if (phase === 0.75) return "Last Quarter"; return "Waning Crescent"; } // Fetches weather data and updates the display export function fetchWeatherData(lat, long, silentLoad = true) { customLog('Fetching weather data...' + (silentLoad ? ' (background load)' : '')); // Show loading spinner, hide forecast container - only if not silent loading const forecastContainer = document.getElementById('forecast-container'); const loadingSpinner = document.getElementById('forecast-loading'); if (!silentLoad) { if (forecastContainer) forecastContainer.style.display = 'none'; if (loadingSpinner) loadingSpinner.style.display = 'flex'; } // Fetch and update sunrise/sunset data fetchSunData(lat, long); // Fetch and update weather data Promise.all([ fetch(`https://secure.geonames.org/findNearByWeatherJSON?lat=${lat}&lng=${long}&username=birgefuller`), fetch(`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${long}&appid=${OPENWX_API_KEY}&units=imperial`) ]) .then(([currentResponse, forecastResponse]) => Promise.all([ currentResponse.json(), forecastResponse ? forecastResponse.json() : null ])) .then(([currentDataResponse, forecastDataResponse]) => { if (currentDataResponse) { // check to see if wind direction is NaN if (isNaN(currentDataResponse.weatherObservation.windDirection)) { currentDataResponse.weatherObservation.windDirection = null; currentDataResponse.weatherObservation.windSpeed = null; } else { // take the reciprocal of the wind direction to get the wind vector currentDataResponse.weatherObservation.windDirection = (currentDataResponse.weatherObservation.windDirection + 180) % 360; } weatherData = currentDataResponse.weatherObservation; updateWeatherDisplay(); } if (forecastDataResponse) { forecastData = forecastDataResponse.list; updateForecastDisplay(); } // Call updateAQI after forecast is obtained updateAQI(lat, long, OPENWX_API_KEY); // Hide spinner and show forecast when data is loaded - only if not silent loading if (forecastContainer) forecastContainer.style.display = 'flex'; if (loadingSpinner) loadingSpinner.style.display = 'none'; }) .catch(error => { console.error('Error fetching weather data: ', error); customLog('Error fetching weather data: ', error); // In case of error, hide spinner and show error message - only if not silent loading if (!silentLoad) { if (loadingSpinner) loadingSpinner.style.display = 'none'; if (forecastContainer) { forecastContainer.style.display = 'flex'; // Show error message in the forecast container forecastContainer.innerHTML = ' '; } } }); } // Fetches sunrise and moon phase data function fetchSunData(lat, long) { // Log the lat/long for debugging customLog(`Fetching sun data for lat: ${lat}, long: ${long}`); Promise.all([ fetch(`https://api.sunrise-sunset.org/json?lat=${lat}&lng=${long}&formatted=0`), fetch(`https://api.farmsense.net/v1/moonphases/?d=${Math.floor(Date.now() / 1000)}`) ]) .then(([sunResponse, moonResponse]) => Promise.all([sunResponse.json(), moonResponse.json()])) .then(([sunData, moonData]) => { sunrise = sunData.results.sunrise; sunset = sunData.results.sunset; moonPhaseData = moonData[0]; updateSunMoonDisplay(); autoDarkMode(lat, long); }) .catch(error => { console.error('Error fetching sun/moon data: ', error); customLog('Error fetching sun/moon data: ', error); }); } // Updates the display for sunrise, sunset, and moon phase function updateSunMoonDisplay() { const sunriseTime = formatTime(new Date(sunrise), { timeZoneName: 'short' }); highlightUpdate('sunrise', sunriseTime); const sunsetTime = formatTime(new Date(sunset), { timeZoneName: 'short' }); highlightUpdate('sunset', sunsetTime); if (moonPhaseData) { const moonPhase = getMoonPhaseName(moonPhaseData.Phase); highlightUpdate('moonphase', moonPhase); } } // Automatically toggles dark mode based on sunrise and sunset times export function autoDarkMode(lat, long) { customLog('Auto dark mode check for coordinates: ', lat, long); if (!sunrise || !sunset) { customLog('autoDarkMode: sunrise/sunset data not available.'); return; } if (settings && settings['auto-dark-mode']) { const now = new Date(); const currentTime = now.getTime(); const sunriseTime = new Date(sunrise).getTime(); const sunsetTime = new Date(sunset).getTime(); if (currentTime >= sunsetTime || currentTime < sunriseTime) { customLog('Applying dark mode based on sunset...'); turnOnDarkMode(); } else { customLog('Applying light mode based on sunrise...'); turnOffDarkMode(); } } else { customLog('Auto dark mode disabled or coordinates not available.'); } } // Checks if a day has hazardous weather conditions function dayHasHazards(forecastList) { const hazardConditions = ['Rain', 'Snow', 'Sleet', 'Hail', 'Thunderstorm', 'Storm', 'Drizzle']; return forecastList.weather.some(w => hazardConditions.some(condition => w.main.includes(condition) || w.description.includes(condition.toLowerCase()) ) ); } // Updates the forecast display with daily data function updateForecastDisplay() { const forecastDays = document.querySelectorAll('.forecast-day'); const dailyData = extractDailyForecast(forecastData); dailyData.forEach((day, index) => { if (index < forecastDays.length) { const date = new Date(day.dt * 1000); const dayElement = forecastDays[index]; // Clear previous content and classes dayElement.innerHTML = ''; dayElement.className = 'forecast-day'; // Add weather condition class based on main weather condition const weatherCondition = day.weather[0].main.toLowerCase(); dayElement.classList.add(weatherCondition); // Add alert symbol if hazards detected if (dayHasHazards(day)) { const alert = document.createElement('div'); alert.className = 'forecast-alert'; alert.innerHTML = '⚠️'; dayElement.appendChild(alert); } // Add the rest of the forecast content dayElement.innerHTML += `