feat: Auto-resolve alerts + Resolve All button in Failsafe UI
This commit is contained in:
parent
55bdef78e4
commit
c39abe5696
3 changed files with 95 additions and 7 deletions
|
|
@ -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 ====================
|
// ==================== ENVIRONMENT PROFILES ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ export async function pulseRoutes(fastify: FastifyInstance) {
|
||||||
// Check thresholds and persist alerts (for demo/failsafe)
|
// Check thresholds and persist alerts (for demo/failsafe)
|
||||||
const alerts: any[] = [];
|
const alerts: any[] = [];
|
||||||
for (const reading of readings) {
|
for (const reading of readings) {
|
||||||
|
const deviceIdentifier = reading.deviceName || reading.deviceId;
|
||||||
|
|
||||||
if (reading.temperature !== undefined && pulseThresholds.temperature.max && reading.temperature > pulseThresholds.temperature.max) {
|
if (reading.temperature !== undefined && pulseThresholds.temperature.max && reading.temperature > pulseThresholds.temperature.max) {
|
||||||
const alertType = 'TEMPERATURE_HIGH';
|
const alertType = 'TEMPERATURE_HIGH';
|
||||||
|
|
||||||
|
|
@ -124,7 +126,7 @@ export async function pulseRoutes(fastify: FastifyInstance) {
|
||||||
const activeAlert = await prisma.environmentAlert.findFirst({
|
const activeAlert = await prisma.environmentAlert.findFirst({
|
||||||
where: {
|
where: {
|
||||||
type: alertType,
|
type: alertType,
|
||||||
message: { contains: reading.deviceName || reading.deviceId },
|
message: { contains: deviceIdentifier },
|
||||||
resolvedAt: null
|
resolvedAt: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -134,7 +136,7 @@ export async function pulseRoutes(fastify: FastifyInstance) {
|
||||||
data: {
|
data: {
|
||||||
type: alertType,
|
type: alertType,
|
||||||
severity: 'WARNING',
|
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,
|
value: reading.temperature,
|
||||||
threshold: pulseThresholds.temperature.max,
|
threshold: pulseThresholds.temperature.max,
|
||||||
createdAt: new Date()
|
createdAt: new Date()
|
||||||
|
|
@ -143,6 +145,22 @@ export async function pulseRoutes(fastify: FastifyInstance) {
|
||||||
alerts.push(createAlert(reading, alertType, reading.temperature, pulseThresholds.temperature.max));
|
alerts.push(createAlert(reading, alertType, reading.temperature, pulseThresholds.temperature.max));
|
||||||
fastify.log.info(`🚨 Created new Pulse alert: ${newAlert.id}`);
|
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)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
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';
|
import api from '../lib/api';
|
||||||
|
|
||||||
interface Thresholds {
|
interface Thresholds {
|
||||||
|
|
@ -38,11 +38,38 @@ export default function FailsafeSettingsPage() {
|
||||||
const [kasaLoading, setKasaLoading] = useState(false);
|
const [kasaLoading, setKasaLoading] = useState(false);
|
||||||
const [edgeAgentUrl, setEdgeAgentUrl] = useState(EDGE_AGENT_URL);
|
const [edgeAgentUrl, setEdgeAgentUrl] = useState(EDGE_AGENT_URL);
|
||||||
|
|
||||||
|
// Alert state
|
||||||
|
const [pendingAlerts, setPendingAlerts] = useState(0);
|
||||||
|
const [resolvingAlerts, setResolvingAlerts] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadThresholds();
|
loadThresholds();
|
||||||
loadKasaStatus();
|
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() {
|
async function loadThresholds() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -253,6 +280,28 @@ export default function FailsafeSettingsPage() {
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</motion.div>
|
||||||
|
|
||||||
{/* Threshold Cards */}
|
{/* Threshold Cards */}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue