diff --git a/agents/k8s/weather-cronjob.yaml b/agents/k8s/weather-cronjob.yaml new file mode 100644 index 0000000..3404799 --- /dev/null +++ b/agents/k8s/weather-cronjob.yaml @@ -0,0 +1,164 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: weather-report + namespace: ai-agents + labels: + app: weather-report +spec: + # 7:00 AM ET = 11:00 UTC (EDT offset UTC-4) + 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 + 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 GROUP = 'https://chat.whatsapp.com/0Txfg7iZBbvIuvTbTqqgAX'; + + 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'); + + const body = JSON.stringify({ to: GROUP, 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', () => { + const parsed = JSON.parse(resp); + if (parsed.success) { + console.log(`[WeatherBot] ✅ Weather report sent to group. MsgID: ${parsed.messageId}`); + } else { + console.error(`[WeatherBot] ❌ Gateway error: ${parsed.error}`); + process.exit(1); + } + }); + }); + req.on('error', e => { console.error('[WeatherBot] Request failed:', e.message); process.exit(1); }); + req.write(body); + req.end(); + + } 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 diff --git a/interface/hq-dashboard/app/api/send-whatsapp/route.js b/interface/hq-dashboard/app/api/send-whatsapp/route.js new file mode 100644 index 0000000..590c733 --- /dev/null +++ b/interface/hq-dashboard/app/api/send-whatsapp/route.js @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; + +const GATEWAY = process.env.WHATSAPP_GATEWAY_URL || + 'http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/send-message'; +const DEFAULT_GROUP = process.env.WHATSAPP_GROUP || + 'https://chat.whatsapp.com/0Txfg7iZBbvIuvTbTqqgAX'; + +export async function POST(request) { + try { + const { message, to } = await request.json(); + if (!message) { + return NextResponse.json({ error: 'message is required' }, { status: 400 }); + } + + const res = await fetch(GATEWAY, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ to: to || DEFAULT_GROUP, message }), + }); + + const data = await res.json(); + + if (!res.ok || !data.success) { + return NextResponse.json({ error: data.error || 'Gateway error' }, { status: 502 }); + } + + return NextResponse.json({ success: true, messageId: data.messageId, recipient: data.recipient }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/interface/hq-dashboard/app/api/weather/route.js b/interface/hq-dashboard/app/api/weather/route.js new file mode 100644 index 0000000..28928df --- /dev/null +++ b/interface/hq-dashboard/app/api/weather/route.js @@ -0,0 +1,77 @@ +import { NextResponse } from 'next/server'; + +const ZIP = process.env.WEATHER_ZIP || '16046'; + +export async function GET(request) { + const { searchParams } = new URL(request.url); + const location = searchParams.get('location') || ZIP; + + try { + const res = await fetch(`https://wttr.in/${encodeURIComponent(location)}?format=j1`, { + headers: { 'User-Agent': 'HQ-Dashboard/1.0' }, + next: { revalidate: 900 } // cache 15 min + }); + + if (!res.ok) throw new Error(`wttr.in returned ${res.status}`); + const data = await res.json(); + + const current = data.current_condition[0]; + const area = data.nearest_area[0]; + const today = data.weather[0]; + const tomorrow = data.weather[1]; + + const city = area.areaName[0].value; + const state = area.region[0].value; + + // Hourly forecast for today (3 slots: morning, afternoon, evening) + const hourly = (today.hourly || []).filter((_, i) => [2, 4, 6].includes(i)).map(h => ({ + time: formatHour(h.time), + tempF: h.tempF, + desc: h.weatherDesc[0].value, + chanceRain: h.chanceofrain, + })); + + return NextResponse.json({ + location: `${city}, ${state}`, + zip: location, + current: { + tempF: current.temp_F, + feelsLikeF: current.FeelsLikeF, + humidity: current.humidity, + windMph: current.windspeedMiles, + windDir: current.winddir16Point, + visibility: current.visibility, + uvIndex: current.uvIndex, + desc: current.weatherDesc[0].value, + cloudcover: current.cloudcover, + pressure: current.pressure, + }, + today: { + maxF: today.maxtempF, + minF: today.mintempF, + sunrise: today.astronomy[0].sunrise, + sunset: today.astronomy[0].sunset, + desc: today.hourly[4]?.weatherDesc[0]?.value || '', + chanceRain: today.hourly[4]?.chanceofrain || '0', + chanceSnow: today.hourly[4]?.chanceofsnow || '0', + }, + tomorrow: tomorrow ? { + maxF: tomorrow.maxtempF, + minF: tomorrow.mintempF, + desc: tomorrow.hourly[4]?.weatherDesc[0]?.value || '', + chanceRain: tomorrow.hourly[4]?.chanceofrain || '0', + } : null, + hourly, + }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + +function formatHour(time) { + const h = parseInt(time) / 100; + if (h === 0) return '12 AM'; + if (h < 12) return `${h} AM`; + if (h === 12) return '12 PM'; + return `${h - 12} PM`; +} diff --git a/interface/hq-dashboard/app/page.jsx b/interface/hq-dashboard/app/page.jsx index b19af5e..e66c811 100644 --- a/interface/hq-dashboard/app/page.jsx +++ b/interface/hq-dashboard/app/page.jsx @@ -22,7 +22,11 @@ import { Trash2, HeartPulse, LogOut, - Maximize2 + Maximize2, + CloudSun, + Wind, + Droplets, + Thermometer } from "lucide-react"; export default function Page() { @@ -32,13 +36,16 @@ export default function Page() { const [messages, setMessages] = useState([ { role: "assistant", - content: "Hello! I am Claw, your local agent. I have access to your Kubernetes namespace and Proxmox cluster nodes. How can I help you today?", + content: "Hello! I am Claw, your local agent. I have access to your Kubernetes namespace and Proxmox cluster nodes. Try asking me: `weather`, `check k8s pods`, or `diagnose proxmox container 102`.", timestamp: "Just now" } ]); const [inputValue, setInputValue] = useState(""); const [isClawTyping, setIsClawTyping] = useState(false); const [clawTerminalLogs, setClawTerminalLogs] = useState([]); + const [weatherCard, setWeatherCard] = useState(null); + const [weatherSending, setWeatherSending] = useState(false); + const [weatherSent, setWeatherSent] = useState(false); // OpsAgent State const [pendingApprovals, setPendingApprovals] = useState([ @@ -102,16 +109,48 @@ export default function Page() { setMessages(prev => [...prev, { role: "user", content: userText, timestamp: "Just now" }]); setInputValue(""); setIsClawTyping(true); + setWeatherCard(null); + setWeatherSent(false); setClawTerminalLogs(prev => [...prev, `[Claw] Received instruction: "${userText}"`]); - // Parse commands for visual interactions let responseText = ""; let logs = []; - await new Promise(resolve => setTimeout(resolve, 1500)); - const cleanInput = userText.toLowerCase(); - if (cleanInput.includes("pod") || cleanInput.includes("k8s") || cleanInput.includes("kubectl")) { + + if (cleanInput.includes("weather")) { + // Extract optional location override + const locMatch = userText.match(/(?:weather\s+(?:in|for|at)?\s*)(.+)/i); + const location = locMatch ? locMatch[1].trim() : '16046'; + + logs = [ + `[Claw] Fetching live weather data for "${location}"...`, + "[Claw] Connecting to wttr.in weather service...", + ]; + setClawTerminalLogs(prev => [...prev, ...logs]); + + try { + const res = await fetch(`/api/weather?location=${encodeURIComponent(location)}`); + const wx = await res.json(); + + if (wx.error) throw new Error(wx.error); + + setWeatherCard(wx); + setClawTerminalLogs(prev => [...prev, + `[Claw] Got weather for ${wx.location}: ${wx.current.tempF}°F, ${wx.current.desc}`, + "[Claw] Rendering weather card. Use the button to push to WhatsApp." + ]); + + const emoji = getWeatherEmoji(wx.current.desc); + responseText = `${emoji} Here's the live weather for **${wx.location}**:\n\n🌡️ **${wx.current.tempF}°F** (feels like ${wx.current.feelsLikeF}°F) — ${wx.current.desc}\n💧 Humidity: ${wx.current.humidity}% | 💨 Wind: ${wx.current.windMph} mph ${wx.current.windDir}\n📊 Today: ${wx.today.minF}°F – ${wx.today.maxF}°F | 🌅 ${wx.today.sunrise} / 🌇 ${wx.today.sunset}\n\nI've generated the full report card below. Hit **Send to WhatsApp** to push it to your group!`; + + } catch (err) { + responseText = `❌ Could not fetch weather: ${err.message}. Check your internet or try a city name like \`weather Toronto\`.`; + setClawTerminalLogs(prev => [...prev, `[Claw] Error: ${err.message}`]); + } + + } else if (cleanInput.includes("pod") || cleanInput.includes("k8s") || cleanInput.includes("kubectl")) { + await new Promise(resolve => setTimeout(resolve, 1500)); logs = [ "Connecting to Kubernetes API server...", "Executing: kubectl get pods -n default", @@ -123,6 +162,7 @@ export default function Page() { ]; responseText = "I've checked the Kubernetes pods. Pod `web-pod-xyz` is currently failing due to a database connection refusal. You can use the **OpsAgent** tab to approve a pod restart or scale the DB deployment."; } else if (cleanInput.includes("proxmox") || cleanInput.includes("vmid") || cleanInput.includes("disk")) { + await new Promise(resolve => setTimeout(resolve, 1500)); logs = [ "Establishing SSH connection to Proxmox cluster host 192.168.8.225...", "Running: pct status 102", @@ -134,8 +174,10 @@ export default function Page() { ]; responseText = "Proxmox Container ID `102` is running out of disk space (94% used). I have created a pending action under the **OpsAgent Center** to perform automated cleanup. Would you like me to execute it?"; } else if (cleanInput.includes("hello") || cleanInput.includes("hi")) { - responseText = "Hi there! I am Claw, your local personal assistant. Try asking me to `check k8s pods` or `diagnose proxmox container 102`."; + await new Promise(resolve => setTimeout(resolve, 800)); + responseText = "Hi there! I am Claw, your local personal assistant. Try asking me to `check k8s pods`, `diagnose proxmox container 102`, or just type `weather` for a live report!"; } else { + await new Promise(resolve => setTimeout(resolve, 1200)); responseText = `I've analyzed your query: "${userText}". I recommend initiating an **OpsRemediateWorkflow** to check the status or scanning your configuration files. Let me know if you would like me to trigger that for you.`; } @@ -144,6 +186,63 @@ export default function Page() { setIsClawTyping(false); }; + // Weather emoji helper + const getWeatherEmoji = (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 '🌫️'; + return '🌤️'; + }; + + // Send weather to WhatsApp + const handleSendWeatherToWhatsApp = async () => { + if (!weatherCard) return; + setWeatherSending(true); + const wx = weatherCard; + const emoji = getWeatherEmoji(wx.current.desc); + const msg = [ + `${emoji} *Good Morning — Daily Weather Report*`, + `📍 *${wx.location}* (ZIP: ${wx.zip})`, + ``, + `${emoji} ${wx.current.desc}`, + `🌡️ *Temp:* ${wx.current.tempF}°F (feels like ${wx.current.feelsLikeF}°F)`, + `💧 *Humidity:* ${wx.current.humidity}%`, + `💨 *Wind:* ${wx.current.windMph} mph ${wx.current.windDir}`, + `👁️ *Visibility:* ${wx.current.visibility} mi`, + `☀️ *UV Index:* ${wx.current.uvIndex}`, + ``, + `📊 *Today:* ${wx.today.minF}°F – ${wx.today.maxF}°F`, + `🌅 Sunrise: ${wx.today.sunrise} 🌇 Sunset: ${wx.today.sunset}`, + wx.tomorrow ? `🔮 *Tomorrow:* ${wx.tomorrow.minF}°F – ${wx.tomorrow.maxF}°F ${getWeatherEmoji(wx.tomorrow.desc)} ${wx.tomorrow.desc}` : '', + ``, + `_Agentic OS HQ • hq.applaude.net_ 🏠`, + ].filter(Boolean).join('\n'); + + try { + const res = await fetch('/api/send-whatsapp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: msg }) + }); + const data = await res.json(); + if (data.success) { + setWeatherSent(true); + setMessages(prev => [...prev, { role: "assistant", content: "✅ Weather report sent to your WhatsApp group!", timestamp: "Just now" }]); + } else { + setMessages(prev => [...prev, { role: "assistant", content: `❌ Failed to send: ${data.error}`, timestamp: "Just now" }]); + } + } catch (err) { + setMessages(prev => [...prev, { role: "assistant", content: `❌ Error: ${err.message}`, timestamp: "Just now" }]); + } finally { + setWeatherSending(false); + } + }; + // Approval Handlers const handleApprove = async (id, action, target) => { setPendingApprovals(prev => prev.map(item => item.id === id ? { ...item, status: "approved" } : item)); @@ -494,7 +593,105 @@ export default function Page() { )} + + {/* Weather Card */} + {weatherCard && ( +
+
+ {/* Card Header */} +
+
+ {getWeatherEmoji(weatherCard.current.desc)} +
+
📍 {weatherCard.location}
+
Live conditions • ZIP {weatherCard.zip}
+
+
+
+
{weatherCard.current.tempF}°F
+
feels like {weatherCard.current.feelsLikeF}°F
+
+
+ + {/* Condition */} +
+ {weatherCard.current.desc} +
+ + {/* Stats Grid */} +
+ {[ + { icon: "💧", label: "Humidity", value: `${weatherCard.current.humidity}%` }, + { icon: "💨", label: "Wind", value: `${weatherCard.current.windMph} mph ${weatherCard.current.windDir}` }, + { icon: "☀️", label: `UV Index`, value: `${weatherCard.current.uvIndex}` }, + { icon: "👁️", label: "Visibility", value: `${weatherCard.current.visibility} mi` }, + { icon: "🔺", label: "High", value: `${weatherCard.today.maxF}°F` }, + { icon: "🔻", label: "Low", value: `${weatherCard.today.minF}°F` }, + ].map((stat, i) => ( +
+
{stat.icon}
+
{stat.value}
+
{stat.label}
+
+ ))} +
+ + {/* Sunrise/Sunset & Tomorrow */} +
+ 🌅 {weatherCard.today.sunrise} + 🌇 {weatherCard.today.sunset} + {weatherCard.tomorrow && ( + {getWeatherEmoji(weatherCard.tomorrow.desc)} Tomorrow: {weatherCard.tomorrow.minF}–{weatherCard.tomorrow.maxF}°F + )} +
+ + {/* Send to WhatsApp CTA */} +
+ +
+
+
+ )} +
+
{/* Chat Input */}