feat: Auto-resolve alerts + Resolve All button in Failsafe UI
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

This commit is contained in:
fullsizemalt 2026-01-06 00:58:53 -08:00
parent 55bdef78e4
commit c39abe5696
3 changed files with 95 additions and 7 deletions

View file

@ -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 ====================
/**

View file

@ -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)`);
}
}
}

View file

@ -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);
@ -253,6 +280,28 @@ export default function FailsafeSettingsPage() {
Refresh
</button>
</div>
{/* Alert Management */}
<div className="mt-6 pt-6 border-t border-slate-700">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-amber-400" />
<span className="text-sm font-medium text-slate-300">Pending Alerts</span>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-bold ${pendingAlerts > 0 ? 'bg-red-500/20 text-red-400' : 'bg-green-500/20 text-green-400'
}`}>
{pendingAlerts}
</span>
</div>
<button
onClick={resolveAllAlerts}
disabled={resolvingAlerts || pendingAlerts === 0}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-emerald-600 hover:bg-emerald-500 disabled:bg-slate-700 disabled:opacity-50 text-white rounded-xl text-sm font-medium transition-all"
>
<CheckCircle className={`w-4 h-4 ${resolvingAlerts ? 'animate-pulse' : ''}`} />
{resolvingAlerts ? 'Resolving...' : 'Resolve All Alerts'}
</button>
</div>
</motion.div>
{/* Threshold Cards */}