208 lines
10 KiB
YAML
208 lines
10 KiB
YAML
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
|