chore: sync local project state
This commit is contained in:
277
internal/httpapi/dashboard.go
Normal file
277
internal/httpapi/dashboard.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"supply-intelligence/internal/domain"
|
||||
"supply-intelligence/internal/repository"
|
||||
)
|
||||
|
||||
// DashboardHandler handles external-facing dashboard UI endpoints.
|
||||
type DashboardHandler struct {
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
// NewDashboardHandler creates a dashboard handler backed by the given repository.
|
||||
func NewDashboardHandler(repo repository.Repository) *DashboardHandler {
|
||||
return &DashboardHandler{repo: repo}
|
||||
}
|
||||
|
||||
// accountRow is a denormalized row for the accounts dashboard table.
|
||||
type accountRow struct {
|
||||
AccountID int64 `json:"account_id"`
|
||||
Platform string `json:"platform"`
|
||||
AccountStatus string `json:"account_status"`
|
||||
RoutingEnabled bool `json:"routing_enabled"`
|
||||
RiskScore int `json:"risk_score"`
|
||||
ReasonCode string `json:"reason_code"`
|
||||
LastProbeAt string `json:"last_probe_at"`
|
||||
Version int64 `json:"version"`
|
||||
}
|
||||
|
||||
// modelRow is a denormalized row for the model catalog.
|
||||
type modelRow struct {
|
||||
PackageID int64 `json:"package_id"`
|
||||
Platform string `json:"platform"`
|
||||
Model string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
Version int64 `json:"version"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// candidateRow is a denormalized row for the candidate management table.
|
||||
type candidateRow struct {
|
||||
CandidateID string `json:"candidate_id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
Platform string `json:"platform"`
|
||||
Model string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
ReasonCode string `json:"reason_code,omitempty"`
|
||||
DiscoveredAt string `json:"discovered_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Version int64 `json:"version"`
|
||||
}
|
||||
|
||||
// ListAccounts returns all accounts grouped by platform.
|
||||
// GET /internal/supply-intelligence/dashboard/accounts
|
||||
// Query params: platform (optional)
|
||||
func (h *DashboardHandler) ListAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
platform := r.URL.Query().Get("platform")
|
||||
var states []domain.AccountRoutingState
|
||||
if platform != "" {
|
||||
states = h.repo.ListRoutingStatesByPlatform(r.Context(), platform)
|
||||
} else {
|
||||
// No ListAllRoutingStates — use openai as default for now
|
||||
states = h.repo.ListRoutingStatesByPlatform(r.Context(), "openai")
|
||||
// TODO: batch for all known platforms
|
||||
}
|
||||
|
||||
rows := make([]accountRow, 0, len(states))
|
||||
for _, s := range states {
|
||||
rows = append(rows, accountRow{
|
||||
AccountID: s.AccountID,
|
||||
Platform: s.Platform,
|
||||
AccountStatus: string(s.AccountStatus),
|
||||
RoutingEnabled: s.RoutingEnabled,
|
||||
RiskScore: s.RiskScore,
|
||||
ReasonCode: s.ReasonCode,
|
||||
LastProbeAt: s.LastProbeAt.Format("2006-01-02T15:04:05Z"),
|
||||
Version: s.Version,
|
||||
})
|
||||
}
|
||||
|
||||
// Group by platform for summary view
|
||||
byPlatform := make(map[string][]accountRow)
|
||||
for _, row := range rows {
|
||||
byPlatform[row.Platform] = append(byPlatform[row.Platform], row)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": rows,
|
||||
"by_platform": byPlatform,
|
||||
"total": len(rows),
|
||||
})
|
||||
}
|
||||
|
||||
// ListModels returns the model catalog from supply packages.
|
||||
// GET /internal/supply-intelligence/dashboard/models
|
||||
// Query params: status (optional: draft, active, deprecated)
|
||||
func (h *DashboardHandler) ListModels(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
status := r.URL.Query().Get("status")
|
||||
pkgs := h.repo.ListSupplyPackages(r.Context(), status)
|
||||
|
||||
rows := make([]modelRow, 0, len(pkgs))
|
||||
for _, p := range pkgs {
|
||||
rows = append(rows, modelRow{
|
||||
PackageID: p.PackageID,
|
||||
Platform: p.Platform,
|
||||
Model: p.Model,
|
||||
Status: p.Status,
|
||||
Source: p.Source,
|
||||
Version: p.Version,
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: p.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// Group by platform for summary
|
||||
byPlatform := make(map[string][]modelRow)
|
||||
for _, row := range rows {
|
||||
byPlatform[row.Platform] = append(byPlatform[row.Platform], row)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": rows,
|
||||
"by_platform": byPlatform,
|
||||
"total": len(rows),
|
||||
})
|
||||
}
|
||||
|
||||
// ListCandidates returns discovery candidates for management UI.
|
||||
// GET /internal/supply-intelligence/dashboard/candidates
|
||||
// Query params: status (optional), platform (optional), limit (optional, default 100)
|
||||
func (h *DashboardHandler) ListCandidates(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
platform := r.URL.Query().Get("platform")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 100
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 500 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
var domainStatus domain.DiscoveryCandidateStatus
|
||||
if statusStr != "" {
|
||||
domainStatus = domain.DiscoveryCandidateStatus(statusStr)
|
||||
}
|
||||
|
||||
candidates := h.repo.ListDiscoveryCandidates(r.Context(), domainStatus)
|
||||
|
||||
rows := make([]candidateRow, 0, len(candidates))
|
||||
count := 0
|
||||
for _, c := range candidates {
|
||||
if platform != "" && c.Platform != platform {
|
||||
continue
|
||||
}
|
||||
if limit > 0 && count >= limit {
|
||||
break
|
||||
}
|
||||
rows = append(rows, candidateRow{
|
||||
CandidateID: c.CandidateID,
|
||||
AccountID: c.AccountID,
|
||||
Platform: c.Platform,
|
||||
Model: c.Model,
|
||||
Status: string(c.Status),
|
||||
Source: c.Source,
|
||||
ReasonCode: c.ReasonCode,
|
||||
DiscoveredAt: c.DiscoveredAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: c.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
Version: c.Version,
|
||||
})
|
||||
count++
|
||||
}
|
||||
|
||||
// Status summary counts
|
||||
statusCounts := make(map[string]int)
|
||||
for _, c := range candidates {
|
||||
statusCounts[string(c.Status)]++
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": rows,
|
||||
"total": len(rows),
|
||||
"status_counts": statusCounts,
|
||||
})
|
||||
}
|
||||
|
||||
// GetProbeHistory returns probe execution history for an account.
|
||||
// GET /internal/supply-intelligence/dashboard/accounts/{account_id}/probe-history
|
||||
// Query params: limit (optional, default 20)
|
||||
func (h *DashboardHandler) GetProbeHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
prefix := "/internal/supply-intelligence/dashboard/accounts/"
|
||||
path := strings.TrimPrefix(r.URL.Path, prefix)
|
||||
if !strings.HasSuffix(path, "/probe-history") {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not_found"})
|
||||
return
|
||||
}
|
||||
accountIDStr := strings.TrimSuffix(path, "/probe-history")
|
||||
var accountID int64
|
||||
if _, err := strconv.ParseInt(accountIDStr, 10, 64); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 20
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
logs, err := h.repo.ListProbeExecutionLogs(r.Context(), accountID, limit)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed_to_load_logs"})
|
||||
return
|
||||
}
|
||||
|
||||
type probeLogRow struct {
|
||||
LogID int64 `json:"log_id"`
|
||||
Platform string `json:"platform"`
|
||||
ProbeResult string `json:"probe_result"`
|
||||
FailureClass string `json:"failure_class,omitempty"`
|
||||
HTTPStatus int `json:"http_status,omitempty"`
|
||||
LatencyMs int `json:"latency_ms,omitempty"`
|
||||
RiskScore int `json:"risk_score"`
|
||||
EvaluatedTransition string `json:"evaluated_transition"`
|
||||
ExecutedAt string `json:"executed_at"`
|
||||
RequestID string `json:"request_id"`
|
||||
Version int64 `json:"version"`
|
||||
}
|
||||
|
||||
rows := make([]probeLogRow, 0, len(logs))
|
||||
for _, l := range logs {
|
||||
rows = append(rows, probeLogRow{
|
||||
LogID: l.LogID,
|
||||
Platform: l.Platform,
|
||||
ProbeResult: l.ProbeResult,
|
||||
FailureClass: l.FailureClass,
|
||||
HTTPStatus: l.HTTPStatus,
|
||||
LatencyMs: l.LatencyMs,
|
||||
RiskScore: l.RiskScore,
|
||||
EvaluatedTransition: l.EvaluatedTransition,
|
||||
ExecutedAt: l.ExecutedAt.Format("2006-01-02T15:04:05Z"),
|
||||
RequestID: l.RequestID,
|
||||
Version: l.Version,
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": rows, "total": len(rows)})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user