diff --git a/backend/src/routes/environment.routes.ts b/backend/src/routes/environment.routes.ts index 165eb93..629a09f 100644 --- a/backend/src/routes/environment.routes.ts +++ b/backend/src/routes/environment.routes.ts @@ -303,6 +303,27 @@ export async function environmentRoutes(fastify: FastifyInstance) { } }); + /** + * POST /alerts/resolve-all + * Resolve all unresolved alerts (for demo reset) + */ + fastify.post('/alerts/resolve-all', { + handler: async (request, reply) => { + try { + const result = await prisma.environmentAlert.updateMany({ + where: { resolvedAt: null }, + data: { resolvedAt: new Date() } + }); + + fastify.log.info(`🔄 Resolved ${result.count} pending alerts`); + return { resolved: result.count, message: `Resolved ${result.count} alerts` }; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to resolve alerts' }); + } + } + }); + // ==================== ENVIRONMENT PROFILES ==================== /** diff --git a/backend/src/routes/pulse.routes.ts b/backend/src/routes/pulse.routes.ts index 04d9ee9..c313d91 100644 --- a/backend/src/routes/pulse.routes.ts +++ b/backend/src/routes/pulse.routes.ts @@ -117,6 +117,8 @@ export async function pulseRoutes(fastify: FastifyInstance) { // Check thresholds and persist alerts (for demo/failsafe) const alerts: any[] = []; for (const reading of readings) { + const deviceIdentifier = reading.deviceName || reading.deviceId; + if (reading.temperature !== undefined && pulseThresholds.temperature.max && reading.temperature > pulseThresholds.temperature.max) { const alertType = 'TEMPERATURE_HIGH'; @@ -124,7 +126,7 @@ export async function pulseRoutes(fastify: FastifyInstance) { const activeAlert = await prisma.environmentAlert.findFirst({ where: { type: alertType, - message: { contains: reading.deviceName || reading.deviceId }, + message: { contains: deviceIdentifier }, resolvedAt: null } }); @@ -134,7 +136,7 @@ export async function pulseRoutes(fastify: FastifyInstance) { data: { type: alertType, severity: 'WARNING', - message: `${reading.deviceName || 'Pulse Device'}: ${alertType.replace('_', ' ')} (${reading.temperature} vs ${pulseThresholds.temperature.max})`, + message: `${deviceIdentifier}: ${alertType.replace('_', ' ')} (${reading.temperature.toFixed(1)}°F > ${pulseThresholds.temperature.max}°F)`, value: reading.temperature, threshold: pulseThresholds.temperature.max, createdAt: new Date() @@ -143,6 +145,22 @@ export async function pulseRoutes(fastify: FastifyInstance) { alerts.push(createAlert(reading, alertType, reading.temperature, pulseThresholds.temperature.max)); fastify.log.info(`🚨 Created new Pulse alert: ${newAlert.id}`); } + } else if (reading.temperature !== undefined && pulseThresholds.temperature.max) { + // Temperature is within threshold - AUTO-RESOLVE any active alerts for this device + const resolvedAlerts = await prisma.environmentAlert.updateMany({ + where: { + type: 'TEMPERATURE_HIGH', + message: { contains: deviceIdentifier }, + resolvedAt: null + }, + data: { + resolvedAt: new Date() + } + }); + + if (resolvedAlerts.count > 0) { + fastify.log.info(`✅ Auto-resolved ${resolvedAlerts.count} alerts for ${deviceIdentifier} (temp now ${reading.temperature.toFixed(1)}°F)`); + } } } diff --git a/frontend/src/pages/FailsafeSettingsPage.tsx b/frontend/src/pages/FailsafeSettingsPage.tsx index 81e778e..3cbd8b5 100644 --- a/frontend/src/pages/FailsafeSettingsPage.tsx +++ b/frontend/src/pages/FailsafeSettingsPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; -import { Settings, Thermometer, Droplets, Wind, Gauge, Save, RefreshCw, Power, Plug, Search, Zap } from 'lucide-react'; +import { Settings, Thermometer, Droplets, Wind, Gauge, Save, RefreshCw, Power, Plug, Search, Zap, AlertTriangle, CheckCircle } from 'lucide-react'; import api from '../lib/api'; interface Thresholds { @@ -38,11 +38,38 @@ export default function FailsafeSettingsPage() { const [kasaLoading, setKasaLoading] = useState(false); const [edgeAgentUrl, setEdgeAgentUrl] = useState(EDGE_AGENT_URL); + // Alert state + const [pendingAlerts, setPendingAlerts] = useState(0); + const [resolvingAlerts, setResolvingAlerts] = useState(false); + useEffect(() => { loadThresholds(); loadKasaStatus(); + loadPendingAlerts(); }, []); + async function loadPendingAlerts() { + try { + const res = await api.get('/environment/alerts?unresolved=true'); + setPendingAlerts(res.data?.length || 0); + } catch { + // Ignore errors + } + } + + async function resolveAllAlerts() { + try { + setResolvingAlerts(true); + const res = await api.post('/environment/alerts/resolve-all'); + setMessage({ type: 'success', text: res.data.message || 'All alerts resolved!' }); + setPendingAlerts(0); + } catch (error) { + setMessage({ type: 'error', text: 'Failed to resolve alerts' }); + } finally { + setResolvingAlerts(false); + } + } + async function loadThresholds() { try { setLoading(true); @@ -181,8 +208,8 @@ export default function FailsafeSettingsPage() {
Status {kasaStatus.connected ? 'Connected' : 'Disconnected'} @@ -253,6 +280,28 @@ export default function FailsafeSettingsPage() { Refresh
+ + {/* Alert Management */} +
+
+
+ + Pending Alerts +
+ 0 ? 'bg-red-500/20 text-red-400' : 'bg-green-500/20 text-green-400' + }`}> + {pendingAlerts} + +
+ +
{/* Threshold Cards */} @@ -323,8 +372,8 @@ export default function FailsafeSettingsPage() { initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className={`mt-6 p-4 rounded-xl ${message.type === 'success' - ? 'bg-green-500/20 border border-green-500/30 text-green-400' - : 'bg-red-500/20 border border-red-500/30 text-red-400' + ? 'bg-green-500/20 border border-green-500/30 text-green-400' + : 'bg-red-500/20 border border-red-500/30 text-red-400' }`} > {message.text}