feat(web): live identity-resolution updates via SSE
useIdentityStream hook mirrors useTopologyStream — opens an
EventSource against /api/v1/identities/events with the JWT in
?token=, dispatches the five named events (snapshot, formed,
observation.linked, merged, unmerged) to the consumer, reconnects
3s after any error.
AttackerDetail subscribes whenever it has an attacker id loaded.
On any event whose payload references this observation's uuid OR
the attacker's current identity_id, refetch /attackers/{id} so the
IDENTITY badge appears (or follows through merges / unmerges) live
without a tab refocus.
IdentityDetail subscribes whenever it has an identity id loaded.
On any event whose payload references this identity_id (formed for
it, merge winner / loser, unmerge resurrected / former-winner), it
refetches both the identity row and its observations list.
Both consumers filter inside onEvent — the hook itself is dumb glue
and stays unaware of which uuids any given component cares about.
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Crosshair, Fingerprint, Globe, Radio } from '../icons';
|
||||
import api from '../utils/api';
|
||||
import { useIdentityStream } from './useIdentityStream';
|
||||
import './Dashboard.css';
|
||||
|
||||
/*
|
||||
@@ -105,6 +106,39 @@ const IdentityDetail: React.FC = () => {
|
||||
fetchObservations();
|
||||
}, [id]);
|
||||
|
||||
// Live updates: when the clusterer fires an identity event that
|
||||
// touches this identity (links a fresh observation, soft-merges,
|
||||
// resurrects on unmerge), refetch both the row and the observations
|
||||
// list so the page reflects current truth without a manual refresh.
|
||||
useIdentityStream({
|
||||
enabled: !!id,
|
||||
onEvent: (ev) => {
|
||||
if (!id) return;
|
||||
const payload = ev.payload || {};
|
||||
const refs = new Set<string>();
|
||||
const addUuid = (v: unknown) => {
|
||||
if (typeof v === 'string') refs.add(v);
|
||||
};
|
||||
addUuid(payload.identity_uuid);
|
||||
addUuid(payload.winner_uuid);
|
||||
addUuid(payload.loser_uuid);
|
||||
addUuid(payload.resurrected_uuid);
|
||||
addUuid(payload.former_winner_uuid);
|
||||
|
||||
if (refs.has(id)) {
|
||||
api.get(`/identities/${id}`)
|
||||
.then((res) => setIdentity(res.data))
|
||||
.catch(() => {});
|
||||
api.get(`/identities/${id}/observations?limit=50&offset=0`)
|
||||
.then((res) => {
|
||||
setObservations(res.data.data ?? []);
|
||||
setObservationTotal(res.data.total ?? 0);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="dashboard">
|
||||
|
||||
Reference in New Issue
Block a user