diff --git a/audit/internal/webui/api.go b/audit/internal/webui/api.go index ca20c94..2fe53bd 100644 --- a/audit/internal/webui/api.go +++ b/audit/internal/webui/api.go @@ -33,8 +33,27 @@ var apiListNvidiaGPUs = func(a *app.App) ([]platform.NvidiaGPU, error) { var jobCounter atomic.Uint64 -func newJobID(prefix string) string { - return fmt.Sprintf("%s-%d", prefix, jobCounter.Add(1)) +func newJobID(_ string) string { + start := int((jobCounter.Add(1) - 1) % 1000) + globalQueue.mu.Lock() + defer globalQueue.mu.Unlock() + for offset := 0; offset < 1000; offset++ { + n := (start + offset) % 1000 + id := fmt.Sprintf("TASK-%03d", n) + if !taskIDInUseLocked(id) { + return id + } + } + return fmt.Sprintf("TASK-%03d", start) +} + +func taskIDInUseLocked(id string) bool { + for _, t := range globalQueue.tasks { + if t != nil && t.ID == id { + return true + } + } + return false } type taskRunResponse struct { diff --git a/audit/internal/webui/tasks.go b/audit/internal/webui/tasks.go index 35b2e70..f67ea3f 100644 --- a/audit/internal/webui/tasks.go +++ b/audit/internal/webui/tasks.go @@ -1149,7 +1149,32 @@ func taskArtifactsDir(root string, t *Task, status string) string { if strings.TrimSpace(root) == "" || t == nil { return "" } - return filepath.Join(root, fmt.Sprintf("%s_%s_%s", t.ID, sanitizeTaskFolderPart(t.Name), taskFolderStatus(status))) + prefix := taskFolderNumberPrefix(t.ID) + return filepath.Join(root, fmt.Sprintf("%s_%s_%s", prefix, sanitizeTaskFolderPart(t.Name), taskFolderStatus(status))) +} + +func taskFolderNumberPrefix(taskID string) string { + taskID = strings.TrimSpace(taskID) + if strings.HasPrefix(taskID, "TASK-") && len(taskID) >= len("TASK-000") { + num := strings.TrimSpace(strings.TrimPrefix(taskID, "TASK-")) + if len(num) == 3 { + allDigits := true + for _, r := range num { + if r < '0' || r > '9' { + allDigits = false + break + } + } + if allDigits { + return num + } + } + } + fallback := sanitizeTaskFolderPart(taskID) + if fallback == "" { + return "000" + } + return fallback } func ensureTaskReportPaths(t *Task) { diff --git a/audit/internal/webui/tasks_test.go b/audit/internal/webui/tasks_test.go index 21f305b..59fe740 100644 --- a/audit/internal/webui/tasks_test.go +++ b/audit/internal/webui/tasks_test.go @@ -163,6 +163,40 @@ func TestTaskQueueSnapshotSortsNewestFirst(t *testing.T) { } } +func TestNewJobIDUsesTASKPrefixAndZeroPadding(t *testing.T) { + globalQueue.mu.Lock() + origTasks := globalQueue.tasks + globalQueue.tasks = nil + globalQueue.mu.Unlock() + origCounter := jobCounter.Load() + jobCounter.Store(0) + t.Cleanup(func() { + globalQueue.mu.Lock() + globalQueue.tasks = origTasks + globalQueue.mu.Unlock() + jobCounter.Store(origCounter) + }) + + if got := newJobID("ignored"); got != "TASK-000" { + t.Fatalf("id=%q want TASK-000", got) + } + if got := newJobID("ignored"); got != "TASK-001" { + t.Fatalf("id=%q want TASK-001", got) + } +} + +func TestTaskArtifactsDirStartsWithTaskNumber(t *testing.T) { + root := t.TempDir() + task := &Task{ + ID: "TASK-007", + Name: "NVIDIA Benchmark", + } + got := filepath.Base(taskArtifactsDir(root, task, TaskDone)) + if !strings.HasPrefix(got, "007_") { + t.Fatalf("artifacts dir=%q want prefix 007_", got) + } +} + func TestHandleAPITasksStreamReplaysPersistedLogWithoutLiveJob(t *testing.T) { dir := t.TempDir() logPath := filepath.Join(dir, "task.log")