feat(webui): add POST /api/sat/abort + update bible-local runtime-flows

- jobState now has optional cancel func; abort() calls it if job is running
- handleAPISATRun passes cancellable context to RunNvidiaAcceptancePackWithOptions
- POST /api/sat/abort?job_id=... cancels the running SAT job
- bible-local/runtime-flows.md: replace TUI SAT flow with Web UI flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 10:23:00 +03:00
parent 9e55728053
commit a393dcb731
4 changed files with 51 additions and 33 deletions

View File

@@ -155,8 +155,11 @@ func (h *handler) handleAPISATRun(target string) http.HandlerFunc {
}
id := newJobID("sat-" + target)
j := globalJobs.create(id)
ctx, cancel := context.WithCancel(context.Background())
j.cancel = cancel
go func() {
defer cancel()
j.append(fmt.Sprintf("Starting %s acceptance test...", target))
var (
archive string
@@ -178,7 +181,7 @@ func (h *handler) handleAPISATRun(target string) http.HandlerFunc {
case "nvidia":
if len(body.GPUIndices) > 0 || body.DiagLevel > 0 {
result, e := h.opts.App.RunNvidiaAcceptancePackWithOptions(
context.Background(), "", body.DiagLevel, body.GPUIndices,
ctx, "", body.DiagLevel, body.GPUIndices,
)
if e != nil {
err = e
@@ -201,8 +204,13 @@ func (h *handler) handleAPISATRun(target string) http.HandlerFunc {
}
if err != nil {
j.append("ERROR: " + err.Error())
j.finish(err.Error())
if ctx.Err() != nil {
j.append("Aborted.")
j.finish("aborted")
} else {
j.append("ERROR: " + err.Error())
j.finish(err.Error())
}
return
}
j.append(fmt.Sprintf("Archive written: %s", archive))
@@ -223,6 +231,20 @@ func (h *handler) handleAPISATStream(w http.ResponseWriter, r *http.Request) {
streamJob(w, r, j)
}
func (h *handler) handleAPISATAbort(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("job_id")
j, ok := globalJobs.get(id)
if !ok {
http.Error(w, "job not found", http.StatusNotFound)
return
}
if j.abort() {
writeJSON(w, map[string]string{"status": "aborted"})
} else {
writeJSON(w, map[string]string{"status": "not_running"})
}
}
// ── Services ──────────────────────────────────────────────────────────────────
func (h *handler) handleAPIServicesList(w http.ResponseWriter, r *http.Request) {

View File

@@ -7,12 +7,23 @@ import (
// jobState holds the output lines and completion status of an async job.
type jobState struct {
lines []string
done bool
err string
mu sync.Mutex
// subs is a list of channels that receive new lines as they arrive.
subs []chan string
lines []string
done bool
err string
mu sync.Mutex
subs []chan string
cancel func() // optional cancel function; nil if job is not cancellable
}
// abort cancels the job if it has a cancel function and is not yet done.
func (j *jobState) abort() bool {
j.mu.Lock()
defer j.mu.Unlock()
if j.done || j.cancel == nil {
return false
}
j.cancel()
return true
}
func (j *jobState) append(line string) {

View File

@@ -132,6 +132,7 @@ func NewHandler(opts HandlerOptions) http.Handler {
mux.HandleFunc("POST /api/sat/storage/run", h.handleAPISATRun("storage"))
mux.HandleFunc("POST /api/sat/cpu/run", h.handleAPISATRun("cpu"))
mux.HandleFunc("GET /api/sat/stream", h.handleAPISATStream)
mux.HandleFunc("POST /api/sat/abort", h.handleAPISATAbort)
// Services
mux.HandleFunc("GET /api/services", h.handleAPIServicesList)