Summary
The hub stores a Raft Cluster reference on Server (internal/hub/server.go:54) and even uses it during GracefulStop (server.go:58-62). But the cluster's leadership state is never read by the Status RPC, so operators have no way to ask "which node is leader?" or "am I talking to the leader?" through the supported API surface. The data exists; the wire is missing.
This was tracked in TASKS.md as "Fix hub cluster: NewCluster result is discarded; Raft runs but leadership status is never queryable" (added 2026-04-08). The "discarded" half is no longer accurate — it was fixed at some point and the task entry didn't get updated. This issue covers only the remaining "leadership status never queryable" half.
What's already in place
Server has the cluster reference and shuts it down properly:
// internal/hub/server.go:48-62
func (s *Server) SetCluster(cluster *Cluster) {
s.cluster = cluster
}
func (s *Server) GracefulStop() {
if s.cluster != nil {
_ = s.cluster.Shutdown()
}
s.grpc.GracefulStop()
}
The methods to query leadership exist on *Cluster:
// internal/hub/cluster.go:127-141
func (c *Cluster) IsLeader() bool { ... }
func (c *Cluster) LeaderAddr() string { ... }
What's missing
StatusResponse has no leadership fields:
// internal/hub/types.go:264-269
type StatusResponse struct {
TotalEntries uint64 `json:"total_entries"`
ConnectedClients uint32 `json:"connected_clients"`
EntriesByType map[string]uint64 `json:"entries_by_type"`
EntriesByProject map[string]uint64 `json:"entries_by_project"`
}
The Status handler (internal/hub/handler.go) populates these from Store but never touches s.cluster. The ctx hub status CLI renders whatever the RPC returns, so even if an operator runs the cluster in HA mode, the CLI is silent about it.
Proposed Shape
Three small changes, all in this order so each is reviewable:
1. Extend StatusResponse
// internal/hub/types.go
type StatusResponse struct {
TotalEntries uint64 `json:"total_entries"`
ConnectedClients uint32 `json:"connected_clients"`
EntriesByType map[string]uint64 `json:"entries_by_type"`
EntriesByProject map[string]uint64 `json:"entries_by_project"`
// Cluster fields are zero values when the hub runs
// standalone (no Raft peers configured).
ClusterEnabled bool `json:"cluster_enabled"`
IsLeader bool `json:"is_leader,omitempty"`
LeaderAddr string `json:"leader_addr,omitempty"`
}
ClusterEnabled is the disambiguator: when false, the other two are meaningless zero values; when true, they reflect live Raft state. Without ClusterEnabled, a standalone hub would always report IsLeader=false, LeaderAddr="" which would mislead anyone reading the response.
2. Populate in the handler
// internal/hub/handler.go (Status handler)
resp := &StatusResponse{
TotalEntries: ...,
ConnectedClients: ...,
EntriesByType: ...,
EntriesByProject: ...,
}
if s.cluster != nil {
resp.ClusterEnabled = true
resp.IsLeader = s.cluster.IsLeader()
resp.LeaderAddr = s.cluster.LeaderAddr()
}
return resp, nil
3. Render in the CLI
Locate the ctx hub status rendering layer (probably internal/cli/hub/core/server/render.go or similar; grep for the existing TotalEntries rendering to find it) and add a short cluster section:
Cluster: enabled (leader: 10.0.0.5:7081)
this node IS the leader
or, when not in cluster mode:
Use the existing rendering style for consistency.
Tests Required
TestStatus_StandaloneReportsClusterDisabled: start a Server without SetCluster, call Status, assert ClusterEnabled == false.
TestStatus_ClusterReportsLeadershipState: start a single-node Raft cluster, wait for it to elect itself leader, call Status, assert ClusterEnabled == true && IsLeader == true && LeaderAddr != "".
- (Render layer) Snapshot/golden-file test for the two CLI rendering shapes (standalone vs cluster).
Out of Scope
- Adding a
Leadership streaming RPC (operator could subscribe to leadership changes). The pull-via-Status surface is the minimum useful; streaming can come later if a real use case appears.
- A
ctx hub leader shortcut command. ctx hub status returning the info is sufficient; a dedicated subcommand is sugar.
- Tracking the Raft term / commit-index / log-position. Useful for debugging quorum issues but a separate concern from "who is leader right now".
Acceptance
Scope for "good first issue"
This is wiring an existing data source (Cluster.IsLeader(), Cluster.LeaderAddr()) through three layers (response struct → handler → CLI render) plus a small test. No new gRPC method, no auth concerns, no concurrency surprise (the Cluster has its own locking). A newcomer can pattern-match against how TotalEntries flows from Store → handler → CLI to land this.
Summary
The hub stores a Raft
Clusterreference onServer(internal/hub/server.go:54) and even uses it duringGracefulStop(server.go:58-62). But the cluster's leadership state is never read by the Status RPC, so operators have no way to ask "which node is leader?" or "am I talking to the leader?" through the supported API surface. The data exists; the wire is missing.This was tracked in TASKS.md as "Fix hub cluster: NewCluster result is discarded; Raft runs but leadership status is never queryable" (added 2026-04-08). The "discarded" half is no longer accurate — it was fixed at some point and the task entry didn't get updated. This issue covers only the remaining "leadership status never queryable" half.
What's already in place
Serverhas the cluster reference and shuts it down properly:The methods to query leadership exist on
*Cluster:What's missing
StatusResponsehas no leadership fields:The
Statushandler (internal/hub/handler.go) populates these fromStorebut never touchess.cluster. Thectx hub statusCLI renders whatever the RPC returns, so even if an operator runs the cluster in HA mode, the CLI is silent about it.Proposed Shape
Three small changes, all in this order so each is reviewable:
1. Extend
StatusResponseClusterEnabledis the disambiguator: whenfalse, the other two are meaningless zero values; whentrue, they reflect live Raft state. WithoutClusterEnabled, a standalone hub would always reportIsLeader=false, LeaderAddr=""which would mislead anyone reading the response.2. Populate in the handler
3. Render in the CLI
Locate the
ctx hub statusrendering layer (probablyinternal/cli/hub/core/server/render.goor similar; grep for the existingTotalEntriesrendering to find it) and add a short cluster section:or, when not in cluster mode:
Use the existing rendering style for consistency.
Tests Required
TestStatus_StandaloneReportsClusterDisabled: start a Server withoutSetCluster, call Status, assertClusterEnabled == false.TestStatus_ClusterReportsLeadershipState: start a single-node Raft cluster, wait for it to elect itself leader, call Status, assertClusterEnabled == true && IsLeader == true && LeaderAddr != "".Out of Scope
Leadershipstreaming RPC (operator could subscribe to leadership changes). The pull-via-Status surface is the minimum useful; streaming can come later if a real use case appears.ctx hub leadershortcut command.ctx hub statusreturning the info is sufficient; a dedicated subcommand is sugar.Acceptance
StatusResponsegainsClusterEnabled,IsLeader,LeaderAddrfields.s.clusterwhen present.ctx hub statusrenders the cluster section.Scope for "good first issue"
This is wiring an existing data source (
Cluster.IsLeader(),Cluster.LeaderAddr()) through three layers (response struct → handler → CLI render) plus a small test. No new gRPC method, no auth concerns, no concurrency surprise (the Cluster has its own locking). A newcomer can pattern-match against howTotalEntriesflows from Store → handler → CLI to land this.