mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 00:40:44 +01:00
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
This commit is contained in:
1 parent
6cd14f06c9
commit
e7c1713457
9 files changed
+219
-26
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -69,6 +69,7 @@ templ Plugin(page string, initialJob string, lane string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@PluginSchedulerStatus()
|
||||
<div class="plugin-section plugin-section-overview">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 mb-3">
|
||||
@@ -98,7 +99,7 @@ templ Plugin(page string, initialJob string, lane string) {
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="card border-secondary h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-uppercase text-muted small mb-1">Next Run</div>
|
||||
<div class="text-uppercase text-muted small mb-1">Next scheduled check</div>
|
||||
<h4 class="mb-0 plugin-scheduler-next-run">-</h4>
|
||||
<div class="text-muted small plugin-scheduler-next-run-meta"></div>
|
||||
</div>
|
||||
@@ -258,7 +259,7 @@ templ Plugin(page string, initialJob string, lane string) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="plugin-admin-detection-interval">Detection Interval (min)</label>
|
||||
<label class="form-label" for="plugin-admin-detection-interval">Scheduled detection interval (min)</label>
|
||||
<input type="number" class="form-control" id="plugin-admin-detection-interval" min="0"/>
|
||||
<div class="form-text">How often to check for new work.</div>
|
||||
</div>
|
||||
@@ -301,7 +302,7 @@ templ Plugin(page string, initialJob string, lane string) {
|
||||
|
||||
<div class="card shadow-sm mt-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-hourglass-split me-2"></i>Next Run</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-hourglass-split me-2"></i>Next scheduled check</h5>
|
||||
<small class="text-muted">Scheduler</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -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();
|
||||
|
||||
@@ -31,6 +31,7 @@ templ PluginLane(page string, lane string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@PluginSchedulerStatus()
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
@@ -102,16 +103,17 @@ templ PluginLane(page string, lane string) {
|
||||
}
|
||||
|
||||
function renderSchedulerStatus(data) {
|
||||
window.renderPluginSchedulerStatus(data || {});
|
||||
if (!data || !data.scheduler) return '<div class="text-muted">No scheduler data</div>';
|
||||
const s = data.scheduler;
|
||||
let html = '<div class="row">';
|
||||
html += '<div class="col-md-3"><strong>Phase:</strong> ' + (s.current_phase || 'idle') + '</div>';
|
||||
html += '<div class="col-md-3"><strong>Idle Sleep:</strong> ' + (s.idle_sleep_seconds || 0) + 's</div>';
|
||||
html += '<div class="col-md-3"><strong>Current Job Type:</strong> ' + (s.current_job_type || '-') + '</div>';
|
||||
html += '<div class="col-md-3"><strong>Next Detection:</strong> ' + (s.next_detection_at ? new Date(s.next_detection_at).toLocaleTimeString() : '-') + '</div>';
|
||||
html += '<div class="col-md-3"><strong>Next scheduled check:</strong> ' + (s.next_detection_at ? new Date(s.next_detection_at).toLocaleString() : '-') + '</div>';
|
||||
html += '</div>';
|
||||
if (s.job_types && s.job_types.length > 0) {
|
||||
html += '<hr><table class="table table-sm table-hover mb-0"><thead><tr><th>Job Type</th><th>Enabled</th><th>Interval</th><th>In Flight</th><th>Next Detection</th></tr></thead><tbody>';
|
||||
html += '<hr><table class="table table-sm table-hover mb-0"><thead><tr><th>Job Type</th><th>Enabled</th><th>Interval</th><th>In Flight</th><th>Next scheduled check</th></tr></thead><tbody>';
|
||||
s.job_types.forEach(jt => {
|
||||
html += '<tr>';
|
||||
html += '<td>' + jt.job_type + '</td>';
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||
@@ -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\"><i class=\"bi bi-arrow-left me-1\"></i>All Workers</a> <button type=\"button\" class=\"btn btn-outline-secondary\" id=\"plugin-lane-refresh-btn\"><i class=\"bi bi-arrow-repeat me-1\"></i>Refresh</button></div></div></div></div><div class=\"row mb-3\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"bi bi-server me-2\"></i>Lane Scheduler Status</h5></div><div class=\"card-body\" id=\"plugin-lane-scheduler-status\"><div class=\"text-center text-muted py-3\"><i class=\"bi bi-arrow-repeat icon-spin me-1\"></i>Loading scheduler status...</div></div></div></div></div><div class=\"row mb-3\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"bi bi-list-check me-2\"></i>Job Types</h5></div><div class=\"card-body\" id=\"plugin-lane-job-types\"><div class=\"text-center text-muted py-3\"><i class=\"bi bi-arrow-repeat icon-spin me-1\"></i>Loading job types...</div></div></div></div></div><div class=\"row\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"bi bi-plug me-2\"></i>Connected Workers</h5></div><div class=\"card-body\" id=\"plugin-lane-workers\"><div class=\"text-center text-muted py-3\"><i class=\"bi bi-arrow-repeat icon-spin me-1\"></i>Loading workers...</div></div></div></div></div></div><script>\n (function() {\n const page = document.getElementById('plugin-lane-page');\n if (!page) return;\n const lane = page.dataset.pluginLane || '';\n if (!lane) return;\n\n function basePath(path) {\n return (window.__BASE_PATH__ || '') + path;\n }\n\n function fetchAndRender(url, containerId, renderFn) {\n fetch(basePath(url))\n .then(r => r.json())\n .then(data => {\n const el = document.getElementById(containerId);\n if (el) el.innerHTML = renderFn(data);\n })\n .catch(err => {\n const el = document.getElementById(containerId);\n if (el) el.innerHTML = '<div class=\"alert alert-danger\">Failed to load: ' + err.message + '</div>';\n });\n }\n\n function renderSchedulerStatus(data) {\n if (!data || !data.scheduler) return '<div class=\"text-muted\">No scheduler data</div>';\n const s = data.scheduler;\n let html = '<div class=\"row\">';\n html += '<div class=\"col-md-3\"><strong>Phase:</strong> ' + (s.current_phase || 'idle') + '</div>';\n html += '<div class=\"col-md-3\"><strong>Idle Sleep:</strong> ' + (s.idle_sleep_seconds || 0) + 's</div>';\n html += '<div class=\"col-md-3\"><strong>Current Job Type:</strong> ' + (s.current_job_type || '-') + '</div>';\n html += '<div class=\"col-md-3\"><strong>Next Detection:</strong> ' + (s.next_detection_at ? new Date(s.next_detection_at).toLocaleTimeString() : '-') + '</div>';\n html += '</div>';\n if (s.job_types && s.job_types.length > 0) {\n html += '<hr><table class=\"table table-sm table-hover mb-0\"><thead><tr><th>Job Type</th><th>Enabled</th><th>Interval</th><th>In Flight</th><th>Next Detection</th></tr></thead><tbody>';\n s.job_types.forEach(jt => {\n html += '<tr>';\n html += '<td>' + jt.job_type + '</td>';\n html += '<td>' + (jt.enabled ? '<span class=\"badge bg-success\">Yes</span>' : '<span class=\"badge bg-secondary\">No</span>') + '</td>';\n html += '<td>' + (jt.detection_interval_minutes || '-') + 'm</td>';\n html += '<td>' + (jt.detection_in_flight ? '<span class=\"badge bg-warning\">Yes</span>' : 'No') + '</td>';\n html += '<td>' + (jt.next_detection_at ? new Date(jt.next_detection_at).toLocaleTimeString() : '-') + '</td>';\n html += '</tr>';\n });\n html += '</tbody></table>';\n }\n return html;\n }\n\n function renderJobTypes(data) {\n if (!data || data.length === 0) return '<div class=\"text-muted\">No job types in this lane</div>';\n const filtered = data.filter(s => s.lane === lane);\n if (filtered.length === 0) return '<div class=\"text-muted\">No job types in this lane</div>';\n let html = '<table class=\"table table-sm table-hover mb-0\"><thead><tr><th>Job Type</th><th>Enabled</th><th>Concurrency</th><th>Detection Interval</th><th>Status</th></tr></thead><tbody>';\n filtered.forEach(s => {\n html += '<tr>';\n html += '<td><a href=\"' + basePath('/plugin/configuration?job=' + s.job_type) + '\">' + s.job_type + '</a></td>';\nLine truncated
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"btn btn-outline-secondary\"><i class=\"bi bi-arrow-left me-1\"></i>All Workers</a> <button type=\"button\" class=\"btn btn-outline-secondary\" id=\"plugin-lane-refresh-btn\"><i class=\"bi bi-arrow-repeat me-1\"></i>Refresh</button></div></div></div></div>")
|
||||
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, "<div class=\"row mb-3\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"bi bi-server me-2\"></i>Lane Scheduler Status</h5></div><div class=\"card-body\" id=\"plugin-lane-scheduler-status\"><div class=\"text-center text-muted py-3\"><i class=\"bi bi-arrow-repeat icon-spin me-1\"></i>Loading scheduler status...</div></div></div></div></div><div class=\"row mb-3\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"bi bi-list-check me-2\"></i>Job Types</h5></div><div class=\"card-body\" id=\"plugin-lane-job-types\"><div class=\"text-center text-muted py-3\"><i class=\"bi bi-arrow-repeat icon-spin me-1\"></i>Loading job types...</div></div></div></div></div><div class=\"row\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"card-title mb-0\"><i class=\"bi bi-plug me-2\"></i>Connected Workers</h5></div><div class=\"card-body\" id=\"plugin-lane-workers\"><div class=\"text-center text-muted py-3\"><i class=\"bi bi-arrow-repeat icon-spin me-1\"></i>Loading workers...</div></div></div></div></div></div><script>\n (function() {\n const page = document.getElementById('plugin-lane-page');\n if (!page) return;\n const lane = page.dataset.pluginLane || '';\n if (!lane) return;\n\n function basePath(path) {\n return (window.__BASE_PATH__ || '') + path;\n }\n\n function fetchAndRender(url, containerId, renderFn) {\n fetch(basePath(url))\n .then(r => r.json())\n .then(data => {\n const el = document.getElementById(containerId);\n if (el) el.innerHTML = renderFn(data);\n })\n .catch(err => {\n const el = document.getElementById(containerId);\n if (el) el.innerHTML = '<div class=\"alert alert-danger\">Failed to load: ' + err.message + '</div>';\n });\n }\n\n function renderSchedulerStatus(data) {\n window.renderPluginSchedulerStatus(data || {});\n if (!data || !data.scheduler) return '<div class=\"text-muted\">No scheduler data</div>';\n const s = data.scheduler;\n let html = '<div class=\"row\">';\n html += '<div class=\"col-md-3\"><strong>Phase:</strong> ' + (s.current_phase || 'idle') + '</div>';\n html += '<div class=\"col-md-3\"><strong>Idle Sleep:</strong> ' + (s.idle_sleep_seconds || 0) + 's</div>';\n html += '<div class=\"col-md-3\"><strong>Current Job Type:</strong> ' + (s.current_job_type || '-') + '</div>';\n html += '<div class=\"col-md-3\"><strong>Next scheduled check:</strong> ' + (s.next_detection_at ? new Date(s.next_detection_at).toLocaleString() : '-') + '</div>';\n html += '</div>';\n if (s.job_types && s.job_types.length > 0) {\n html += '<hr><table class=\"table table-sm table-hover mb-0\"><thead><tr><th>Job Type</th><th>Enabled</th><th>Interval</th><th>In Flight</th><th>Next scheduled check</th></tr></thead><tbody>';\n s.job_types.forEach(jt => {\n html += '<tr>';\n html += '<td>' + jt.job_type + '</td>';\n html += '<td>' + (jt.enabled ? '<span class=\"badge bg-success\">Yes</span>' : '<span class=\"badge bg-secondary\">No</span>') + '</td>';\n html += '<td>' + (jt.detection_interval_minutes || '-') + 'm</td>';\n html += '<td>' + (jt.detection_in_flight ? '<span class=\"badge bg-warning\">Yes</span>' : 'No') + '</td>';\n html += '<td>' + (jt.next_detection_at ? new Date(jt.next_detection_at).toLocaleTimeString() : '-') + '</td>';\n html += '</tr>';\n });\n html += '</tbody></table>';\n }\n return html;\n }\n\n function renderJobTypes(data) {\n if (!data || data.length === 0) return '<div class=\"text-muted\">No job types in this lane</div>';\n const filtered = data.filter(s => s.lane === lane);\n if (filtered.length === 0) return '<div class=\"text-muted\">No job types in this lane</div>';\n let html = '<table class=\"table table-sm table-hover mb-0\"><thead><tr><th>Job Type</th><th>Enabled</th><th>Concurrency</th><th>Detection Interval</th><th>Status</th></tr></thead><tbody>';\n filtered.forEach(s => {\n html += '<tr>';\n html += '<td><a href=\"' + basePath('/plugin/configuration?job=' + s.job_type) + '\">' + s.job_type + '</a></td>';\n html += '<td>' + (s.enabled ? '<span class=\"badge bg-success\">Yes</span>' : '<span class=\"badge bg-secondary\">No</span>') + '</td>';\n html += '<td>' + (s.global_Line truncated
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package app
|
||||
|
||||
templ PluginSchedulerStatus() {
|
||||
<div id="plugin-scheduler-blockers" aria-live="polite"></div>
|
||||
<script>
|
||||
window.renderPluginSchedulerStatus = function(data) {
|
||||
const root = document.getElementById('plugin-scheduler-blockers');
|
||||
if (!root) return;
|
||||
root.replaceChildren();
|
||||
const lock = data.lock || {};
|
||||
const waiters = lock.waiters || [];
|
||||
if (!waiters.length) return;
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-warning';
|
||||
const title = document.createElement('strong');
|
||||
title.textContent = 'Waiting for the maintenance lock';
|
||||
alert.appendChild(title);
|
||||
const owner = document.createElement('div');
|
||||
owner.textContent = lock.holder ? 'Lock owner: ' + lock.holder : (lock.holder_error || 'Lock owner not reported');
|
||||
if (lock.holder_message) owner.textContent += ' — ' + lock.holder_message;
|
||||
alert.appendChild(owner);
|
||||
waiters.forEach(function(waiter) {
|
||||
const line = document.createElement('div');
|
||||
const since = new Date(waiter.since);
|
||||
const now = new Date((data.scheduler || {}).now || Date.now());
|
||||
const seconds = Math.max(0, Math.floor((now - since) / 1000));
|
||||
line.textContent = waiter.reason + ' · since ' + since.toLocaleString() +
|
||||
' (local time) · waiting ' + Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';
|
||||
alert.appendChild(line);
|
||||
});
|
||||
const note = document.createElement('div');
|
||||
note.className = 'small mt-1';
|
||||
note.textContent = 'This lane cannot advance until the lock becomes available. A scheduled check time is not a guaranteed start time.';
|
||||
alert.appendChild(note);
|
||||
root.appendChild(alert);
|
||||
};
|
||||
</script>
|
||||
}
|
||||
@@ -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, "<div id=\"plugin-scheduler-blockers\" aria-live=\"polite\"></div><script>\n window.renderPluginSchedulerStatus = function(data) {\n const root = document.getElementById('plugin-scheduler-blockers');\n if (!root) return;\n root.replaceChildren();\n const lock = data.lock || {};\n const waiters = lock.waiters || [];\n if (!waiters.length) return;\n const alert = document.createElement('div');\n alert.className = 'alert alert-warning';\n const title = document.createElement('strong');\n title.textContent = 'Waiting for the maintenance lock';\n alert.appendChild(title);\n const owner = document.createElement('div');\n owner.textContent = lock.holder ? 'Lock owner: ' + lock.holder : (lock.holder_error || 'Lock owner not reported');\n if (lock.holder_message) owner.textContent += ' — ' + lock.holder_message;\n alert.appendChild(owner);\n waiters.forEach(function(waiter) {\n const line = document.createElement('div');\n const since = new Date(waiter.since);\n const now = new Date((data.scheduler || {}).now || Date.now());\n const seconds = Math.max(0, Math.floor((now - since) / 1000));\n line.textContent = waiter.reason + ' · since ' + since.toLocaleString() +\n ' (local time) · waiting ' + Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';\n alert.appendChild(line);\n });\n const note = document.createElement('div');\n note.className = 'small mt-1';\n note.textContent = 'This lane cannot advance until the lock becomes available. A scheduled check time is not a guaranteed start time.';\n alert.appendChild(note);\n root.appendChild(alert);\n };\n </script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -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, "</p></div><div class=\"btn-group\"><button type=\"button\" class=\"btn btn-outline-secondary\" id=\"plugin-refresh-all-btn\"><i class=\"bi bi-arrow-repeat me-1\"></i>Refresh</button></div></div></div></div><div class=\"row mb-3\"><div class=\"col-12\"><ul class=\"nav nav-tabs\" id=\"plugin-top-tabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-top-tab=\"overview\"><i class=\"bi bi-graph-up-arrow me-1\"></i>Overview</button></li></ul></div></div><div class=\"row mb-3 d-none\" id=\"plugin-subtab-row\"><div class=\"col-12\"><ul class=\"nav nav-pills gap-2\" id=\"plugin-subtabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-subtab=\"configuration\"><i class=\"bi bi-sliders me-1\"></i>Configuration</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"detection\"><i class=\"bi bi-search me-1\"></i>Job Detection</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"queue\"><i class=\"bi bi-list me-1\"></i>Job Queue</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"execution\"><i class=\"bi bi-list-check me-1\"></i>Job Execution</button></li></ul></div></div><div class=\"plugin-section plugin-section-overview\"><div class=\"row mb-4\"><div class=\"col-md-3 mb-3\"><div class=\"card border-info h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Workers</div><h4 class=\"mb-0\" id=\"plugin-status-workers\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-warning h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Active Jobs</div><h4 class=\"mb-0\" id=\"plugin-status-active-jobs\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-success h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Activities (recent)</div><h4 class=\"mb-0\" id=\"plugin-status-activities\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-secondary h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Next Run</div><h4 class=\"mb-0 plugin-scheduler-next-run\">-</h4><div class=\"text-muted small plugin-scheduler-next-run-meta\"></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"bi bi-bar-chart me-2\"></i>Per Job Type Summary</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Active Jobs</th><th>Recent Activities</th></tr></thead> <tbody id=\"plugin-jobtype-summary-body\"><tr><td colspan=\"3\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header d-flex justify-content-between align-items-center flex-wrap gap-2\"><h5 class=\"mb-0\"><i class=\"bi bi-clock me-2\"></i>Scheduler State</h5><small class=\"text-muted\">Sequential scheduler with per-job runtime limits</small></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Enabled</th><th>Detector</th><th>In Flight</th><th>Max Runtime</th><th>Exec Global</th><th>Exec/Worker</th><th>Executor Workers</th><th>Effective Exec</th><th>Last Run</th></tr></thead> <tbody id=\"plugin-scheduler-table-body\"><tr><td colspan=\"10\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"bi bi-people me-2\"></i>Workers</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\" style=\"max-height: 560px; overflow-y: auto;\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light sticky-top\"><tr><th>Worker</th><th>Address</th><th>Capabilities</th><th>Load</th></tr></thead> <tbody id=\"plugin-workers-table-body\"><tr><td colspan=\"4\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div></div><div class=\"plugin-section plugin-section-configuration\"><div class=\"row mb-4\"><div class=\"col-lg-8 mb-3\"><div class=\"card shadow-sm h-100\"><div class=\"card-header d-flex justify-content-between align-items-center\"><h5 class=\"mb-0\"><i class=\"bi bi-sliders me-2\"></i>Job Type Configuration</h5><small class=\"text-muted\" id=\"plugin-config-updated-at\">Not loaded</small></div><div class=\"card-body\"><div class=Line truncated
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p></div><div class=\"btn-group\"><button type=\"button\" class=\"btn btn-outline-secondary\" id=\"plugin-refresh-all-btn\"><i class=\"bi bi-arrow-repeat me-1\"></i>Refresh</button></div></div></div></div><div class=\"row mb-3\"><div class=\"col-12\"><ul class=\"nav nav-tabs\" id=\"plugin-top-tabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-top-tab=\"overview\"><i class=\"bi bi-graph-up-arrow me-1\"></i>Overview</button></li></ul></div></div><div class=\"row mb-3 d-none\" id=\"plugin-subtab-row\"><div class=\"col-12\"><ul class=\"nav nav-pills gap-2\" id=\"plugin-subtabs\"><li class=\"nav-item\"><button type=\"button\" class=\"nav-link active\" data-plugin-subtab=\"configuration\"><i class=\"bi bi-sliders me-1\"></i>Configuration</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"detection\"><i class=\"bi bi-search me-1\"></i>Job Detection</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"queue\"><i class=\"bi bi-list me-1\"></i>Job Queue</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"execution\"><i class=\"bi bi-list-check me-1\"></i>Job Execution</button></li></ul></div></div>")
|
||||
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, "<div class=\"plugin-section plugin-section-overview\"><div class=\"row mb-4\"><div class=\"col-md-3 mb-3\"><div class=\"card border-info h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Workers</div><h4 class=\"mb-0\" id=\"plugin-status-workers\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-warning h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Active Jobs</div><h4 class=\"mb-0\" id=\"plugin-status-active-jobs\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-success h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Activities (recent)</div><h4 class=\"mb-0\" id=\"plugin-status-activities\">0</h4></div></div></div><div class=\"col-md-3 mb-3\"><div class=\"card border-secondary h-100\"><div class=\"card-body text-center\"><div class=\"text-uppercase text-muted small mb-1\">Next scheduled check</div><h4 class=\"mb-0 plugin-scheduler-next-run\">-</h4><div class=\"text-muted small plugin-scheduler-next-run-meta\"></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"bi bi-bar-chart me-2\"></i>Per Job Type Summary</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Active Jobs</th><th>Recent Activities</th></tr></thead> <tbody id=\"plugin-jobtype-summary-body\"><tr><td colspan=\"3\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header d-flex justify-content-between align-items-center flex-wrap gap-2\"><h5 class=\"mb-0\"><i class=\"bi bi-clock me-2\"></i>Scheduler State</h5><small class=\"text-muted\">Sequential scheduler with per-job runtime limits</small></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Enabled</th><th>Detector</th><th>In Flight</th><th>Max Runtime</th><th>Exec Global</th><th>Exec/Worker</th><th>Executor Workers</th><th>Effective Exec</th><th>Last Run</th></tr></thead> <tbody id=\"plugin-scheduler-table-body\"><tr><td colspan=\"10\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"bi bi-people me-2\"></i>Workers</h5></div><div class=\"card-body p-0\"><div class=\"table-responsive\" style=\"max-height: 560px; overflow-y: auto;\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light sticky-top\"><tr><th>Worker</th><th>Address</th><th>Capabilities</th><th>Load</th></tr></thead> <tbody id=\"plugin-workers-table-body\"><tr><td colspan=\"4\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div></div><div class=\"plugin-section plugin-section-configuration\"><div class=\"row mb-4\"><div class=\"col-lg-8 mb-3\"><div class=\"card shadow-sm h-100\"><div class=\"card-header d-flex justify-content-between align-items-center\"><h5 class=\"mb-0\"><i class=\"bi bi-sliders me-2\"></i>Job Type Configuration</h5><small class=\"text-muted\" id=\"plugin-config-updated-at\">Not loaded</small></div><div class=\"card-body\"><div class=\"mb-3\"><div class=\"small text-muted\">Selected Job Type</div><div class=\"fw-semibold\" id=\"plugin-selected-job-type\">-</div></div><div class=\"mb-3\"><div class=\"d-flex justify-content-between align-items-center mb-2\"><h6 class=\"mb-0\">Descriptor</h6><button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" id=\"plugin-refresh-schema-btn\"><i class=\"bi bi-cloud-download me-1\"></i>Refresh Schema</button></div><div class=\"alert alert-light border\" id=\"plugin-descriptor-summary\">Select a job type to load schema and config.</div></div><div class=\"mb-3\"><h6>Admin Config Form</h6><div id=\"plugin-admin-config-form\" class=\"plugin-form-root\"><div class=\"text-muted\">No admin form loaded.</div></div></div><div class=\"mb-3\"><h6>Worker Config Form</h6><div id=\"plugin-worker-config-form\" class=\"plugin-form-root\"><div class=\"text-muted\">No worker form loaded.</div></div></div><div class=\"d-flex flex-wrap gap-2\"><button type=\"button\" class=\"btn btn-primary\" id=\"plugin-save-config-btn\"><i class=\"bi bi-floppy me-1\"></i>Save Config</button> <button type=\"button\" class=\"btn btn-outline-primary\" id=\"plugin-trigger-detection-btn\"><i class=\"bi bi-search me-1\"></i>Run Detection</button> <button type=\"button\" class=\"btn btnLine truncated
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user