/3d beside /images, while some model makes meshes: a picture chosen, the
job asked for through the same path an API client's takes, its progress
shown (waiting for the card, n ahead; making it, the node and percent),
and the mesh in a viewer — model-viewer 4.4.0 (Apache 2.0, with three.js
and lit), served from the gateway since the page's policy loads scripts
from nowhere else — with a download and the model's licence. The mesh is
served to its owner from /3d/jobs/{id}/file; another project's person is
told there is no such job.
A job made from the portal acted for a key that does not exist: the
worker now rebuilds a session's principal from the account that made it,
while it is active and still in the project the job is charged to.
ctl 3d list|get|cancel, over /api/v1/client-jobs (/api/v1/jobs is the
control plane's own work, which ctl jobs shows).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
package adminapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// Jobs — 3D models asked for and asked after — as an operator sees them:
|
|
// every owner's, which a client sees only of its own.
|
|
|
|
// jobStore is what jobs need of the store; asked of the usage source by
|
|
// type assertion, as requestFinder is.
|
|
type jobStore interface {
|
|
AllJobs(ctx context.Context, status string, limit int) ([]store.Job, error)
|
|
JobByID(ctx context.Context, id string) (store.Job, error)
|
|
CancelJob(ctx context.Context, id string, now time.Time) (store.Job, error)
|
|
}
|
|
|
|
// JobView is one job.
|
|
type JobView struct {
|
|
ID string `json:"id"`
|
|
Owner string `json:"owner"`
|
|
KeyID string `json:"key_id"`
|
|
Endpoint string `json:"endpoint"`
|
|
Model string `json:"model"`
|
|
Status string `json:"status"`
|
|
Progress json.RawMessage `json:"progress,omitempty"`
|
|
OutputFileID string `json:"output_file_id,omitempty"`
|
|
ContentType string `json:"content_type,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
|
}
|
|
|
|
func jobView(j store.Job) JobView {
|
|
v := JobView{ID: j.ID, Owner: j.Owner, KeyID: j.KeyID, Endpoint: j.Endpoint, Model: j.Model, Status: j.Status,
|
|
OutputFileID: j.OutputFileID, ContentType: j.ContentType, Error: j.Error, CreatedAt: j.CreatedAt}
|
|
if j.Progress != "" && json.Valid([]byte(j.Progress)) {
|
|
v.Progress = json.RawMessage(j.Progress)
|
|
}
|
|
if !j.StartedAt.IsZero() {
|
|
v.StartedAt = &j.StartedAt
|
|
}
|
|
if !j.FinishedAt.IsZero() {
|
|
v.FinishedAt = &j.FinishedAt
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (a *API) jobs() (jobStore, bool) {
|
|
s, ok := a.usage.(jobStore)
|
|
return s, ok
|
|
}
|
|
|
|
func (a *API) listJobs(w http.ResponseWriter, r *http.Request) {
|
|
s, ok := a.jobs()
|
|
if !ok {
|
|
writeError(w, http.StatusNotImplemented, "jobs need the store, which this control plane runs without")
|
|
return
|
|
}
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
js, err := s.AllJobs(r.Context(), r.URL.Query().Get("status"), limit)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
out := make([]JobView, 0, len(js))
|
|
for _, j := range js {
|
|
out = append(out, jobView(j))
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (a *API) getJob(w http.ResponseWriter, r *http.Request) {
|
|
s, ok := a.jobs()
|
|
if !ok {
|
|
writeError(w, http.StatusNotImplemented, "jobs need the store, which this control plane runs without")
|
|
return
|
|
}
|
|
j, err := s.JobByID(r.Context(), r.PathValue("id"))
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "no job "+r.PathValue("id"))
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, jobView(j))
|
|
}
|
|
|
|
func (a *API) cancelJob(w http.ResponseWriter, r *http.Request) {
|
|
s, ok := a.jobs()
|
|
if !ok {
|
|
writeError(w, http.StatusNotImplemented, "jobs need the store, which this control plane runs without")
|
|
return
|
|
}
|
|
if _, err := s.JobByID(r.Context(), r.PathValue("id")); errors.Is(err, store.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "no job "+r.PathValue("id"))
|
|
return
|
|
}
|
|
j, err := s.CancelJob(r.Context(), r.PathValue("id"), time.Now())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, jobView(j))
|
|
}
|