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:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

229
status/js/weather.js Normal file
View 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 &ldquo;City, Country&rdquo;</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">&deg;F / mph</span></label>
<label class="weather-unit-option"><input type="radio" name="weather-unit-radio" value="metric"><span class="weather-unit-card">&deg;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}&current=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);
})();