feat(web): read-only campaigns API + SSE + frontend
API: /api/v1/campaigns (paginated list), /api/v1/campaigns/{uuid}
(soft-merge chain follow), /api/v1/campaigns/{uuid}/identities
(member identities), and /api/v1/campaigns/events (SSE under
campaign.> + JWT-via-?token=, snapshot-on-connect). Mirror of the
identity router; same auth, same shape, same OpenAPI tags pattern.
Frontend: CampaignDetail.tsx page (same visual vocabulary as
IdentityDetail), useCampaignStream hook (mirror of
useIdentityStream), /campaigns/:id route, IdentityDetail's
CAMPAIGN badge becomes clickable and navigates to the campaign.
useIdentityStream now listens for identity.campaign.assigned so
the badge appears live without a manual refresh.
This commit is contained in:
@@ -184,8 +184,9 @@ const IdentityDetail: React.FC = () => {
|
||||
{identity.campaign_id && (
|
||||
<span
|
||||
className="traversal-badge"
|
||||
style={{ fontSize: '0.8rem', cursor: 'default', letterSpacing: '2px' }}
|
||||
title="Campaign assignment from the campaign clusterer"
|
||||
style={{ fontSize: '0.8rem', cursor: 'pointer', letterSpacing: '2px' }}
|
||||
title="Campaign assignment from the campaign clusterer. Click to view campaign."
|
||||
onClick={() => navigate(`/campaigns/${identity.campaign_id}`)}
|
||||
>
|
||||
CAMPAIGN · {identity.campaign_id.slice(0, 8)}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user