Add job detail page with activity log and cancel support
Operations are now clickable from the host detail page, linking to
/ops/{id} which shows the operation info, host link, duration, and
activity log filtered to that operation. Active operations can be
cancelled, which transitions the host to failed and releases the lock.
SSE activity events now include operation_id for real-time filtering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -256,6 +256,87 @@ func TestDashboardHTML(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationDetailPage(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
body := `{"hostname":"pve-test-op","mac":"aa:bb:cc:dd:ee:10","server_type":"test-type"}`
|
||||
resp, _ := http.Post(ts.URL+"/api/hosts", "application/json", bytes.NewBufferString(body))
|
||||
var created model.Host
|
||||
json.NewDecoder(resp.Body).Decode(&created)
|
||||
resp.Body.Close()
|
||||
|
||||
// Trigger rebuild to create an operation
|
||||
resp, _ = http.Post(ts.URL+"/api/hosts/"+itoa(created.ID)+"/rebuild", "application/json", nil)
|
||||
resp.Body.Close()
|
||||
|
||||
// Get operation detail page
|
||||
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
resp, err := client.Get(ts.URL + "/ops/1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("op detail: got %d, want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "text/html; charset=utf-8" {
|
||||
t.Fatalf("content-type = %q", ct)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestCancelOperation(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
body := `{"hostname":"pve-test-cancel","mac":"aa:bb:cc:dd:ee:11","server_type":"test-type"}`
|
||||
resp, _ := http.Post(ts.URL+"/api/hosts", "application/json", bytes.NewBufferString(body))
|
||||
var created model.Host
|
||||
json.NewDecoder(resp.Body).Decode(&created)
|
||||
resp.Body.Close()
|
||||
|
||||
resp, _ = http.Post(ts.URL+"/api/hosts/"+itoa(created.ID)+"/rebuild", "application/json", nil)
|
||||
resp.Body.Close()
|
||||
|
||||
// Cancel the operation
|
||||
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
resp, err := client.Post(ts.URL+"/ops/1/cancel", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusSeeOther {
|
||||
t.Fatalf("cancel: got %d, want %d", resp.StatusCode, http.StatusSeeOther)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Verify host is now failed
|
||||
resp, _ = http.Get(ts.URL + "/api/hosts/" + itoa(created.ID))
|
||||
var host model.Host
|
||||
json.NewDecoder(resp.Body).Decode(&host)
|
||||
resp.Body.Close()
|
||||
if host.State != model.StateFailed {
|
||||
t.Fatalf("state = %q, want %q", host.State, model.StateFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationNotFound(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
resp, err := http.Get(ts.URL + "/ops/99999")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("op not found: got %d, want %d", resp.StatusCode, http.StatusNotFound)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func itoa(i int64) string {
|
||||
return fmt.Sprintf("%d", i)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user