package api import ( "strings" "time" fleetai "crypto-miner-server/internal/ai" "crypto-miner-server/internal/atlas" "crypto-miner-server/internal/db" "crypto-miner-server/internal/strategy" ) // HubCourtChamberAdapter supplies court tribunal data from hub + SQLite. type HubCourtChamberAdapter struct { Hub *WSHub DB *db.Database } // NewHubCourtChamberAdapter wires hub evidence for adversarial court sessions. func NewHubCourtChamberAdapter(hub *WSHub, database *db.Database) *HubCourtChamberAdapter { return &HubCourtChamberAdapter{Hub: hub, DB: database} } func (a *HubCourtChamberAdapter) FailureAtlasSummary(fingerprintKey, goos string) string { if a == nil { return "" } inner := &DatabaseCourtAdapter{DB: a.DB} return inner.FailureAtlasSummary(fingerprintKey, goos) } func (a *HubCourtChamberAdapter) BestPhenotype(fingerprintKey, goos string) (*strategy.FleetPhenotype, bool) { if a == nil { return nil, false } inner := &DatabaseCourtAdapter{DB: a.DB} return inner.BestPhenotype(fingerprintKey, goos) } func (a *HubCourtChamberAdapter) ChamberEvidence(agentID string, snap fleetai.AgentSnapshot) fleetai.CourtChamberEvidence { ev := fleetai.CourtChamberEvidence{ AgentID: agentID, AgentName: snap.Name, Hashrate: snap.MiningHashrate, Stuck: snap.Stuck, ChainExhausted: snap.ChainExhausted, ClearanceLevel: snap.ClearanceLevel, JoinLane: snap.JoinLane, ActiveMethod: snap.ActiveMethod, FingerprintKey: snap.FingerprintKey, } if a == nil { return ev } goos := firstNonEmpty(snap.GOOS, snap.Platform) ev.AtlasSummary = a.FailureAtlasSummary(snap.FingerprintKey, goos) ev.SubnetImmune, ev.ErasureRecovery, ev.GossipWhispers = a.hubEvidence(agentID, snap) return ev } func (a *HubCourtChamberAdapter) hubEvidence(agentID string, snap fleetai.AgentSnapshot) (subnetImmune, erasureRecovery, gossip string) { if a.Hub == nil { return "hub unavailable", fleetai.FormatErasureRecovery(false, false, false), "none" } prefix := a.Hub.agentSubnetFor(agentID) if prefix == "" && a.Hub.db != nil { if ag, err := a.Hub.db.GetAgent(agentID); err == nil && ag != nil { prefix = atlas.PrefixFromHostOrIP(ag.IP) } } failCount := 0 var pausedUntil *time.Time if a.Hub.db != nil && prefix != "" { if row, err := a.Hub.db.GetSubnetSpreadPause(prefix); err == nil && row != nil { failCount = row.FailCount pausedUntil = row.PausedUntil } } subnetImmune = fleetai.FormatSubnetImmuneRow(prefix, failCount, pausedUntil) policy := a.Hub.serverPolicySnapshot() stageAttempted, stageOK := stageFetchStatus(snap.LOTLAttempts) erasureRecovery = fleetai.FormatErasureRecovery(policy.ErasureLanesEnabled, stageAttempted, stageOK) hints := a.Hub.gossipWhispersForSubnet(prefix) gossip = formatGossipForCourt(hints) return subnetImmune, erasureRecovery, gossip } func stageFetchStatus(attempts []fleetai.TierAttempt) (attempted, ok bool) { for _, a := range attempts { if a.Tier == "stage_fetch" || a.Tier == "bits_curl" || a.Tier == "bits" || a.Tier == "curl" { attempted = true if a.OK { ok = true } } } return attempted, ok } func formatGossipForCourt(hints []atlas.GossipHint) string { if len(hints) == 0 { return "none recorded on this /24" } parts := make([]string, 0, len(hints)) for _, h := range hints { line := h.Tier + "|" + h.Condition if h.Reason != "" { line += " (" + h.Reason + ")" } parts = append(parts, line) } return strings.Join(parts, "; ") } func firstNonEmpty(values ...string) string { for _, v := range values { if strings.TrimSpace(v) != "" { return strings.TrimSpace(v) } } return "" } // BroadcastEmberwakeCourtDebate optionally surfaces court transcripts on Emberwake dashboard WS. func (h *WSHub) BroadcastEmberwakeCourtDebate(agentID string, transcript fleetai.CourtDebateTranscript) { if h == nil { return } h.broadcastDashboard(Message{ Type: "emberwake_court_debate", Payload: mustMarshal(map[string]interface{}{ "agent_id": agentID, "agent_name": transcript.AgentName, "verdict": transcript.Verdict, "transcript": transcript.Transcript, "evidence": transcript.Evidence, "ts": transcript.Timestamp, }), }) }