From e7c1713457c5a0216d76a49757909b84153bd0c5 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 28 Oct 2026 21:28:15 -0700 Subject: [PATCH] admin: show maintenance lock blockers in scheduler UI (#12184) * admin: expose maintenance lock waiters and current owner * admin: display maintenance lock waits across scheduler pages * admin: preserve the active lock message while acquisitions wait * admin: synchronize lock fairness assertions with waiter registration * admin: display lock wait start times alongside elapsed duration --- weed/admin/dash/admin_lock_manager.go | 33 ++++++++++-- weed/admin/dash/admin_lock_manager_test.go | 53 +++++++++++++++++-- weed/admin/dash/plugin_api.go | 24 ++++++++- weed/admin/view/app/plugin.templ | 12 +++-- weed/admin/view/app/plugin_lane.templ | 8 ++- weed/admin/view/app/plugin_lane_templ.go | 20 ++++--- .../view/app/plugin_scheduler_status.templ | 38 +++++++++++++ .../view/app/plugin_scheduler_status_templ.go | 37 +++++++++++++ weed/admin/view/app/plugin_templ.go | 20 ++++--- 9 files changed, 219 insertions(+), 26 deletions(-) create mode 100644 weed/admin/view/app/plugin_scheduler_status.templ create mode 100644 weed/admin/view/app/plugin_scheduler_status_templ.go diff --git a/weed/admin/dash/admin_lock_manager.go b/weed/admin/dash/admin_lock_manager.go index 5be4c421fe..419da8abbf 100644 --- a/weed/admin/dash/admin_lock_manager.go +++ b/weed/admin/dash/admin_lock_manager.go @@ -1,6 +1,7 @@ package dash import ( + "sort" "sync" "time" @@ -52,6 +53,7 @@ type AdminLockManager struct { waitingSince time.Time waitingReason string currentReason string + waiters map[*LockWait]struct{} } func NewAdminLockManager(masterClient *wdclient.MasterClient, clientName string) *AdminLockManager { @@ -81,13 +83,23 @@ func (m *AdminLockManager) Acquire(reason string) (func(), error) { } m.mu.Lock() + waiter := &LockWait{Reason: reason, Since: time.Now().UTC()} + if m.waiters == nil { + m.waiters = make(map[*LockWait]struct{}) + } + m.waiters[waiter] = struct{}{} + defer func() { + m.mu.Lock() + delete(m.waiters, waiter) + m.mu.Unlock() + }() + for m.acquiring || m.releasing || (m.holdCount > 0 && time.Since(m.heldSince) > m.fairnessWindow) { + m.cond.Wait() + } if reason != "" { m.locker.SetMessage(reason) m.currentReason = reason } - for m.acquiring || m.releasing || (m.holdCount > 0 && time.Since(m.heldSince) > m.fairnessWindow) { - m.cond.Wait() - } if m.holdCount == 0 { m.acquiring = true m.waitingSince = time.Now().UTC() @@ -153,6 +165,11 @@ func (m *AdminLockManager) Release() { } } +type LockWait struct { + Reason string `json:"reason"` + Since time.Time `json:"since"` +} + type LockStatus struct { Held bool `json:"held"` HoldCount int `json:"hold_count"` @@ -162,6 +179,10 @@ type LockStatus struct { LastAcquiredAt *time.Time `json:"last_acquired_at,omitempty"` LastReleasedAt *time.Time `json:"last_released_at,omitempty"` WaitingSince *time.Time `json:"waiting_since,omitempty"` + Waiters []LockWait `json:"waiters,omitempty"` + Holder string `json:"holder,omitempty"` + HolderMessage string `json:"holder_message,omitempty"` + HolderError string `json:"holder_error,omitempty"` } func (m *AdminLockManager) Status() LockStatus { @@ -191,5 +212,11 @@ func (m *AdminLockManager) Status() LockStatus { at := m.waitingSince status.WaitingSince = &at } + for waiter := range m.waiters { + status.Waiters = append(status.Waiters, *waiter) + } + sort.Slice(status.Waiters, func(i, j int) bool { + return status.Waiters[i].Since.Before(status.Waiters[j].Since) + }) return status } diff --git a/weed/admin/dash/admin_lock_manager_test.go b/weed/admin/dash/admin_lock_manager_test.go index 4f3b262895..de08c78602 100644 --- a/weed/admin/dash/admin_lock_manager_test.go +++ b/weed/admin/dash/admin_lock_manager_test.go @@ -4,10 +4,14 @@ import ( "sync" "testing" "time" + + "github.com/stretchr/testify/require" ) type fakeAdminLocker struct { - releaseDelay time.Duration + releaseDelay time.Duration + requestStarted chan struct{} + requestGate <-chan struct{} mu sync.Mutex requests []time.Time @@ -15,11 +19,44 @@ type fakeAdminLocker struct { } func (f *fakeAdminLocker) RequestLock(clientName string) { + if f.requestStarted != nil { + close(f.requestStarted) + } + if f.requestGate != nil { + <-f.requestGate + } f.mu.Lock() f.requests = append(f.requests, time.Now()) f.mu.Unlock() } +func TestAdminLockManagerStatusWhileWaiting(t *testing.T) { + for _, reason := range []string{"plugin scheduler vacuum detection", "plugin scheduler vacuum job vacuum-1"} { + t.Run(reason, func(t *testing.T) { + gate := make(chan struct{}) + locker := &fakeAdminLocker{requestStarted: make(chan struct{}), requestGate: gate} + m := newTestLockManager(locker, 0, time.Minute) + done := make(chan func(), 1) + go func() { + release, _ := m.Acquire(reason) + done <- release + }() + <-locker.requestStarted + status := m.Status() + close(gate) + release := <-done + defer release() + if len(status.Waiters) != 1 || status.Waiters[0].Reason != reason || status.Waiters[0].Since.IsZero() { + t.Fatalf("missing lock wait: %+v", status) + } + status = m.Status() + if len(status.Waiters) != 0 || !status.Held { + t.Fatalf("an acquired lock must not be reported as blocked: %+v", status) + } + }) + } +} + func (f *fakeAdminLocker) ReleaseLock() { if f.releaseDelay > 0 { time.Sleep(f.releaseDelay) @@ -97,6 +134,8 @@ func TestAdminLockManagerFairnessWindowForcesFullRelease(t *testing.T) { m := newTestLockManager(locker, 20*time.Millisecond, 50*time.Millisecond) release1, _ := m.Acquire("long-hold") + releaseFirst := sync.OnceFunc(release1) + defer releaseFirst() m.mu.Lock() m.heldSince = time.Now().Add(-time.Second) m.mu.Unlock() @@ -107,13 +146,21 @@ func TestAdminLockManagerFairnessWindowForcesFullRelease(t *testing.T) { acquired <- release2 }() + require.Eventually(t, func() bool { return len(m.Status().Waiters) == 1 }, time.Second, time.Millisecond) select { case <-acquired: t.Fatalf("late acquire joined a hold older than the fairness window") - case <-time.After(50 * time.Millisecond): + default: } - release1() + status := m.Status() + if status.Message != "long-hold" { + t.Errorf("waiter replaced the current holder's message: %q", status.Message) + } + if len(status.Waiters) != 1 || status.Waiters[0].Reason != "late-joiner" { + t.Errorf("missing fairness waiter: %+v", status.Waiters) + } + releaseFirst() select { case release2 := <-acquired: diff --git a/weed/admin/dash/plugin_api.go b/weed/admin/dash/plugin_api.go index c38b0aa26a..934f8db7c4 100644 --- a/weed/admin/dash/plugin_api.go +++ b/weed/admin/dash/plugin_api.go @@ -304,6 +304,26 @@ func (s *AdminServer) GetPluginSchedulerStatusAPI(w http.ResponseWriter, r *http laneFilter := strings.TrimSpace(r.URL.Query().Get("lane")) + var lockStatus LockStatus + if s.pluginLock != nil && (laneFilter == "" || plugin.LaneRequiresLock(plugin.SchedulerLane(laneFilter))) { + lockStatus = s.pluginLock.Status() + if len(lockStatus.Waiters) > 0 && s.masterClient != nil { + ctx, cancel := context.WithTimeout(r.Context(), time.Second) + err := s.masterClient.WithClient(ctx, false, func(client master_pb.SeaweedClient) error { + holder, err := client.GetAdminLockStatus(ctx, &master_pb.GetAdminLockStatusRequest{LockName: cluster.AdminShellLockName}) + if err == nil && holder.GetIsLocked() { + lockStatus.Holder = holder.GetClientName() + lockStatus.HolderMessage = holder.GetMessage() + } + return err + }) + cancel() + if err != nil { + lockStatus.HolderError = "Lock owner unavailable" + } + } + } + if laneFilter != "" { lane := plugin.SchedulerLane(laneFilter) response := map[string]interface{}{ @@ -311,7 +331,7 @@ func (s *AdminServer) GetPluginSchedulerStatusAPI(w http.ResponseWriter, r *http "scheduler": pluginSvc.GetLaneSchedulerStatus(lane), } if s.pluginLock != nil { - response["lock"] = s.pluginLock.Status() + response["lock"] = lockStatus } writeJSON(w, http.StatusOK, response) return @@ -322,7 +342,7 @@ func (s *AdminServer) GetPluginSchedulerStatusAPI(w http.ResponseWriter, r *http "scheduler": pluginSvc.GetSchedulerStatus(), } if s.pluginLock != nil { - response["lock"] = s.pluginLock.Status() + response["lock"] = lockStatus } writeJSON(w, http.StatusOK, response) diff --git a/weed/admin/view/app/plugin.templ b/weed/admin/view/app/plugin.templ index 5aab757a9c..4cf3f1911f 100644 --- a/weed/admin/view/app/plugin.templ +++ b/weed/admin/view/app/plugin.templ @@ -69,6 +69,7 @@ templ Plugin(page string, initialJob string, lane string) { + @PluginSchedulerStatus()
@@ -98,7 +99,7 @@ templ Plugin(page string, initialJob string, lane string) {
-
Next Run
+
Next scheduled check

-

@@ -258,7 +259,7 @@ templ Plugin(page string, initialJob string, lane string) {
- +
How often to check for new work.
@@ -301,7 +302,7 @@ templ Plugin(page string, initialJob string, lane string) {
-
Next Run
+
Next scheduled check
Scheduler
@@ -616,6 +617,7 @@ templ Plugin(page string, initialJob string, lane string) { activities: [], schedulerStates: [], schedulerStatus: null, + schedulerLock: null, allJobs: [], allActivities: [], loadedJobType: '', @@ -1606,6 +1608,7 @@ templ Plugin(page string, initialJob string, lane string) { } function renderSchedulerStatus() { + window.renderPluginSchedulerStatus({scheduler: state.schedulerStatus, lock: state.schedulerLock}); var valueNodes = document.querySelectorAll('.plugin-scheduler-next-run'); if (!valueNodes.length) { return; @@ -1617,7 +1620,7 @@ templ Plugin(page string, initialJob string, lane string) { valueNodes.forEach(function(node) { node.textContent = display; }); - var metaText = nextRun ? 'Local time' : 'Not scheduled'; + var metaText = nextRun ? 'Next scheduled check (local time); locks and other jobs may delay detection.' : 'Not scheduled'; if (metaNodes.length) { metaNodes.forEach(function(node) { node.textContent = metaText; @@ -3076,6 +3079,7 @@ templ Plugin(page string, initialJob string, lane string) { state.allActivities = state.activities; state.schedulerStates = Array.isArray(schedulerStates) ? schedulerStates : []; state.schedulerStatus = schedulerStatus && schedulerStatus.scheduler ? schedulerStatus.scheduler : null; + state.schedulerLock = schedulerStatus && schedulerStatus.lock ? schedulerStatus.lock : null; renderQueueJobs(); renderDetectionJobs(); renderExecutionJobs(); diff --git a/weed/admin/view/app/plugin_lane.templ b/weed/admin/view/app/plugin_lane.templ index 18583e5cfc..097b92ccf7 100644 --- a/weed/admin/view/app/plugin_lane.templ +++ b/weed/admin/view/app/plugin_lane.templ @@ -31,6 +31,7 @@ templ PluginLane(page string, lane string) {
+ @PluginSchedulerStatus()
@@ -102,16 +103,17 @@ templ PluginLane(page string, lane string) { } function renderSchedulerStatus(data) { + window.renderPluginSchedulerStatus(data || {}); if (!data || !data.scheduler) return '
No scheduler data
'; const s = data.scheduler; let html = '
'; html += '
Phase: ' + (s.current_phase || 'idle') + '
'; html += '
Idle Sleep: ' + (s.idle_sleep_seconds || 0) + 's
'; html += '
Current Job Type: ' + (s.current_job_type || '-') + '
'; - html += '
Next Detection: ' + (s.next_detection_at ? new Date(s.next_detection_at).toLocaleTimeString() : '-') + '
'; + html += '
Next scheduled check: ' + (s.next_detection_at ? new Date(s.next_detection_at).toLocaleString() : '-') + '
'; html += '
'; if (s.job_types && s.job_types.length > 0) { - html += '
'; + html += '
Job TypeEnabledIntervalIn FlightNext Detection
'; s.job_types.forEach(jt => { html += ''; html += ''; @@ -168,6 +170,8 @@ templ PluginLane(page string, lane string) { } refresh(); + const timer = setInterval(refresh, 10000); + window.addEventListener('beforeunload', function() { clearInterval(timer); }); const btn = document.getElementById('plugin-lane-refresh-btn'); if (btn) btn.addEventListener('click', refresh); })(); diff --git a/weed/admin/view/app/plugin_lane_templ.go b/weed/admin/view/app/plugin_lane_templ.go index 63394d1c1a..88a664dbe2 100644 --- a/weed/admin/view/app/plugin_lane_templ.go +++ b/weed/admin/view/app/plugin_lane_templ.go @@ -41,7 +41,7 @@ func PluginLane(page string, lane string) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.NewAttributeExpression(currentPage).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin_lane.templ`, Line: 14, Col: 85} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_lane.templ`, Line: 14, Col: 85} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) if templ_7745c5c3_Err != nil { @@ -54,7 +54,7 @@ func PluginLane(page string, lane string) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.NewAttributeExpression(lane).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin_lane.templ`, Line: 14, Col: 111} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_lane.templ`, Line: 14, Col: 111} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) if templ_7745c5c3_Err != nil { @@ -67,7 +67,7 @@ func PluginLane(page string, lane string) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lane) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin_lane.templ`, Line: 19, Col: 80} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_lane.templ`, Line: 19, Col: 80} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -80,7 +80,7 @@ func PluginLane(page string, lane string) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lane) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin_lane.templ`, Line: 20, Col: 94} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_lane.templ`, Line: 20, Col: 94} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -93,13 +93,21 @@ func PluginLane(page string, lane string) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.NewAttributeExpression(dash.PUrl(ctx, "/plugin")).ResolveURL(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin_lane.templ`, Line: 23, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_lane.templ`, Line: 23, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"btn btn-outline-secondary\">All Workers
Lane Scheduler Status
Loading scheduler status...
Job Types
Loading job types...
Connected Workers
Loading workers...
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"btn btn-outline-secondary\">All Workers") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = PluginSchedulerStatus().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
Lane Scheduler Status
Loading scheduler status...
Job Types
Loading job types...
Connected Workers
Loading workers...
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/plugin_scheduler_status.templ b/weed/admin/view/app/plugin_scheduler_status.templ new file mode 100644 index 0000000000..715498eec1 --- /dev/null +++ b/weed/admin/view/app/plugin_scheduler_status.templ @@ -0,0 +1,38 @@ +package app + +templ PluginSchedulerStatus() { +
+ +} diff --git a/weed/admin/view/app/plugin_scheduler_status_templ.go b/weed/admin/view/app/plugin_scheduler_status_templ.go new file mode 100644 index 0000000000..5b85466333 --- /dev/null +++ b/weed/admin/view/app/plugin_scheduler_status_templ.go @@ -0,0 +1,37 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1100 + +package app + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func PluginSchedulerStatus() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/app/plugin_templ.go b/weed/admin/view/app/plugin_templ.go index e6ac42967d..5babafdff5 100644 --- a/weed/admin/view/app/plugin_templ.go +++ b/weed/admin/view/app/plugin_templ.go @@ -43,7 +43,7 @@ func Plugin(page string, initialJob string, lane string) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.NewAttributeExpression(currentPage).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin.templ`, Line: 16, Col: 80} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 16, Col: 80} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) if templ_7745c5c3_Err != nil { @@ -56,7 +56,7 @@ func Plugin(page string, initialJob string, lane string) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.NewAttributeExpression(initialJob).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin.templ`, Line: 16, Col: 111} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 16, Col: 111} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) if templ_7745c5c3_Err != nil { @@ -69,7 +69,7 @@ func Plugin(page string, initialJob string, lane string) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.NewAttributeExpression(currentLane).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin.templ`, Line: 16, Col: 144} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 16, Col: 144} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) if templ_7745c5c3_Err != nil { @@ -82,7 +82,7 @@ func Plugin(page string, initialJob string, lane string) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(laneTitle) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin.templ`, Line: 21, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 21, Col: 83} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -95,13 +95,21 @@ func Plugin(page string, initialJob string, lane string) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(laneDescription) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin.templ`, Line: 22, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 22, Col: 68} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Workers

0

Active Jobs

0

Activities (recent)

0

Next Run

-

Per Job Type Summary
Job TypeEnabledIntervalIn FlightNext scheduled check
' + jt.job_type + '
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Sequential scheduler with per-job runtime limits
Job TypeEnabledDetectorIn FlightMax RuntimeExec GlobalExec/WorkerExecutor WorkersEffective ExecLast Run
Loading...
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
How often to check for new work.
Per-attempt deadline for one job. Size-aware tasks may extend this automatically.
Next Run
Scheduler
-
Not scheduled
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = PluginSchedulerStatus().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
Workers

0

Active Jobs

0

Activities (recent)

0

Next scheduled check

-

Per Job Type Summary
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Sequential scheduler with per-job runtime limits
Job TypeEnabledDetectorIn FlightMax RuntimeExec GlobalExec/WorkerExecutor WorkersEffective ExecLast Run
Loading...
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
How often to check for new work.
Per-attempt deadline for one job. Size-aware tasks may extend this automatically.
Next scheduled check
Scheduler
-
Not scheduled
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }