diff --git a/src/components/dashboard/Settings.jsx b/src/components/dashboard/Settings.jsx
index 83a9038..3f5ddf2 100644
--- a/src/components/dashboard/Settings.jsx
+++ b/src/components/dashboard/Settings.jsx
@@ -2,9 +2,8 @@ import React, { useMemo, useState, useEffect } from "react";
import { useSettings } from "../../hooks/useSettings";
import { useStore } from "../../lib/store";
import { getEnvironmentConfig } from "../../lib/config";
-import { updateCustomNetworkConfig } from "../../lib/stellar";
-
-const SESSION_API_KEY = 'stellar_custom_api_key';
+import { saveAlertRule, getAlertRules, deleteAlertRule } from "../../lib/alertRulesDb"; // Import IndexedDB helpers
+import { ALERT_RULE_TYPE, ALERT_CHANNEL } from "../../lib/alerts"; // Import alert types
function FieldLabel({ children }) {
return (
@@ -58,16 +57,22 @@ export default function Settings() {
const [apiKey, setApiKey] = useState(() => sessionStorage.getItem(SESSION_API_KEY) || "");
const baseline = useMemo(() => getEnvironmentConfig(), []);
- function handleApiKeyChange(val) {
- setApiKey(val);
- if (val) {
- sessionStorage.setItem(SESSION_API_KEY, val);
- updateCustomNetworkConfig({ customHeaders: { Authorization: `Bearer ${val}` } });
- } else {
- sessionStorage.removeItem(SESSION_API_KEY);
- updateCustomNetworkConfig({ customHeaders: {} });
+ // State for Alert Rules
+ const [alertRules, setAlertRules] = useState([]);
+ const [newRuleType, setNewRuleType] = useState(ALERT_RULE_TYPE.BALANCE_LOW);
+ const [newRuleThreshold, setNewRuleThreshold] = useState(0);
+ const [newRuleAssetCode, setNewRuleAssetCode] = useState("XLM");
+ const [newRuleChannel, setNewRuleChannel] = useState(ALERT_CHANNEL.EFFECTS);
+ const [newRuleAccount, setNewRuleAccount] = useState(""); // Optional: specific account for the rule
+
+ // Load alert rules on component mount
+ useEffect(() => {
+ async function loadRules() {
+ const rules = await getAlertRules();
+ setAlertRules(rules);
}
- }
+ loadRules();
+ }, []);
function handleSaveProfile() {
const name = profileName.trim() || activeProfileName;
@@ -177,6 +182,22 @@ export default function Settings() {
});
}
+ async function handleAddAlertRule() {
+ if (newRuleThreshold < 0) {
+ alert("Threshold cannot be negative.");
+ return;
+ }
+ const newRule = { id: `rule-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, type: newRuleType, threshold: Number(newRuleThreshold), assetCode: newRuleAssetCode.trim().toUpperCase(), channel: newRuleChannel, account: newRuleAccount.trim() || undefined };
+ await saveAlertRule(newRule);
+ setAlertRules(await getAlertRules()); // Refresh list
+ setNewRuleThreshold(0); setNewRuleAssetCode("XLM"); setNewRuleAccount(""); // Reset form fields
+ }
+
+ async function handleDeleteAlertRule(ruleId) {
+ await deleteAlertRule(ruleId);
+ setAlertRules(await getAlertRules()); // Refresh list
+ }
+
return (
@@ -244,6 +265,144 @@ export default function Settings() {
+ {/* New section for Alert Rules */}
+
+
Alert Rules
+
+ {alertRules.length === 0 ? (
+
No alert rules configured.
+ ) : (
+ alertRules.map((rule) => (
+
+
+ {rule.type.replace(/_/g, ' ')}: {rule.threshold} {rule.assetCode} (Channel: {rule.channel}) {rule.account ? `(Account: ${rule.account})` : ''}
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Configuration Profiles
diff --git a/src/hooks/useAccountStream.ts b/src/hooks/useAccountStream.ts
index 1ce2672..0a88a31 100644
--- a/src/hooks/useAccountStream.ts
+++ b/src/hooks/useAccountStream.ts
@@ -13,6 +13,8 @@ import type {
AccountStreamEvent,
StreamStatus,
} from '../lib/websocket/StreamTypes'
+import { evaluateEventRules, ALERT_RULE_TYPE, ALERT_CHANNEL } from '../lib/alerts'; // Import new evaluation function and types
+} from '../lib/websocket/StreamTypes'
export interface UseAccountStreamOptions {
channels?: AccountStreamChannel[]
@@ -49,12 +51,22 @@ export function useAccountStream(
const [status, setStatus] = useState
('idle')
const [lastEventAt, setLastEventAt] = useState(null)
const [errored, setErrored] = useState(false)
+ const [activeAlertRules, setActiveAlertRules] = useState([]); // New state for active alert rules
// Latest channels list — kept in a ref so we don't re-subscribe on prop churn
// when the array is reference-new but value-equal.
const channelsRef = useRef(channels)
channelsRef.current = channels
+ useEffect(() => {
+ // Load alert rules from IndexedDB on mount
+ async function loadRules() {
+ const rules = await getAlertRules();
+ setActiveAlertRules(rules);
+ }
+ loadRules();
+ }, []); // Empty dependency array means this runs once on mount
+
useEffect(() => {
if (!accountId) {
setStatus('idle')
@@ -76,6 +88,10 @@ export function useAccountStream(
if (emitNotifications) {
const notif = summarizeEvent(event, accountId)
if (notif) notificationStore.push(notif)
+
+ // Evaluate custom alert rules
+ const triggeredAlerts = evaluateEventRules(event, activeAlertRules, accountId);
+ triggeredAlerts.forEach(alert => notificationStore.push(alert));
}
},
{
@@ -104,7 +120,7 @@ export function useAccountStream(
return () => {
for (const cleanup of unsubscribers) cleanup()
}
- }, [accountId, network, channels, options.cursor, bufferSize, emitNotifications])
+ }, [accountId, network, channels, options.cursor, bufferSize, emitNotifications, activeAlertRules]) // Add activeAlertRules to dependencies
return { events, status, lastEventAt, errored }
}
diff --git a/src/lib/alertRulesDb.js b/src/lib/alertRulesDb.js
new file mode 100644
index 0000000..9d8904e
--- /dev/null
+++ b/src/lib/alertRulesDb.js
@@ -0,0 +1,60 @@
+import { openDB } from 'idb'; // Using idb for simpler IndexedDB interactions
+
+const DB_NAME = 'stellar-dev-dashboard';
+const DB_VERSION = 1;
+const STORE_NAME = 'alert-rules';
+
+let dbPromise;
+
+function initDb() {
+ if (!dbPromise) {
+ dbPromise = openDB(DB_NAME, DB_VERSION, {
+ upgrade(db) {
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
+ db.createObjectStore(STORE_NAME, { keyPath: 'id' });
+ }
+ },
+ });
+ }
+ return dbPromise;
+}
+
+/**
+ * Saves an alert rule to IndexedDB.
+ * @param {AlertRule} rule - The alert rule object to save.
+ * @returns {Promise} The ID of the saved rule.
+ */
+export async function saveAlertRule(rule) {
+ const db = await initDb();
+ const tx = db.transaction(STORE_NAME, 'readwrite');
+ await tx.store.put(rule);
+ await tx.done;
+ return rule.id;
+}
+
+/**
+ * Reads all alert rules from IndexedDB.
+ * @returns {Promise} An array of alert rule objects.
+ */
+export async function getAlertRules() {
+ const db = await initDb();
+ const tx = db.transaction(STORE_NAME, 'readonly');
+ const rules = await tx.store.getAll();
+ await tx.done;
+ return rules;
+}
+
+/**
+ * Deletes an alert rule from IndexedDB by its ID.
+ * @param {string} ruleId - The ID of the rule to delete.
+ * @returns {Promise}
+ */
+export async function deleteAlertRule(ruleId) {
+ const db = await initDb();
+ const tx = db.transaction(STORE_NAME, 'readwrite');
+ await tx.store.delete(ruleId);
+ await tx.done;
+}
+
+// Initialize the database on module load
+initDb();
\ No newline at end of file
diff --git a/src/lib/alerts.js b/src/lib/alerts.js
index 0029590..efdb221 100644
--- a/src/lib/alerts.js
+++ b/src/lib/alerts.js
@@ -4,6 +4,31 @@ const ALERT_SEVERITY = {
CRITICAL: "critical",
};
+const ALERT_RULE_TYPE = {
+ BALANCE_LOW: "balance_low",
+ BALANCE_HIGH: "balance_high",
+ PAYMENT_INCOMING: "payment_incoming",
+ PAYMENT_OUTGOING: "payment_outgoing",
+ TRANSACTION_FAILED: "transaction_failed",
+ // Add other types as needed
+};
+
+const ALERT_CHANNEL = {
+ PAYMENTS: "payments",
+ EFFECTS: "effects",
+ TRANSACTIONS: "transactions",
+};
+
+/**
+ * @typedef {object} AlertRule
+ * @property {string} id - Unique ID for the rule
+ * @property {ALERT_RULE_TYPE} type - Type of alert rule (e.g., 'balance_low', 'payment_incoming')
+ * @property {number} threshold - Numeric threshold for the rule (e.g., 100 for balance, 0 for payment)
+ * @property {string} assetCode - Asset code for balance/payment rules (e.g., 'XLM', 'USD')
+ * @property {ALERT_CHANNEL} channel - The stream channel this rule applies to
+ * @property {string} [account] - Optional account ID if the rule is specific to an account
+ */
+
export function evaluateAlertRules(snapshot, score) {
const alerts = [];
const memory = snapshot?.memory;
@@ -41,6 +66,122 @@ export function evaluateAlertRules(snapshot, score) {
return alerts;
}
+/**
+ * Evaluates an incoming streaming event against a collection of user-defined alert rules.
+ * @param {AccountStreamEvent} incomingEvent - The event received from the account stream.
+ * @param {AlertRule[]} activeRules - An array of active alert rules.
+ * @param {string} watchedAccount - The account ID being watched.
+ * @returns {Array