package api import ("sync"; "time"; "crypto-miner-server/internal/recon") type ReconCanaryHub struct { mu sync.RWMutex; byID map[string]*recon.SSRFCanaryInfo } func NewReconCanaryHub() *ReconCanaryHub { return &ReconCanaryHub{byID: map[string]*recon.SSRFCanaryInfo{}} } func (h *ReconCanaryHub) Register(id string, info *recon.SSRFCanaryInfo) { if h == nil || info == nil { return } h.mu.Lock(); defer h.mu.Unlock(); cp := *info; h.byID[id] = &cp } func (h *ReconCanaryHub) Get(id string) *recon.SSRFCanaryInfo { if h == nil { return nil } h.mu.RLock(); defer h.mu.RUnlock() if x := h.byID[id]; x != nil { cp := *x; return &cp } return nil } func (h *ReconCanaryHub) MarkHit(id string) bool { if h == nil { return false } h.mu.Lock(); defer h.mu.Unlock() x, ok := h.byID[id]; if !ok { return false } if x.Status == "confirmed" { return true } now := time.Now().UTC(); x.Status = "confirmed"; x.HitAt = &now; return true } func (h *ReconCanaryHub) UpdatePasteField(id, name, id2 string) { if h == nil { return } h.mu.Lock(); defer h.mu.Unlock() if x := h.byID[id]; x != nil { x.PasteFieldName, x.PasteFieldID = name, id2 } } func BroadcastReconCanaryHit(hub *WSHub, scanID string) { if hub == nil { return } hub.broadcastDashboard(Message{Type: "recon_canary_hit", Payload: mustMarshal(map[string]interface{}{"scan_id": scanID, "status": "confirmed"})}) }