Files
Vetting/internal/httpserver/router.go
T
josh bb658a8435
CI / Lint + build + test (push) Has been cancelled
Host detail page + pipeline timeline
Click a tile to open /hosts/{id} — the canonical control surface per
host. Timeline renders every pre-stage, stage, and terminal node in
order, with the current one pulsing, failed ones flagged, and
downstream ones dimmed as skipped. Detail page shows summary, hold
card (when holding), all action buttons, spec diffs, a full-height
log pane, and a collapsed expected-spec YAML.

Tile slims to name, last-seen, status, and one primary action; a
CSS-overlay <a> makes the whole card clickable while buttons stay
receptive via z-index.

Runner.publishTileUpdate now also emits pipeline-{runID} fragments,
and CompleteStage wraps Stages.CompleteByName so stage completions
advance the timeline live — without this the dots only moved on
state transitions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 23:59:43 -04:00

83 lines
2.6 KiB
Go

// Package httpserver assembles the chi router. It lives in its own
// package because it depends on both `api` and `orchestrator`, and
// those two packages must stay import-independent.
package httpserver
import (
"io/fs"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"vetting/internal/api"
"vetting/internal/web"
)
type Deps struct {
UI *api.UI
Agent *api.Agent
LiveDir string // directory containing vmlinuz + initrd.img; "" disables /live
AgentAssetDir string // directory containing vetting-agent-linux-amd64; "" disables /assets
}
func NewRouter(d Deps) http.Handler {
r := chi.NewRouter()
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Use(middleware.Logger)
staticFS, err := fs.Sub(web.Static, "static")
if err != nil {
panic(err)
}
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
if d.LiveDir != "" {
r.Handle("/live/*", http.StripPrefix("/live/", http.FileServer(http.Dir(d.LiveDir))))
}
// Host-mode agent binary is served here so the quick-register
// one-liner can curl it without the operator pre-staging anything.
if d.AgentAssetDir != "" {
r.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServer(http.Dir(d.AgentAssetDir))))
}
// Agent / PXE endpoints — authenticated per-request by bearer token
// or by the unforgeable MAC path parameter.
r.Get("/ipxe/{mac}", d.Agent.IPXEScript)
r.Route("/api/v1/runs/{id}", func(r chi.Router) {
r.Post("/hello", d.Agent.Hello)
r.Post("/claim", d.Agent.Claim)
r.Post("/heartbeat", d.Agent.Heartbeat)
r.Post("/log", d.Agent.Log)
r.Post("/result", d.Agent.Result)
r.Post("/hold", d.Agent.Hold)
r.Post("/sensor", d.Agent.Sensor)
})
// Quick-register: the bash one-liner fetched from /register/quick.sh
// POSTs here from the target host. LAN-trusted, same threat model
// as the browser UI.
r.Post("/api/v1/hosts", d.UI.CreateHostJSON)
// Host-mode agent heartbeat. Keyed by MAC (no bearer token), same
// LAN-trust model as /api/v1/hosts.
r.Post("/api/v1/hosts/{mac}/heartbeat", d.UI.Heartbeat)
// Browser UI — no auth; bind to loopback or LAN only, or front
// with a reverse proxy if you need a password.
r.Get("/", d.UI.Dashboard)
r.Get("/hosts/new", d.UI.NewHostForm)
r.Post("/hosts", d.UI.CreateHost)
r.Get("/hosts/{id}", d.UI.HostDetail)
r.Post("/hosts/{id}/delete", d.UI.DeleteHost)
r.Post("/hosts/{id}/start", d.UI.StartRun)
r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage)
r.Get("/reports/{runID}", d.UI.Report)
r.Get("/register/quick.sh", d.UI.QuickRegisterScript)
r.Get("/events", d.UI.SSE)
return r
}