Browse Source
feat: add per-lane scheduler status API and lane worker UI pages
feat: add per-lane scheduler status API and lane worker UI pages
- GET /api/plugin/lanes returns all lanes with status and job types
- GET /api/plugin/workers?lane=X filters workers by lane
- GET /api/plugin/scheduler-states?lane=X filters job types by lane
- GET /api/plugin/scheduler-status?lane=X returns lane-scoped status
- GET /plugin/lanes/{lane}/workers renders per-lane worker page
- SchedulerJobTypeState now includes a "lane" field
The lane worker pages show scheduler status, job type configuration,
and connected workers scoped to a single lane, with links back to
the main plugin overview.
pull/8436/merge
2 changed files with 263 additions and 0 deletions
@ -0,0 +1,167 @@ |
|||||
|
package app |
||||
|
|
||||
|
templ PluginLane(page string, lane string) { |
||||
|
{{ |
||||
|
currentPage := page |
||||
|
if currentPage == "" { |
||||
|
currentPage = "lane_workers" |
||||
|
} |
||||
|
}} |
||||
|
<div class="container-fluid" id="plugin-lane-page" data-plugin-page={ currentPage } data-plugin-lane={ lane }> |
||||
|
<div class="row mb-4"> |
||||
|
<div class="col-12"> |
||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2"> |
||||
|
<div> |
||||
|
<h2 class="mb-0"><i class="fas fa-layer-group me-2"></i>{ lane } Lane Workers</h2> |
||||
|
<p class="text-muted mb-0">Workers and scheduler status for the { lane } scheduling lane.</p> |
||||
|
</div> |
||||
|
<div class="btn-group"> |
||||
|
<a href="/plugin" class="btn btn-outline-secondary"> |
||||
|
<i class="fas fa-arrow-left me-1"></i>All Workers |
||||
|
</a> |
||||
|
<button type="button" class="btn btn-outline-secondary" id="plugin-lane-refresh-btn"> |
||||
|
<i class="fas fa-sync-alt 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="fas fa-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="fas fa-spinner fa-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="fas fa-tasks 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="fas fa-spinner fa-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="fas fa-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="fas fa-spinner fa-spin me-1"></i>Loading workers... |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<script> |
||||
|
(function() { |
||||
|
const page = document.getElementById('plugin-lane-page'); |
||||
|
if (!page) return; |
||||
|
const lane = page.dataset.pluginLane || ''; |
||||
|
if (!lane) return; |
||||
|
|
||||
|
function fetchAndRender(url, containerId, renderFn) { |
||||
|
fetch(url) |
||||
|
.then(r => r.json()) |
||||
|
.then(data => { |
||||
|
const el = document.getElementById(containerId); |
||||
|
if (el) el.innerHTML = renderFn(data); |
||||
|
}) |
||||
|
.catch(err => { |
||||
|
const el = document.getElementById(containerId); |
||||
|
if (el) el.innerHTML = '<div class="alert alert-danger">Failed to load: ' + err.message + '</div>'; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function renderSchedulerStatus(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>'; |
||||
|
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>'; |
||||
|
s.job_types.forEach(jt => { |
||||
|
html += '<tr>'; |
||||
|
html += '<td>' + jt.job_type + '</td>'; |
||||
|
html += '<td>' + (jt.enabled ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>') + '</td>'; |
||||
|
html += '<td>' + (jt.detection_interval_seconds || '-') + 's</td>'; |
||||
|
html += '<td>' + (jt.detection_in_flight ? '<span class="badge bg-warning">Yes</span>' : 'No') + '</td>'; |
||||
|
html += '<td>' + (jt.next_detection_at ? new Date(jt.next_detection_at).toLocaleTimeString() : '-') + '</td>'; |
||||
|
html += '</tr>'; |
||||
|
}); |
||||
|
html += '</tbody></table>'; |
||||
|
} |
||||
|
return html; |
||||
|
} |
||||
|
|
||||
|
function renderJobTypes(data) { |
||||
|
if (!data || data.length === 0) return '<div class="text-muted">No job types in this lane</div>'; |
||||
|
const filtered = data.filter(s => s.lane === lane); |
||||
|
if (filtered.length === 0) return '<div class="text-muted">No job types in this lane</div>'; |
||||
|
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>'; |
||||
|
filtered.forEach(s => { |
||||
|
html += '<tr>'; |
||||
|
html += '<td><a href="/plugin/configuration?job=' + s.job_type + '">' + s.job_type + '</a></td>'; |
||||
|
html += '<td>' + (s.enabled ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>') + '</td>'; |
||||
|
html += '<td>' + (s.global_execution_concurrency || 1) + '</td>'; |
||||
|
html += '<td>' + (s.detection_interval_seconds || '-') + 's</td>'; |
||||
|
html += '<td>' + (s.last_run_status || '-') + '</td>'; |
||||
|
html += '</tr>'; |
||||
|
}); |
||||
|
html += '</tbody></table>'; |
||||
|
return html; |
||||
|
} |
||||
|
|
||||
|
function renderWorkers(data) { |
||||
|
if (!data || data.length === 0) return '<div class="text-muted">No workers connected for this lane</div>'; |
||||
|
let html = '<table class="table table-sm table-hover mb-0"><thead><tr><th>Worker ID</th><th>Address</th><th>Job Types</th><th>Connected</th><th>Last Seen</th></tr></thead><tbody>'; |
||||
|
data.forEach(w => { |
||||
|
const jobTypes = Object.keys(w.Capabilities || {}).join(', '); |
||||
|
html += '<tr>'; |
||||
|
html += '<td>' + (w.WorkerID || '-') + '</td>'; |
||||
|
html += '<td>' + (w.Address || '-') + '</td>'; |
||||
|
html += '<td>' + jobTypes + '</td>'; |
||||
|
html += '<td>' + (w.ConnectedAt ? new Date(w.ConnectedAt).toLocaleString() : '-') + '</td>'; |
||||
|
html += '<td>' + (w.LastSeenAt ? new Date(w.LastSeenAt).toLocaleString() : '-') + '</td>'; |
||||
|
html += '</tr>'; |
||||
|
}); |
||||
|
html += '</tbody></table>'; |
||||
|
return html; |
||||
|
} |
||||
|
|
||||
|
function refresh() { |
||||
|
fetchAndRender('/api/plugin/scheduler-status?lane=' + lane, 'plugin-lane-scheduler-status', renderSchedulerStatus); |
||||
|
fetchAndRender('/api/plugin/scheduler-states?lane=' + lane, 'plugin-lane-job-types', renderJobTypes); |
||||
|
fetchAndRender('/api/plugin/workers?lane=' + lane, 'plugin-lane-workers', renderWorkers); |
||||
|
} |
||||
|
|
||||
|
refresh(); |
||||
|
const btn = document.getElementById('plugin-lane-refresh-btn'); |
||||
|
if (btn) btn.addEventListener('click', refresh); |
||||
|
})(); |
||||
|
</script> |
||||
|
} |
||||
96
weed/admin/view/app/plugin_lane_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue