Files
Ilker ManapandClaude Opus 5.5 2f54780802 G4: a page that makes 3D models, and ctl 3d
/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>
2026-10-10 19:16:20 +02:00

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))
}