step one
This commit is contained in:
parent
7a8d5d13fa
commit
9e663db9dc
68 changed files with 5647 additions and 2958 deletions
206
internal/api/admin/datasets.go
Normal file
206
internal/api/admin/datasets.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
// Package admin implements dataset-management HTTP endpoints used by the
|
||||
// stratoflights operator console.
|
||||
//
|
||||
// Endpoints:
|
||||
//
|
||||
// GET /api/v1/admin/datasets list stored epochs
|
||||
// POST /api/v1/admin/datasets trigger a download
|
||||
// DELETE /api/v1/admin/datasets/{epoch} delete a stored epoch
|
||||
// GET /api/v1/admin/jobs list all jobs
|
||||
// GET /api/v1/admin/jobs/{id} fetch one job
|
||||
// DELETE /api/v1/admin/jobs/{id} cancel a running job
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"predictor-refactored/internal/datasets"
|
||||
)
|
||||
|
||||
// Handler serves all /api/v1/admin/* endpoints.
|
||||
type Handler struct {
|
||||
mgr *datasets.Manager
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// New wires an admin handler.
|
||||
func New(mgr *datasets.Manager, log *zap.Logger) *Handler {
|
||||
if log == nil {
|
||||
log = zap.NewNop()
|
||||
}
|
||||
return &Handler{mgr: mgr, log: log}
|
||||
}
|
||||
|
||||
// Register installs admin routes on mux. Routes are mounted under
|
||||
// /api/v1/admin/...
|
||||
func (h *Handler) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/v1/admin/datasets", h.listDatasets)
|
||||
mux.HandleFunc("POST /api/v1/admin/datasets", h.triggerDownload)
|
||||
mux.HandleFunc("DELETE /api/v1/admin/datasets/{epoch}", h.deleteDataset)
|
||||
mux.HandleFunc("GET /api/v1/admin/jobs", h.listJobs)
|
||||
mux.HandleFunc("GET /api/v1/admin/jobs/{id}", h.getJob)
|
||||
mux.HandleFunc("DELETE /api/v1/admin/jobs/{id}", h.cancelJob)
|
||||
}
|
||||
|
||||
// listDatasets handles GET /api/v1/admin/datasets.
|
||||
func (h *Handler) listDatasets(w http.ResponseWriter, _ *http.Request) {
|
||||
epochs, err := h.mgr.ListEpochs()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
active := ""
|
||||
if a := h.mgr.Active(); a != nil {
|
||||
active = a.Epoch().UTC().Format(time.RFC3339)
|
||||
}
|
||||
out := struct {
|
||||
Source string `json:"source"`
|
||||
Active string `json:"active,omitempty"`
|
||||
Epochs []string `json:"epochs"`
|
||||
}{
|
||||
Source: h.mgr.Source(),
|
||||
Active: active,
|
||||
}
|
||||
for _, e := range epochs {
|
||||
out.Epochs = append(out.Epochs, e.UTC().Format(time.RFC3339))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// triggerDownload handles POST /api/v1/admin/datasets.
|
||||
//
|
||||
// Body: {"epoch": "2026-03-28T06:00:00Z"} OR {"latest": true}.
|
||||
func (h *Handler) triggerDownload(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Epoch string `json:"epoch,omitempty"`
|
||||
Latest bool `json:"latest,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body: "+err.Error())
|
||||
return
|
||||
}
|
||||
if !body.Latest && body.Epoch == "" {
|
||||
writeError(w, http.StatusBadRequest, "specify either epoch or latest=true")
|
||||
return
|
||||
}
|
||||
|
||||
var epoch time.Time
|
||||
if body.Latest {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
jobID, err := h.mgr.Refresh(ctx, 0)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"job_id": jobID})
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
epoch, err = time.Parse(time.RFC3339, body.Epoch)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid epoch: "+err.Error())
|
||||
return
|
||||
}
|
||||
jobID := h.mgr.Download(epoch)
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"job_id": jobID})
|
||||
}
|
||||
|
||||
// deleteDataset handles DELETE /api/v1/admin/datasets/{epoch}.
|
||||
func (h *Handler) deleteDataset(w http.ResponseWriter, r *http.Request) {
|
||||
rawEpoch := r.PathValue("epoch")
|
||||
epoch, err := time.Parse(time.RFC3339, rawEpoch)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid epoch: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.mgr.RemoveEpoch(epoch); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// listJobs handles GET /api/v1/admin/jobs.
|
||||
func (h *Handler) listJobs(w http.ResponseWriter, _ *http.Request) {
|
||||
jobs := h.mgr.ListJobs()
|
||||
out := make([]jobDTO, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
out = append(out, toDTO(j))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// getJob handles GET /api/v1/admin/jobs/{id}.
|
||||
func (h *Handler) getJob(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
job, ok := h.mgr.GetJob(id)
|
||||
if !ok {
|
||||
writeError(w, http.StatusNotFound, "job not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, toDTO(job))
|
||||
}
|
||||
|
||||
// cancelJob handles DELETE /api/v1/admin/jobs/{id}.
|
||||
func (h *Handler) cancelJob(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if !h.mgr.CancelJob(id) {
|
||||
writeError(w, http.StatusConflict, "job not found or already terminal")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type jobDTO struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Epoch string `json:"epoch"`
|
||||
Status string `json:"status"`
|
||||
StartedAt string `json:"started_at"`
|
||||
EndedAt string `json:"ended_at,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Total int `json:"total_units"`
|
||||
Done int `json:"done_units"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
}
|
||||
|
||||
func toDTO(j datasets.JobInfo) jobDTO {
|
||||
dto := jobDTO{
|
||||
ID: j.ID,
|
||||
Source: j.Source,
|
||||
Epoch: j.Epoch.UTC().Format(time.RFC3339),
|
||||
Status: string(j.Status),
|
||||
StartedAt: j.StartedAt.UTC().Format(time.RFC3339),
|
||||
Err: j.Err,
|
||||
Total: j.Total,
|
||||
Done: j.Done,
|
||||
Bytes: j.Bytes,
|
||||
}
|
||||
if j.EndedAt != nil {
|
||||
dto.EndedAt = j.EndedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, description string) {
|
||||
writeJSON(w, status, map[string]any{
|
||||
"error": map[string]string{
|
||||
"type": http.StatusText(status),
|
||||
"description": description,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue