iop/apps/control-plane/cmd/control-plane/http_views.go
toki 9024c852df refactor: architecture refactor - fleet polling & client state split
- Move fleet polling logic to control orchestrator
- Split client state management into separate controller/repository
- Add client bootstrap and home page components
- Update HTTP fleet handlers and views
- Add unit tests for client bootstrap and status controller
- Archive completed subtask documents
2026-06-06 19:10:47 +09:00

345 lines
11 KiB
Go

package main
import (
"time"
iop "iop/proto/gen/iop"
"iop/apps/control-plane/internal/wire"
)
// ── DTO structs ──────────────────────────────────────────────────────────────
type edgeRegistryResponse struct {
Edges []edgeRegistryView `json:"edges"`
}
type edgeRegistryView struct {
EdgeID string `json:"edge_id"`
EdgeName string `json:"edge_name"`
Version string `json:"version"`
Capabilities []string `json:"capabilities"`
Protocol string `json:"protocol"`
Connected bool `json:"connected"`
LastSeen time.Time `json:"last_seen"`
}
type edgeNodeEventsResponse struct {
EdgeID string `json:"edge_id"`
Events []edgeNodeEventView `json:"events"`
}
type edgeNodeEventView struct {
EdgeID string `json:"edge_id"`
EventID string `json:"event_id"`
Type string `json:"type"`
Source string `json:"source"`
NodeID string `json:"node_id"`
Alias string `json:"alias"`
Reason string `json:"reason"`
Timestamp time.Time `json:"timestamp"`
ReceivedAt time.Time `json:"received_at"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type edgeStatusResponseView struct {
RequestID string `json:"request_id"`
EdgeID string `json:"edge_id"`
EdgeName string `json:"edge_name"`
ObservedTimeUnixNano int64 `json:"observed_time_unix_nano"`
Nodes []edgeNodeSnapshotView `json:"nodes"`
Capabilities []edgeCapabilitySummaryView `json:"capabilities,omitempty"`
DomainAgents []edgeDomainAgentSummaryView `json:"domain_agents,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Error string `json:"error,omitempty"`
}
type edgeCapabilitySummaryView struct {
Kind string `json:"kind"`
Available bool `json:"available"`
Status string `json:"status,omitempty"`
Summary string `json:"summary,omitempty"`
}
type edgeDomainAgentSummaryView struct {
AgentKind string `json:"agent_kind"`
Available bool `json:"available"`
LifecycleState string `json:"lifecycle_state,omitempty"`
ActiveCommandID string `json:"active_command_id,omitempty"`
Summary string `json:"summary,omitempty"`
}
// edgeCommandRequestBody is the JSON body for POST /edges/{id}/commands and
// POST /fleet/commands. It carries the surface-neutral command intent only.
type edgeCommandRequestBody struct {
Operation string `json:"operation"`
TargetSelector string `json:"target_selector,omitempty"`
Parameters map[string]string `json:"parameters,omitempty"`
}
type edgeCommandResponseView struct {
RequestID string `json:"request_id,omitempty"`
CommandID string `json:"command_id"`
EdgeID string `json:"edge_id"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Error string `json:"error,omitempty"`
}
type edgeOperationsResponse struct {
EdgeID string `json:"edge_id"`
Operations []edgeCommandRecordView `json:"operations"`
}
type edgeCommandRecordView struct {
EdgeID string `json:"edge_id"`
CommandID string `json:"command_id"`
Operation string `json:"operation,omitempty"`
TargetSelector string `json:"target_selector,omitempty"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Error string `json:"error,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Events []edgeCommandEventView `json:"events,omitempty"`
}
type edgeCommandEventView struct {
Phase string `json:"phase,omitempty"`
Summary string `json:"summary,omitempty"`
OccurredAt time.Time `json:"occurred_at,omitempty"`
ReceivedAt time.Time `json:"received_at"`
}
type fleetStatusResponse struct {
GeneratedAt time.Time `json:"generated_at"`
Edges []fleetEdgeView `json:"edges"`
}
type fleetEdgeView struct {
EdgeID string `json:"edge_id"`
EdgeName string `json:"edge_name,omitempty"`
Version string `json:"version,omitempty"`
Protocol string `json:"protocol,omitempty"`
Connected bool `json:"connected"`
Health string `json:"health"`
LastSeen time.Time `json:"last_seen"`
NodeCount int `json:"node_count"`
Capabilities []edgeCapabilitySummaryView `json:"capabilities,omitempty"`
DomainAgents []edgeDomainAgentSummaryView `json:"domain_agents,omitempty"`
Error string `json:"error,omitempty"`
}
type fleetCommandResponse struct {
Operation string `json:"operation"`
Results []edgeCommandResponseView `json:"results"`
}
type edgeNodeSnapshotView struct {
NodeID string `json:"node_id"`
Alias string `json:"alias"`
Label string `json:"label"`
Connected bool `json:"connected"`
Config *nodeConfigSummaryView `json:"config,omitempty"`
}
type nodeConfigSummaryView struct {
Adapters []adapterSummaryView `json:"adapters"`
Concurrency int32 `json:"concurrency"`
}
type adapterSummaryView struct {
Type string `json:"type"`
Enabled bool `json:"enabled"`
}
// ── Transformation functions ─────────────────────────────────────────────────
func newNodeConfigSummaryView(payload *iop.NodeConfigPayload) *nodeConfigSummaryView {
if payload == nil {
return nil
}
var adapters []adapterSummaryView
for _, a := range payload.GetAdapters() {
adapters = append(adapters, adapterSummaryView{
Type: a.GetType(),
Enabled: a.GetEnabled(),
})
}
concurrency := int32(0)
if payload.GetRuntime() != nil {
concurrency = payload.GetRuntime().GetConcurrency()
}
return &nodeConfigSummaryView{
Adapters: adapters,
Concurrency: concurrency,
}
}
func edgeRegistryViewFromState(state wire.EdgeConnectionState) edgeRegistryView {
return edgeRegistryView{
EdgeID: state.EdgeID,
EdgeName: state.EdgeName,
Version: state.Version,
Capabilities: append([]string(nil), state.Capabilities...),
Protocol: state.Protocol,
Connected: state.Connected,
LastSeen: state.LastSeen,
}
}
func edgeStatusResponseViewFromProto(resp *iop.EdgeStatusResponse) edgeStatusResponseView {
nodes := make([]edgeNodeSnapshotView, 0, len(resp.GetNodes()))
for _, n := range resp.GetNodes() {
nodes = append(nodes, edgeNodeSnapshotView{
NodeID: n.GetNodeId(),
Alias: n.GetAlias(),
Label: n.GetLabel(),
Connected: n.GetConnected(),
Config: newNodeConfigSummaryView(n.GetConfig()),
})
}
return edgeStatusResponseView{
RequestID: resp.GetRequestId(),
EdgeID: resp.GetEdgeId(),
EdgeName: resp.GetEdgeName(),
ObservedTimeUnixNano: resp.GetObservedTimeUnixNano(),
Nodes: nodes,
Capabilities: edgeCapabilityViews(resp.GetCapabilities()),
DomainAgents: edgeDomainAgentViews(resp.GetDomainAgents()),
Metadata: resp.GetMetadata(),
Error: resp.GetError(),
}
}
func edgeCapabilityViews(caps []*iop.EdgeCapabilitySummary) []edgeCapabilitySummaryView {
if len(caps) == 0 {
return nil
}
views := make([]edgeCapabilitySummaryView, 0, len(caps))
for _, c := range caps {
views = append(views, edgeCapabilitySummaryView{
Kind: c.GetKind(),
Available: c.GetAvailable(),
Status: c.GetStatus(),
Summary: c.GetSummary(),
})
}
return views
}
func edgeDomainAgentViews(agents []*iop.EdgeDomainAgentSummary) []edgeDomainAgentSummaryView {
if len(agents) == 0 {
return nil
}
views := make([]edgeDomainAgentSummaryView, 0, len(agents))
for _, a := range agents {
views = append(views, edgeDomainAgentSummaryView{
AgentKind: a.GetAgentKind(),
Available: a.GetAvailable(),
LifecycleState: a.GetLifecycleState(),
ActiveCommandID: a.GetActiveCommandId(),
Summary: a.GetSummary(),
})
}
return views
}
func edgeCommandResponseViewFromProto(resp *iop.EdgeCommandResponse) edgeCommandResponseView {
return edgeCommandResponseView{
RequestID: resp.GetRequestId(),
CommandID: resp.GetCommandId(),
EdgeID: resp.GetEdgeId(),
Status: resp.GetStatus(),
Summary: resp.GetSummary(),
Error: resp.GetError(),
}
}
func edgeCommandRecordViewFromRecord(record wire.EdgeCommandRecord) edgeCommandRecordView {
view := edgeCommandRecordView{
EdgeID: record.EdgeID,
CommandID: record.CommandID,
Operation: record.Operation,
TargetSelector: record.TargetSelector,
Status: record.Status,
Summary: record.Summary,
Error: record.Error,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
}
if len(record.Events) > 0 {
view.Events = make([]edgeCommandEventView, 0, len(record.Events))
for _, e := range record.Events {
view.Events = append(view.Events, edgeCommandEventView{
Phase: e.Phase,
Summary: e.Summary,
OccurredAt: e.OccurredAt,
ReceivedAt: e.ReceivedAt,
})
}
}
return view
}
func edgeNodeEventViewFromRecord(record wire.EdgeNodeEventRecord) edgeNodeEventView {
event := record.Event
view := edgeNodeEventView{
EdgeID: record.EdgeID,
ReceivedAt: record.ReceivedAt,
}
if event == nil {
return view
}
if event.GetTimestamp() != 0 {
view.Timestamp = time.Unix(0, event.GetTimestamp()).UTC()
}
view.EventID = event.GetEventId()
view.Type = event.GetType()
view.Source = event.GetSource()
view.NodeID = event.GetNodeId()
view.Alias = event.GetAlias()
view.Reason = event.GetReason()
if metadata := event.GetMetadata(); len(metadata) > 0 {
view.Metadata = make(map[string]string, len(metadata))
for k, v := range metadata {
view.Metadata[k] = v
}
}
return view
}
func fleetEdgeViewFromState(state wire.EdgeConnectionState, requestStatus edgeStatusRequester, statusTimeout time.Duration) fleetEdgeView {
view := fleetEdgeView{
EdgeID: state.EdgeID,
EdgeName: state.EdgeName,
Version: state.Version,
Protocol: state.Protocol,
Connected: state.Connected,
LastSeen: state.LastSeen,
}
if !state.Connected {
view.Health = "offline"
return view
}
if requestStatus == nil {
view.Health = "online"
return view
}
resp, err := requestStatus(state.EdgeID, statusTimeout)
if err != nil {
view.Health = "degraded"
view.Error = err.Error()
return view
}
view.NodeCount = len(resp.GetNodes())
view.Capabilities = edgeCapabilityViews(resp.GetCapabilities())
view.DomainAgents = edgeDomainAgentViews(resp.GetDomainAgents())
if resp.GetError() != "" {
view.Health = "degraded"
view.Error = resp.GetError()
return view
}
view.Health = "online"
return view
}