Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
229
status/js/weather.js
Normal file
229
status/js/weather.js
Normal file
@@ -0,0 +1,229 @@
|
||||
// ========== WEATHER WIDGET ==========
|
||||
(function() {
|
||||
// Inject modal HTML
|
||||
injectModal('weather-modal', `<div id="weather-modal" class="weather-modal"><div class="weather-modal-content"><h3>Weather Settings</h3>
|
||||
<label for="weather-location-input">Location:</label>
|
||||
<input type="text" id="weather-location-input" placeholder="City name or ZIP (e.g., Hamburg, 90210)" maxlength="100">
|
||||
<div style="font-size: 0.78rem; color: var(--muted); margin-top: 4px;">Enter a city name, postal code, or “City, Country”</div>
|
||||
<div style="margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border);">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--fg);">Units</label>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<label class="weather-unit-option"><input type="radio" name="weather-unit-radio" value="imperial"><span class="weather-unit-card">°F / mph</span></label>
|
||||
<label class="weather-unit-option"><input type="radio" name="weather-unit-radio" value="metric"><span class="weather-unit-card">°C / km/h</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weather-modal-buttons"><button id="weather-cancel">Cancel</button><button id="weather-save">Save</button></div></div></div>`);
|
||||
|
||||
/* Weather Widget — powered by Open-Meteo (free, no API key) */
|
||||
const LOCATION_KEY = 'weather-location';
|
||||
const LEGACY_KEY = 'weather-zip';
|
||||
const GEO_CACHE_KEY = 'weather-geo';
|
||||
const UNIT_KEY = 'weather-unit';
|
||||
|
||||
// Migrate from old ZIP-only key
|
||||
if (!safeGet(LOCATION_KEY) && safeGet(LEGACY_KEY)) {
|
||||
safeSet(LOCATION_KEY, safeGet(LEGACY_KEY));
|
||||
}
|
||||
|
||||
function getUnit() {
|
||||
return safeGet(UNIT_KEY) || 'imperial';
|
||||
}
|
||||
|
||||
function getWeatherElements() {
|
||||
return {
|
||||
icon: document.querySelector('.weather-icon'),
|
||||
temp: document.querySelector('.weather-temp'),
|
||||
condition: document.querySelector('.weather-condition'),
|
||||
location: document.querySelector('.weather-location'),
|
||||
wind: document.querySelector('.weather-wind')
|
||||
};
|
||||
}
|
||||
|
||||
// WMO Weather Code mapping
|
||||
const wmoDescriptions = {
|
||||
0: 'Clear sky', 1: 'Mainly clear', 2: 'Partly cloudy', 3: 'Overcast',
|
||||
45: 'Fog', 48: 'Rime fog',
|
||||
51: 'Light drizzle', 53: 'Drizzle', 55: 'Dense drizzle',
|
||||
56: 'Freezing drizzle', 57: 'Dense freezing drizzle',
|
||||
61: 'Light rain', 63: 'Moderate rain', 65: 'Heavy rain',
|
||||
66: 'Light freezing rain', 67: 'Heavy freezing rain',
|
||||
71: 'Light snow', 73: 'Moderate snow', 75: 'Heavy snow', 77: 'Snow grains',
|
||||
80: 'Light showers', 81: 'Moderate showers', 82: 'Violent showers',
|
||||
85: 'Light snow showers', 86: 'Heavy snow showers',
|
||||
95: 'Thunderstorm', 96: 'Thunderstorm with hail', 99: 'Severe thunderstorm'
|
||||
};
|
||||
|
||||
const wmoIcons = {
|
||||
0: '☀️', 1: '🌤️', 2: '⛅', 3: '☁️',
|
||||
45: '🌫️', 48: '🌫️',
|
||||
51: '🌦️', 53: '🌦️', 55: '🌦️',
|
||||
56: '🌨️', 57: '🌨️',
|
||||
61: '🌦️', 63: '🌧️', 65: '🌧️',
|
||||
66: '🌨️', 67: '🌨️',
|
||||
71: '🌨️', 73: '❄️', 75: '❄️', 77: '❄️',
|
||||
80: '🌦️', 81: '🌧️', 82: '🌧️',
|
||||
85: '🌨️', 86: '❄️',
|
||||
95: '⛈️', 96: '⛈️', 99: '⛈️'
|
||||
};
|
||||
|
||||
// Wind direction from degrees
|
||||
const windDirs = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'];
|
||||
function degToDir(deg) {
|
||||
return windDirs[Math.round(deg / 22.5) % 16];
|
||||
}
|
||||
|
||||
// Geocode location via Open-Meteo, with localStorage cache
|
||||
async function geocodeLocation(query) {
|
||||
const cached = safeGet(GEO_CACHE_KEY);
|
||||
if (cached) {
|
||||
try {
|
||||
const geo = JSON.parse(cached);
|
||||
if (geo.query === query) return geo;
|
||||
} catch {}
|
||||
}
|
||||
const resp = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1&language=en&format=json`);
|
||||
if (!resp.ok) throw new Error('Geocoding failed');
|
||||
const data = await resp.json();
|
||||
if (!data.results || !data.results.length) throw new Error('Location not found');
|
||||
const r = data.results[0];
|
||||
const geo = {
|
||||
query,
|
||||
lat: r.latitude,
|
||||
lon: r.longitude,
|
||||
city: r.name,
|
||||
state: r.admin1 || '',
|
||||
country: r.country || '',
|
||||
countryCode: r.country_code || ''
|
||||
};
|
||||
safeSet(GEO_CACHE_KEY, JSON.stringify(geo));
|
||||
return geo;
|
||||
}
|
||||
|
||||
function formatLocation(geo) {
|
||||
// For US locations show "City, State", for others show "City, Country"
|
||||
if (geo.countryCode === 'US' && geo.state) {
|
||||
return `${geo.city}, ${geo.state}`;
|
||||
}
|
||||
if (geo.country) {
|
||||
return `${geo.city}, ${geo.country}`;
|
||||
}
|
||||
return geo.city;
|
||||
}
|
||||
|
||||
async function fetchWeather(location) {
|
||||
try {
|
||||
const geo = await geocodeLocation(location);
|
||||
const unit = getUnit();
|
||||
const tempUnit = unit === 'metric' ? 'celsius' : 'fahrenheit';
|
||||
const windUnit = unit === 'metric' ? 'kmh' : 'mph';
|
||||
const url = `https://api.open-meteo.com/v1/forecast?latitude=${geo.lat}&longitude=${geo.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${tempUnit}&wind_speed_unit=${windUnit}`;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error('Weather fetch failed');
|
||||
const data = await resp.json();
|
||||
const c = data.current;
|
||||
const code = c.weather_code;
|
||||
return {
|
||||
temp: Math.round(c.temperature_2m),
|
||||
condition: wmoDescriptions[code] || 'Unknown',
|
||||
icon: wmoIcons[code] || '🌤️',
|
||||
locationStr: formatLocation(geo),
|
||||
windSpeed: Math.round(c.wind_speed_10m),
|
||||
windDir: degToDir(c.wind_direction_10m),
|
||||
unit
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Weather fetch failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWeather() {
|
||||
const weatherWidget = getWeatherElements();
|
||||
if (!weatherWidget.icon || !weatherWidget.temp || !weatherWidget.condition || !weatherWidget.location || !weatherWidget.wind) {
|
||||
console.warn('Weather widget elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const location = safeGet(LOCATION_KEY);
|
||||
if (!location) {
|
||||
weatherWidget.location.textContent = 'Set Location';
|
||||
weatherWidget.temp.textContent = '--\u00B0';
|
||||
weatherWidget.condition.textContent = 'Click \u2699\uFE0F to configure';
|
||||
weatherWidget.wind.textContent = '--';
|
||||
weatherWidget.icon.innerHTML = '<span class="weather-emoji">\uD83C\uDF24\uFE0F</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const weather = await fetchWeather(location);
|
||||
if (weather) {
|
||||
const tempSuffix = weather.unit === 'metric' ? '\u00B0C' : '\u00B0F';
|
||||
const windLabel = weather.unit === 'metric' ? 'km/h' : 'mph';
|
||||
weatherWidget.location.textContent = weather.locationStr;
|
||||
weatherWidget.temp.textContent = `${weather.temp}${tempSuffix}`;
|
||||
weatherWidget.condition.textContent = weather.condition;
|
||||
weatherWidget.wind.textContent = `Wind: ${weather.windSpeed} ${windLabel} ${weather.windDir}`;
|
||||
weatherWidget.icon.innerHTML = `<span class="weather-emoji">${weather.icon}</span>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Weather update error:', error);
|
||||
weatherWidget.location.textContent = 'Weather Error';
|
||||
weatherWidget.temp.textContent = 'Error';
|
||||
weatherWidget.condition.textContent = 'Failed to load';
|
||||
weatherWidget.wind.textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
// Weather settings modal
|
||||
const modal = document.getElementById('weather-modal');
|
||||
const locationInput = document.getElementById('weather-location-input');
|
||||
|
||||
document.getElementById('weather-settings')?.addEventListener('click', () => {
|
||||
locationInput.value = safeGet(LOCATION_KEY) || '';
|
||||
// Set unit radio
|
||||
const unit = getUnit();
|
||||
const radio = modal.querySelector(`input[name="weather-unit-radio"][value="${unit}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
modal.classList.add('show');
|
||||
locationInput.focus();
|
||||
});
|
||||
|
||||
document.getElementById('weather-cancel')?.addEventListener('click', () => {
|
||||
modal.classList.remove('show');
|
||||
});
|
||||
|
||||
document.getElementById('weather-save')?.addEventListener('click', () => {
|
||||
const loc = locationInput.value.trim();
|
||||
if (loc) {
|
||||
// Clear geo cache when location changes so it re-geocodes
|
||||
const oldLoc = safeGet(LOCATION_KEY);
|
||||
if (oldLoc !== loc) safeSet(GEO_CACHE_KEY, '');
|
||||
safeSet(LOCATION_KEY, loc);
|
||||
// Save unit preference
|
||||
const unitRadio = modal.querySelector('input[name="weather-unit-radio"]:checked');
|
||||
const newUnit = unitRadio ? unitRadio.value : 'imperial';
|
||||
const oldUnit = getUnit();
|
||||
safeSet(UNIT_KEY, newUnit);
|
||||
// Clear geo cache if unit changed (need fresh weather data)
|
||||
if (oldUnit !== newUnit) safeSet(GEO_CACHE_KEY, '');
|
||||
modal.classList.remove('show');
|
||||
updateWeather();
|
||||
} else {
|
||||
showNotification('Please enter a location (e.g., Hamburg, London, 90210)', 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
wireModal(modal);
|
||||
|
||||
// Close modal on Escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.classList.contains('show')) {
|
||||
modal.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize weather
|
||||
updateWeather();
|
||||
// Update weather every 10 minutes
|
||||
setInterval(updateWeather, DC.POLL.WEATHER);
|
||||
})();
|
||||
Reference in New Issue
Block a user