apiVersion: batch/v1 kind: CronJob metadata: name: weather-report namespace: ai-agents labels: app: weather-report spec: schedule: "0 11 * * *" timeZone: "America/New_York" # K8s 1.27+ supports timeZone natively concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: weather-reporter image: node:18-alpine env: - name: GROUP_URL valueFrom: configMapKeyRef: name: whatsapp-config key: default-group-url - name: PERSONAL_NUMBER valueFrom: configMapKeyRef: name: whatsapp-config key: personal-number command: - "node" - "-e" - | const https = require('https'); const http = require('http'); const ZIP = '16046'; const GATEWAY = 'http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/send-message'; const CONFIG_API = 'http://hq-dashboard.ai-agents.svc.cluster.local/api/config/whatsapp'; const PERSONAL = process.env.PERSONAL_NUMBER; function fetchConfig() { return new Promise((resolve) => { http.get(CONFIG_API, (res) => { let d = ''; res.on('data', chunk => d += chunk); res.on('end', () => { try { const parsed = JSON.parse(d); if (parsed.success && parsed.data && parsed.data['default-group-url']) { resolve(parsed.data['default-group-url']); return; } } catch (e) {} resolve(process.env.GROUP_URL); }); }).on('error', () => resolve(process.env.GROUP_URL)); }); } function weatherEmoji(desc) { const d = desc.toLowerCase(); if (d.includes('sunny') || d.includes('clear')) return '☀️'; if (d.includes('partly cloudy')) return '⛅'; if (d.includes('cloud') || d.includes('overcast')) return '☁️'; if (d.includes('thunder') || d.includes('storm')) return '⛈️'; if (d.includes('rain') || d.includes('drizzle')) return '🌧️'; if (d.includes('snow') || d.includes('blizzard')) return '❄️'; if (d.includes('fog') || d.includes('mist')) return '🌫️'; if (d.includes('sleet') || d.includes('ice')) return '🌨️'; return '🌤️'; } function uvLabel(uv) { const u = parseInt(uv); if (u <= 2) return 'Low'; if (u <= 5) return 'Moderate'; if (u <= 7) return 'High'; if (u <= 10) return 'Very High'; return 'Extreme'; } function windLabel(mph) { const m = parseInt(mph); if (m < 5) return 'Calm'; if (m < 15) return 'Breezy'; if (m < 25) return 'Windy'; if (m < 40) return 'Very Windy'; return 'Dangerous'; } const url = `https://wttr.in/${ZIP}?format=j1`; https.get(url, { headers: { 'User-Agent': 'WeatherBot/1.0' } }, (res) => { let raw = ''; res.on('data', d => raw += d); res.on('end', () => { try { const data = JSON.parse(raw); const cur = data.current_condition[0]; const area = data.nearest_area[0]; const today = data.weather[0]; const tmrw = data.weather[1]; const city = area.areaName[0].value; const state = area.region[0].value; const desc = cur.weatherDesc[0].value; const emoji = weatherEmoji(desc); const now = new Date(); const etTime = now.toLocaleString('en-US', { timeZone: 'America/New_York', hour: 'numeric', minute: '2-digit', hour12: true }); const etDate = now.toLocaleDateString('en-US', { timeZone: 'America/New_York', weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); // Hourly snapshots (morning 9am, afternoon 3pm, evening 9pm) const hours = today.hourly || []; const morn = hours[2]; // index 2 = 600 (6am slot) or pick closest const aftn = hours[4]; // 1200 const even = hours[6]; // 1800 const tmrwDesc = tmrw ? tmrw.hourly[4]?.weatherDesc[0]?.value : ''; const tmrwEmoji = tmrw ? weatherEmoji(tmrwDesc) : ''; const msg = [ `${emoji} *Good Morning — Daily Weather Briefing*`, `📍 *${city}, ${state}* (ZIP: ${ZIP})`, `📅 ${etDate} • ${etTime} ET`, ``, `━━━━━━ 🌡️ *Current Conditions* ━━━━━━`, `${emoji} ${desc}`, `🌡️ *Temp:* ${cur.temp_F}°F (feels like ${cur.FeelsLikeF}°F)`, `💧 *Humidity:* ${cur.humidity}%`, `💨 *Wind:* ${cur.windspeedMiles} mph ${cur.winddir16Point} (${windLabel(cur.windspeedMiles)})`, `👁️ *Visibility:* ${cur.visibility} mi`, `☀️ *UV Index:* ${cur.uvIndex} — ${uvLabel(cur.uvIndex)}`, ``, `━━━━━━ 📊 *Today's Forecast* ━━━━━━`, `🔺 High: ${today.maxtempF}°F 🔻 Low: ${today.mintempF}°F`, `🌅 Sunrise: ${today.astronomy[0].sunrise} 🌇 Sunset: ${today.astronomy[0].sunset}`, morn ? `🕗 Morning (9am): ${morn.tempF}°F — ${morn.weatherDesc[0].value} 🌧️ ${morn.chanceofrain}%` : '', aftn ? `🕒 Afternoon (3pm): ${aftn.tempF}°F — ${aftn.weatherDesc[0].value} 🌧️ ${aftn.chanceofrain}%` : '', even ? `🕘 Evening (9pm): ${even.tempF}°F — ${even.weatherDesc[0].value} 🌧️ ${even.chanceofrain}%` : '', ``, tmrw ? `━━━━━━ 🔮 *Tomorrow* ━━━━━━` : '', tmrw ? `${tmrwEmoji} ${tmrwDesc}` : '', tmrw ? `🔺 High: ${tmrw.maxtempF}°F 🔻 Low: ${tmrw.mintempF}°F 🌧️ Rain: ${tmrw.hourly[4]?.chanceofrain || 0}%` : '', ``, `_Powered by Agentic OS • hq.applaude.net_ 🏠`, ].filter(l => l !== null && l !== undefined).join('\n'); // Helper: send msg to one recipient function sendTo(to, label) { return new Promise((resolve) => { const body = JSON.stringify({ to, message: msg }); const gwUrl = new URL(GATEWAY); const opts = { hostname: gwUrl.hostname, port: parseInt(gwUrl.port) || 80, path: gwUrl.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }; const req = http.request(opts, (r) => { let resp = ''; r.on('data', d => resp += d); r.on('end', () => { try { const parsed = JSON.parse(resp); if (parsed.success) { console.log(`[WeatherBot] ✅ Sent to ${label}. MsgID: ${parsed.messageId}`); } else { console.error(`[WeatherBot] ❌ ${label} error: ${parsed.error}`); } } catch(e) { console.error(`[WeatherBot] ${label} parse error:`, e.message); } resolve(); }); }); req.on('error', e => { console.error(`[WeatherBot] ${label} request failed:`, e.message); resolve(); }); req.write(body); req.end(); }); } // Send to group then personal number fetchConfig().then(GROUP => { sendTo(GROUP, 'WhatsApp Group') .then(() => sendTo(PERSONAL, 'Personal (' + PERSONAL + ')')) .catch(e => { console.error('[WeatherBot] Fatal:', e.message); process.exit(1); }); }); } catch (e) { console.error('[WeatherBot] Parse error:', e.message); process.exit(1); } }); }).on('error', e => { console.error('[WeatherBot] Fetch error:', e.message); process.exit(1); }); resources: requests: cpu: 50m memory: 64Mi limits: cpu: 200m memory: 128Mi