'use client';
import { useState, useEffect, useRef } from 'react';
import { useApp } from '@/lib/context';
import { fetchAlerts, createAlert, updateAlert, deleteAlert } from '@/lib/api';
import { resolveName, resolveKey } from '@/lib/localization';
import type { AlertConfig } from '@/lib/types';
interface Props {
open: boolean;
onClose: () => void;
}
const inputCls =
'w-full bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-indigo-500';
const selectCls =
'flex-1 bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-white focus:outline-none';
interface AlertFormState {
itemKey: string;
itemKeyIsRegex: boolean;
combinator: string;
signalType: 'green' | 'red';
condition: 'above' | 'below';
threshold: string;
}
function emptyForm(): AlertFormState {
return {
itemKey: '',
itemKeyIsRegex: false,
combinator: '',
signalType: 'green',
condition: 'below',
threshold: '0',
};
}
function alertToForm(a: AlertConfig): AlertFormState {
return {
itemKey: a.item_key,
itemKeyIsRegex: a.item_key_is_regex,
combinator: a.combinator ?? '',
signalType: a.signal_type,
condition: a.condition,
threshold: String(a.threshold),
};
}
function Tooltip({ text }: { text: string }) {
return (
ⓘ
{text}
);
}
function AlertForm({
value,
onChange,
onSubmit,
onCancel,
submitLabel,
}: {
value: AlertFormState;
onChange: (s: AlertFormState) => void;
onSubmit: () => void;
onCancel?: () => void;
submitLabel: string;
}) {
return (
);
}
export default function AlertPanel({ open, onClose }: Props) {
const { triggeredAlerts, refreshAlerts, localeMap, reverseMap } = useApp();
const [alerts, setAlerts] = useState([]);
const [newForm, setNewForm] = useState(emptyForm());
const [editingId, setEditingId] = useState(null);
const [editForm, setEditForm] = useState(emptyForm());
const prevTriggeredCount = useRef(0);
useEffect(() => {
if (open) fetchAlerts().then(setAlerts);
}, [open]);
useEffect(() => {
if (triggeredAlerts.length > prevTriggeredCount.current) {
try {
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 880;
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
osc.start();
osc.stop(ctx.currentTime + 0.4);
} catch {
/* AudioContext blocked */
}
}
prevTriggeredCount.current = triggeredAlerts.length;
}, [triggeredAlerts.length]);
/** Resolves item key input — skips resolution for regex mode. */
function normalizeItemKey(form: AlertFormState): string {
if (form.itemKeyIsRegex) return form.itemKey.trim();
return resolveKey(form.itemKey.trim(), reverseMap);
}
async function handleCreate() {
if (!newForm.itemKey.trim()) return;
const created = await createAlert({
item_key: normalizeItemKey(newForm),
item_key_is_regex: newForm.itemKeyIsRegex,
combinator: newForm.combinator.trim() || null,
signal_type: newForm.signalType,
condition: newForm.condition,
threshold: parseInt(newForm.threshold, 10),
});
setAlerts((a) => [created, ...a]);
setNewForm(emptyForm());
await refreshAlerts();
}
async function handleEdit(id: string) {
if (!editForm.itemKey.trim()) return;
const updated = await updateAlert(id, {
item_key: normalizeItemKey(editForm),
item_key_is_regex: editForm.itemKeyIsRegex,
combinator: editForm.combinator.trim() || null,
signal_type: editForm.signalType,
condition: editForm.condition,
threshold: parseInt(editForm.threshold, 10),
});
setAlerts((a) => a.map((x) => (x.id === id ? updated : x)));
setEditingId(null);
await refreshAlerts();
}
async function handleDelete(id: string) {
await deleteAlert(id);
setAlerts((a) => a.filter((x) => x.id !== id));
await refreshAlerts();
}
function startEdit(alert: AlertConfig) {
setEditingId(alert.id);
setEditForm(alertToForm(alert));
}
return (
Alerts
{triggeredAlerts.length > 0 && (
🔴 TRIGGERED ({triggeredAlerts.length})
{triggeredAlerts.map((a, i) => (
{resolveName(a.matched_item_key, localeMap)}
({a.combinator_match})
[{a.signal_type}]
= {a.current_value} {a.condition} {a.threshold}
{a.item_key_is_regex && a.matched_item_key !== a.item_key && (
)}
))}
)}
{alerts.length === 0 &&
No alerts configured.
}
{alerts.map((a) => (
{editingId === a.id ? (
handleEdit(a.id)}
onCancel={() => setEditingId(null)}
submitLabel="Save"
/>
) : (
{resolveName(a.item_key, localeMap)}
{a.item_key_is_regex && }
{a.combinator && @ {a.combinator}}
[{a.signal_type}]
{' '}
{a.condition} {a.threshold}
)}
))}
);
}