@startuml hb_failure_evict
title HeartbeatFailure → evictPeer → TriggerConsensus ou DHT replenish

participant "Node A" as NodeA
participant "Indexer X\n(défaillant)" as IX
participant "Indexer Y\n(voter)" as IY
participant "Indexer Z\n(voter)" as IZ
participant "DHT" as DHT
participant "Indexer NEW\n(candidat)" as INEW

note over NodeA: SendHeartbeat tick — Indexer X dans le pool

NodeA -> IX: stream.Encode(Heartbeat{...})
IX -->x NodeA: timeout / transport error

NodeA -> NodeA: HeartbeatFailure(h, proto, dir, addr_X, info_X, isIndexerHB=true, maxPool)

NodeA -> NodeA: evictPeer(dir, addr_X, id_X, proto)\n→ Streams.Delete(proto, &id_X)\n→ DeleteAddr(addr_X)\n→ DeleteScore(addr_X)\n→ voters = remaining AddrInfos

NodeA -> NodeA: poolSize = len(dir.GetAddrs())

alt poolSize == 0
    NodeA -> NodeA: reconnectToSeeds()\n→ réinjecte IndexerAddresses (IsSeed=true)
    alt seeds ajoutés
        NodeA -> NodeA: need = maxPool\nNudgeIt() → tick immédiat
    else aucun seed configuré ou seeds injoignables
        NodeA -> NodeA: go retryUntilSeedResponds()\n(backoff 10s→5min, panic si IndexerAddresses vide)
    end
else poolSize > 0 AND len(voters) > 0
    NodeA -> NodeA: go TriggerConsensus(h, voters, need)
    NodeA -> IY: stream GET → GetValue{Key: candidate_DID}
    IY --> NodeA: GetResponse{Found, Records}
    NodeA -> IZ: stream GET → GetValue{Key: candidate_DID}
    IZ --> NodeA: GetResponse{Found, Records}
    note over NodeA: Quorum check:\nfound=true AND lastSeen ≤ 2×interval\nAND lastScore ≥ 30\n→ majorité → admission INEW
    NodeA -> NodeA: Indexers.SetAddr(addr_NEW, &INEW_AddrInfo)\nIndexers.SetScore(addr_NEW, Score{IsSeed:false})\nNudgeIt()
else poolSize > 0 AND len(voters) == 0
    NodeA -> DHT: go replenishIndexersFromDHT(h, need)\nDiscoverIndexersFromDHT → SelectByFillRate\n→ add to Indexers Directory
end

@enduml
