/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>
131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
)
|
|
|
|
// newMeshJobsCmd is the 3D models clients asked for, as jobs: every owner's,
|
|
// what each is doing, and a way to stop one. (ctl jobs is the control
|
|
// plane's own work, serving and updating models.)
|
|
func newMeshJobsCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "3d",
|
|
Short: "3D models clients asked for: the jobs, how far along each is, and stopping one",
|
|
}
|
|
var status string
|
|
var limit int
|
|
list := &cobra.Command{
|
|
Use: "list",
|
|
Short: "Every owner's 3D jobs, newest first",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
q := url.Values{}
|
|
if status != "" {
|
|
q.Set("status", status)
|
|
}
|
|
if limit > 0 {
|
|
q.Set("limit", fmt.Sprint(limit))
|
|
}
|
|
var jobs []adminapi.JobView
|
|
if err := client().get(cmd.Context(), "/api/v1/client-jobs?"+q.Encode(), &jobs); err != nil {
|
|
return err
|
|
}
|
|
if *asJSON {
|
|
return emitJSON(jobs)
|
|
}
|
|
if len(jobs) == 0 {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "no 3D jobs")
|
|
return nil
|
|
}
|
|
w := tabTo(cmd.OutOrStdout())
|
|
fmt.Fprintln(w, "JOB\tMODEL\tSTATUS\tPROGRESS\tOWNER\tASKED")
|
|
for _, j := range jobs {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s ago\n", j.ID, j.Model, j.Status, progressText(j),
|
|
j.Owner, time.Since(j.CreatedAt).Round(time.Second))
|
|
}
|
|
return w.Flush()
|
|
},
|
|
}
|
|
list.Flags().StringVar(&status, "status", "", "only jobs in this status: queued, running, completed, failed…")
|
|
list.Flags().IntVar(&limit, "limit", 0, "the most jobs to show (50)")
|
|
get := &cobra.Command{
|
|
Use: "get JOB",
|
|
Short: "One 3D job: its status, progress, result or error",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var j adminapi.JobView
|
|
if err := client().get(cmd.Context(), "/api/v1/client-jobs/"+url.PathEscape(args[0]), &j); err != nil {
|
|
return err
|
|
}
|
|
if *asJSON {
|
|
return emitJSON(j)
|
|
}
|
|
out := cmd.OutOrStdout()
|
|
fmt.Fprintf(out, "job %s\nmodel %s\nstatus %s\n", j.ID, j.Model, j.Status)
|
|
if p := progressText(j); p != "" {
|
|
fmt.Fprintf(out, "progress %s\n", p)
|
|
}
|
|
fmt.Fprintf(out, "owner %s (key %s)\nasked %s\n", j.Owner, j.KeyID, j.CreatedAt.UTC().Format(time.RFC3339))
|
|
if j.FinishedAt != nil && j.StartedAt != nil {
|
|
fmt.Fprintf(out, "took %s\n", j.FinishedAt.Sub(*j.StartedAt).Round(time.Second))
|
|
}
|
|
if j.OutputFileID != "" {
|
|
fmt.Fprintf(out, "result %s (%s)\n", j.OutputFileID, j.ContentType)
|
|
}
|
|
if j.Error != "" {
|
|
fmt.Fprintf(out, "error %s\n", j.Error)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
cancel := &cobra.Command{
|
|
Use: "cancel JOB",
|
|
Short: "Stop a 3D job, in the engine's queue too",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var j adminapi.JobView
|
|
if err := client().post(cmd.Context(), "/api/v1/client-jobs/"+url.PathEscape(args[0])+"/cancel", nil, &j); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%s is %s\n", j.ID, j.Status)
|
|
return nil
|
|
},
|
|
}
|
|
cmd.AddCommand(list, get, cancel)
|
|
return cmd
|
|
}
|
|
|
|
// progressText is a job's progress as a person reads it.
|
|
func progressText(j adminapi.JobView) string {
|
|
if len(j.Progress) == 0 {
|
|
return ""
|
|
}
|
|
var p struct {
|
|
State string `json:"state"`
|
|
Ahead int `json:"ahead"`
|
|
NodeClass string `json:"node_class"`
|
|
Step int `json:"step"`
|
|
Steps int `json:"steps"`
|
|
Percent int `json:"percent"`
|
|
}
|
|
if json.Unmarshal(j.Progress, &p) != nil {
|
|
return ""
|
|
}
|
|
switch {
|
|
case p.State == "queued" && p.Ahead > 0:
|
|
return fmt.Sprintf("queued, %d ahead", p.Ahead)
|
|
case p.State == "running" && p.Steps > 0:
|
|
return fmt.Sprintf("running %d%%, %s step %d/%d", p.Percent, p.NodeClass, p.Step, p.Steps)
|
|
case p.State == "running" && p.NodeClass != "":
|
|
return "running " + p.NodeClass
|
|
}
|
|
return p.State
|
|
}
|