audit: fix issues #1-#9 (mock data, notifier interface, retry/timeout, digest scheduling, real status, RPC retry, dedup, address edit UI)
- #1: remove mock alert events that poisoned production responses - #2: add Notifier interface + Send implementations for all channels - #3/#4: notification goroutines now carry a 30s context timeout and retry up to 3x with exponential backoff - #5: schedule email digest in poller via DIGEST_INTERVAL_HOURS (default 24h) - #6: SystemStatus queries real checkpoint data; returns starting/active/idle with latestBlock and lag - #7: Ethereum RPC client retries on network errors, 429, and 5xx with 1s/2s backoff - #8: alert_events dedup index + ON CONFLICT DO NOTHING to prevent duplicate events on poller restart - #9: PATCH /v1/addresses/{id} for label editing; frontend address list gains inline edit and remove buttons
This commit is contained in:
@@ -80,6 +80,41 @@ export async function getAddresses() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an address (e.g. change its label)
|
||||
* @param {number} addressId - Address ID to update
|
||||
* @param {Object} data - Fields to update (e.g. { label: "New Label" })
|
||||
* @returns {Promise<Object>} Updated address
|
||||
*/
|
||||
export async function updateAddress(addressId, data) {
|
||||
try {
|
||||
const headers = await getAuthHeaders();
|
||||
const response = await fetch(`${API_BASE}/addresses/${addressId}`, {
|
||||
method: "PATCH",
|
||||
headers: headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = "Failed to update address";
|
||||
try {
|
||||
const error = await response.json();
|
||||
errorMessage = error.message || errorMessage;
|
||||
} catch {
|
||||
errorMessage = `Server error: ${response.status} ${response.statusText}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
if (error.message.includes("fetch")) {
|
||||
throw new Error("Cannot connect to server. Is the backend running?");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tracked address
|
||||
* @param {number} addressId - Address ID to delete
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import AddressForm from "../components/AddressForm";
|
||||
import { getAddresses, createAddress } from "../api/addresses";
|
||||
import { getAddresses, createAddress, deleteAddress, updateAddress } from "../api/addresses";
|
||||
|
||||
export default function Addresses() {
|
||||
const [addresses, setAddresses] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editLabel, setEditLabel] = useState("");
|
||||
|
||||
// Load addresses on mount
|
||||
useEffect(() => {
|
||||
@@ -29,15 +31,52 @@ export default function Addresses() {
|
||||
async function handleAddressSubmit(data) {
|
||||
try {
|
||||
const newAddress = await createAddress(data);
|
||||
// Append new address to state
|
||||
setAddresses((prev) => [...prev, newAddress]);
|
||||
setError(null); // Clear any previous errors
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Failed to create address:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id, label) {
|
||||
const displayName = label || "this address";
|
||||
if (!window.confirm(`Remove "${displayName}"? This will also delete all associated alert rules.`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteAddress(id);
|
||||
setAddresses((prev) => prev.filter((a) => a.id !== id));
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Failed to delete address:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditStart(addr) {
|
||||
setEditingId(addr.id);
|
||||
setEditLabel(addr.label ?? "");
|
||||
}
|
||||
|
||||
async function handleEditSave(id) {
|
||||
try {
|
||||
const updated = await updateAddress(id, { label: editLabel || null });
|
||||
setAddresses((prev) => prev.map((a) => (a.id === id ? updated : a)));
|
||||
setEditingId(null);
|
||||
setEditLabel("");
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Failed to update address:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditCancel() {
|
||||
setEditingId(null);
|
||||
setEditLabel("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "800px", margin: "0 auto", padding: "2rem" }}>
|
||||
<h1>Tracked Addresses</h1>
|
||||
@@ -68,22 +107,88 @@ export default function Addresses() {
|
||||
backgroundColor: "#333",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
marginBottom: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{addr.label || "Unlabeled"}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "1.035rem",
|
||||
color: "#b3b3b3",
|
||||
}}
|
||||
>
|
||||
{addr.address}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
{editingId === addr.id ? (
|
||||
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginBottom: "0.25rem" }}>
|
||||
<input
|
||||
value={editLabel}
|
||||
onChange={(e) => setEditLabel(e.target.value)}
|
||||
placeholder="Label (optional)"
|
||||
style={{
|
||||
background: "#444",
|
||||
border: "1px solid #666",
|
||||
borderRadius: "3px",
|
||||
color: "#fff",
|
||||
padding: "0.25rem 0.5rem",
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleEditSave(addr.id);
|
||||
if (e.key === "Escape") handleEditCancel();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleEditSave(addr.id)}
|
||||
style={{ cursor: "pointer", padding: "0.25rem 0.6rem", fontSize: "0.85rem" }}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEditCancel}
|
||||
style={{ cursor: "pointer", padding: "0.25rem 0.6rem", fontSize: "0.85rem", background: "transparent", color: "#aaa", border: "1px solid #555" }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.25rem" }}>
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
{addr.label || "Unlabeled"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleEditStart(addr)}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#6699cc",
|
||||
fontSize: "0.8rem",
|
||||
padding: "0",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "1.035rem",
|
||||
color: "#b3b3b3",
|
||||
}}
|
||||
>
|
||||
{addr.address}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(addr.id, addr.label)}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
background: "transparent",
|
||||
border: "1px solid #884444",
|
||||
color: "#cc6666",
|
||||
borderRadius: "3px",
|
||||
padding: "0.3rem 0.7rem",
|
||||
fontSize: "0.85rem",
|
||||
marginLeft: "1rem",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user