@startuml 17_startup_consensus_phase1_phase2
title Démarrage avec natifs — Phase 1 (admission) + Phase 2 (vivacité)

participant "Node / Indexer\n(appelant)" as Caller
participant "Native A" as NA
participant "Native B" as NB
participant "Indexer A" as IA
participant "Indexer B" as IB

note over Caller: ConnectToNatives()\nNativeIndexerAddresses configuré

== Étape 0 : heartbeat vers le mesh natif ==
Caller -> NA: SendHeartbeat /opencloud/heartbeat/1.0 (goroutine longue durée)
Caller -> NB: SendHeartbeat /opencloud/heartbeat/1.0 (goroutine longue durée)

== Étape 1 : fetch pool en parallèle ==
par Fetch parallèle (timeout 6s)
    Caller -> NA: GET /opencloud/native/indexers/1.0\nGetIndexersRequest{Count: max, FillRates demandés}
    NA -> NA: reachableLiveIndexers()\ntri par w(F) = fillRate×(1−fillRate)
    NA --> Caller: GetIndexersResponse{Indexers:[IA,IB], FillRates:{IA:0.3, IB:0.6}}
else
    Caller -> NB: GET /opencloud/native/indexers/1.0
    NB -> NB: reachableLiveIndexers()
    NB --> Caller: GetIndexersResponse{Indexers:[IA,IB], FillRates:{IA:0.3, IB:0.6}}
end par

note over Caller: Fusion + dédup → candidates = [IA, IB]\nisFallback = false

== Étape 2a : Phase 1 — Admission native (clientSideConsensus) ==
par Consensus parallèle (timeout 3s par natif, 4s total)
    Caller -> NA: /opencloud/native/consensus/1.0\nConsensusRequest{Candidates:[IA,IB]}
    NA -> NA: croiser avec liveIndexers
    NA --> Caller: ConsensusResponse{Trusted:[IA,IB], Suggestions:[]}
else
    Caller -> NB: /opencloud/native/consensus/1.0\nConsensusRequest{Candidates:[IA,IB]}
    NB -> NB: croiser avec liveIndexers
    NB --> Caller: ConsensusResponse{Trusted:[IA], Suggestions:[IC]}
end par

note over Caller: IA → 2/2 votes → confirmé ✓\nIB → 1/2 vote → refusé ✗\nadmittedAt = time.Now()

== Étape 2b : Phase 2 — Liveness vote (indexerLivenessVote) ==
note over Caller: Cherche votants stables dans StaticIndexerMeta\n(AdmittedAt != zero, age >= MinStableAge=2min)

alt Votants stables disponibles
    par Phase 2 parallèle (timeout 3s)
        Caller -> IA: /opencloud/indexer/consensus/1.0\nIndexerConsensusRequest{Candidates:[IA]}
        IA -> IA: vérifier StreamRecords[ProtocolHB][candidate]\nLastSeen ≤ 2×60s && LastScore ≥ 30
        IA --> Caller: IndexerConsensusResponse{Alive:[IA]}
    end par
    note over Caller: IA confirmé vivant par quorum > 0.5
else Aucun votant stable (premier démarrage)
    note over Caller: Phase 1 conservée directement\n(aucun votant MinStableAge atteint)
end

== Étape 3 : remplacement StaticIndexers ==
Caller -> Caller: replaceStaticIndexers(pool={IA}, admittedAt)\nStaticIndexerMeta[IA].AdmittedAt = time.Now()

== Étape 4 : heartbeat long-lived vers pool ==
Caller -> IA: SendHeartbeat /opencloud/heartbeat/1.0 (goroutine longue durée)
note over Caller: Pool actif. NudgeIndexerHeartbeat()

@enduml
