You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
57 lines
129 KiB
57 lines
129 KiB
// Code generated by templ - DO NOT EDIT.
|
|
|
|
// templ: version: v0.3.977
|
|
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 Plugin(page string) 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)
|
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
if templ_7745c5c3_Var1 == nil {
|
|
templ_7745c5c3_Var1 = templ.NopComponent
|
|
}
|
|
ctx = templ.ClearChildren(ctx)
|
|
currentPage := page
|
|
if currentPage == "" {
|
|
currentPage = "overview"
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"container-fluid\" id=\"plugin-page\" data-plugin-page=\"")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
var templ_7745c5c3_Var2 string
|
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(currentPage)
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin.templ`, Line: 10, Col: 80}
|
|
}
|
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><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-plug me-2\"></i>Workers</h2><p class=\"text-muted mb-0\">Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.</p></div><div class=\"btn-group\"><button type=\"button\" class=\"btn btn-outline-secondary\" id=\"plugin-refresh-all-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\"><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=\"fas fa-chart-line 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=\"fas fa-sliders-h me-1\"></i>Configuration</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"detection\"><i class=\"fas fa-search me-1\"></i>Job Detection</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"queue\"><i class=\"fas fa-list me-1\"></i>Job Queue</button></li><li class=\"nav-item\"><button type=\"button\" class=\"nav-link\" data-plugin-subtab=\"execution\"><i class=\"fas fa-tasks 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-4 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-4 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-4 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><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow-sm\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-chart-bar 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=\"fas fa-clock me-2\"></i>Scheduler State</h5><small class=\"text-muted\">Per job type detection schedule and execution 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>Next Detection</th><th>Interval</th><th>Exec Global</th><th>Exec/Worker</th><th>Executor Workers</th><th>Effective Exec</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=\"fas fa-users 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=\"fas fa-sliders-h 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=\"fas fa-cloud-download-alt 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=\"fas fa-save me-1\"></i>Save Config</button> <button type=\"button\" class=\"btn btn-outline-primary\" id=\"plugin-trigger-detection-btn\"><i class=\"fas fa-search me-1\"></i>Run Detection</button> <button type=\"button\" class=\"btn btn-outline-success\" id=\"plugin-run-workflow-btn\"><i class=\"fas fa-play me-1\"></i>Run Detect + Execute</button></div></div></div></div><div class=\"col-lg-4 mb-3\"><div class=\"card shadow-sm h-100\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-cogs me-2\"></i>Job Scheduling Settings</h5></div><div class=\"card-body\"><div class=\"row g-2\" id=\"plugin-admin-settings-form\"><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-enabled\">Enabled</label><div class=\"form-check form-switch mt-1\"><input class=\"form-check-input\" type=\"checkbox\" id=\"plugin-admin-enabled\"></div></div><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-detection-interval\">Detection Interval (s)</label> <input type=\"number\" class=\"form-control\" id=\"plugin-admin-detection-interval\" min=\"0\"></div><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-detection-timeout\">Detection Timeout (s)</label> <input type=\"number\" class=\"form-control\" id=\"plugin-admin-detection-timeout\" min=\"0\"></div><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-max-results\">Max Jobs / Detection</label> <input type=\"number\" class=\"form-control\" id=\"plugin-admin-max-results\" min=\"0\"></div><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-global-exec\">Global Execution Concurrency</label> <input type=\"number\" class=\"form-control\" id=\"plugin-admin-global-exec\" min=\"0\"></div><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-per-worker-exec\">Per Worker Concurrency</label> <input type=\"number\" class=\"form-control\" id=\"plugin-admin-per-worker-exec\" min=\"0\"></div><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-retry-limit\">Retry Limit</label> <input type=\"number\" class=\"form-control\" id=\"plugin-admin-retry-limit\" min=\"0\"></div><div class=\"col-12\"><label class=\"form-label\" for=\"plugin-admin-retry-backoff\">Retry Backoff (s)</label> <input type=\"number\" class=\"form-control\" id=\"plugin-admin-retry-backoff\" min=\"0\"></div></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-lg-6 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=\"fas fa-clipboard-check me-2\"></i>Run History</h5><small class=\"text-muted\">Keep last 10 success + last 10 errors</small></div><div class=\"card-body\"><div class=\"row\"><div class=\"col-12 mb-3\"><h6 class=\"text-success\">Successful Runs</h6><div class=\"table-responsive\"><table class=\"table table-sm table-striped\"><thead><tr><th>Time</th><th>Job ID</th><th>Worker</th><th>Duration</th></tr></thead> <tbody id=\"plugin-runs-success-body\"><tr><td colspan=\"4\" class=\"text-muted\">No data</td></tr></tbody></table></div></div><div class=\"col-12\"><h6 class=\"text-danger\">Error Runs</h6><div class=\"table-responsive\"><table class=\"table table-sm table-striped\"><thead><tr><th>Time</th><th>Job ID</th><th>Worker</th><th>Error</th></tr></thead> <tbody id=\"plugin-runs-error-body\"><tr><td colspan=\"4\" class=\"text-muted\">No data</td></tr></tbody></table></div></div></div></div></div></div><div class=\"col-lg-6 mb-3\"><div class=\"card shadow-sm h-100\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-lightbulb me-2\"></i>Detection Results</h5></div><div class=\"card-body\" id=\"plugin-detection-results\"><div class=\"text-muted\">Run detection to see proposals.</div></div></div></div></div></div><div class=\"plugin-section plugin-section-queue\"><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=\"fas fa-list me-2\"></i>Job Queue</h5><div class=\"d-flex gap-2 align-items-center flex-wrap\"><small class=\"text-muted\">States: pending/assigned/running</small></div></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>Job ID</th><th>Type</th><th>State</th><th>Progress</th><th>Worker</th><th>Updated</th><th>Message</th></tr></thead> <tbody id=\"plugin-queue-jobs-table-body\"><tr><td colspan=\"7\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div></div><div class=\"plugin-section plugin-section-detection\"><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=\"fas fa-search me-2\"></i>Detection Jobs</h5><div class=\"small text-muted\">Detection activities for selected job type</div></div><div class=\"card-body p-0\"><div class=\"table-responsive\" style=\"max-height: 420px; overflow-y: auto;\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light sticky-top\"><tr><th>Time</th><th>Job Type</th><th>Request ID</th><th>Worker</th><th>Stage</th><th>Source</th><th>Message</th></tr></thead> <tbody id=\"plugin-detection-table-body\"><tr><td colspan=\"7\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div></div><div class=\"plugin-section plugin-section-execution\"><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=\"fas fa-tasks me-2\"></i>Execution Jobs</h5><div class=\"d-flex gap-2 align-items-center flex-wrap\"><select class=\"form-select form-select-sm\" id=\"plugin-monitor-job-state-filter\" style=\"min-width: 180px;\"><option value=\"\">All States</option> <option value=\"assigned\">Assigned</option> <option value=\"running\">Running</option> <option value=\"succeeded\">Succeeded</option> <option value=\"failed\">Failed</option> <option value=\"canceled\">Canceled</option></select></div></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job ID</th><th>Type</th><th>State</th><th>Progress</th><th>Worker</th><th>Updated</th><th>Message</th></tr></thead> <tbody id=\"plugin-execution-jobs-table-body\"><tr><td colspan=\"7\" 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=\"fas fa-stream me-2\"></i>Execution Activities</h5><small class=\"text-muted\">Non-detection events only</small></div><div class=\"card-body p-0\"><div class=\"table-responsive\" style=\"max-height: 420px; overflow-y: auto;\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light sticky-top\"><tr><th>Time</th><th>Job Type</th><th>Job ID</th><th>Source</th><th>Stage</th><th>Message</th></tr></thead> <tbody id=\"plugin-execution-activities-table-body\"><tr><td colspan=\"6\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div></div></div><div class=\"modal fade\" id=\"plugin-job-detail-modal\" tabindex=\"-1\" aria-labelledby=\"plugin-job-detail-modal-label\" aria-hidden=\"true\"><div class=\"modal-dialog modal-xl modal-dialog-scrollable\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"plugin-job-detail-modal-label\"><i class=\"fas fa-file-alt me-2\"></i>Job Detail</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\" id=\"plugin-job-detail-content\"><div class=\"text-muted\">Select a job to view details.</div></div></div></div></div><style>\n .plugin-section {\n display: none;\n }\n\n .plugin-section.plugin-section-active {\n display: block;\n }\n\n .plugin-form-root .card {\n border: 1px solid #dee2e6;\n }\n\n .plugin-field-hidden {\n display: none;\n }\n\n .plugin-capability-badge {\n font-size: 0.72rem;\n margin-right: 0.25rem;\n }\n\n .plugin-job-progress {\n min-width: 120px;\n }\n\n .plugin-detection-item {\n border: 1px solid #dee2e6;\n border-radius: 6px;\n padding: 0.5rem 0.75rem;\n margin-bottom: 0.5rem;\n background: #fbfcfd;\n }\n\n .plugin-job-link {\n text-decoration: none;\n }\n\n .plugin-job-detail-json {\n max-height: 220px;\n overflow: auto;\n background: #f8f9fa;\n border: 1px solid #dee2e6;\n border-radius: 6px;\n padding: 0.5rem;\n margin-bottom: 0;\n font-size: 0.82rem;\n }\n </style><script>\n (function() {\n var page = document.getElementById('plugin-page');\n if (!page) {\n return;\n }\n\n var state = {\n selectedJobType: '',\n jobTypes: [],\n lastDetectionByJobType: {},\n initialPage: String(page.getAttribute('data-plugin-page') || 'overview').trim().toLowerCase(),\n activeTopTab: 'overview',\n activeSubTab: 'configuration',\n initialNavigationApplied: false,\n monitorExecutionJobState: '',\n workers: [],\n jobs: [],\n activities: [],\n schedulerStates: [],\n allJobs: [],\n allActivities: [],\n loadedJobType: '',\n descriptor: null,\n config: null,\n refreshTimer: null,\n };\n\n function getCSRFToken() {\n var meta = document.querySelector('meta[name=\"csrf-token\"]');\n if (!meta) {\n return '';\n }\n return meta.getAttribute('content') || '';\n }\n\n function notify(message, level) {\n if (typeof window.showAlert === 'function') {\n window.showAlert(message, level || 'info');\n return;\n }\n if (level === 'error') {\n console.error(message);\n }\n alert(message);\n }\n\n async function readResponseJSON(response) {\n var bodyText = await response.text();\n if (!bodyText) {\n return {};\n }\n try {\n return JSON.parse(bodyText);\n } catch (e) {\n return { error: bodyText };\n }\n }\n\n async function pluginRequest(method, url, payload) {\n var headers = {};\n var requestOptions = {\n method: method,\n headers: headers,\n };\n\n if (payload !== undefined && payload !== null) {\n headers['Content-Type'] = 'application/json';\n requestOptions.body = JSON.stringify(payload);\n }\n\n if (method !== 'GET') {\n var csrf = getCSRFToken();\n if (csrf) {\n headers['X-CSRF-Token'] = csrf;\n }\n }\n\n var response = await fetch(url, requestOptions);\n var data = await readResponseJSON(response);\n if (!response.ok) {\n var message = (data && (data.error || data.message)) ? (data.error || data.message) : ('Request failed: ' + response.status);\n var requestError = new Error(message);\n requestError.responseData = data;\n requestError.responseStatus = response.status;\n throw requestError;\n }\n return data;\n }\n\n function textOrDash(value) {\n if (value === null || value === undefined || value === '') {\n return '-';\n }\n return String(value);\n }\n\n function escapeHtml(value) {\n if (value === null || value === undefined) {\n return '';\n }\n return String(value)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n function encodePath(value) {\n return encodeURIComponent(value || '');\n }\n\n function normalizeEnum(value) {\n if (value === undefined || value === null) {\n return '';\n }\n return String(value).toUpperCase();\n }\n\n function parseTime(value) {\n if (!value) {\n return '';\n }\n var date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n return '';\n }\n return date.toLocaleString();\n }\n\n function normalizeSubTab(value) {\n var normalized = String(value || '').toLowerCase();\n if (normalized === 'configuration' || normalized === 'detection' || normalized === 'queue' || normalized === 'execution') {\n return normalized;\n }\n return 'configuration';\n }\n\n function topTabKeyForJobType(jobType) {\n var normalized = String(jobType || '').trim();\n if (!normalized) {\n return '';\n }\n return 'job:' + normalized;\n }\n\n function parseJobTypeFromTopTab(tabKey) {\n var normalized = String(tabKey || '').trim();\n if (normalized.indexOf('job:') !== 0) {\n return '';\n }\n return normalized.slice(4);\n }\n\n function hasJobType(jobType) {\n var normalized = String(jobType || '').trim();\n if (!normalized) {\n return false;\n }\n var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : [];\n for (var i = 0; i < jobTypes.length; i++) {\n if (String(jobTypes[i] || '').trim() === normalized) {\n return true;\n }\n }\n return false;\n }\n\n function applyInitialNavigation() {\n if (state.initialNavigationApplied) {\n return;\n }\n state.initialNavigationApplied = true;\n state.activeSubTab = normalizeSubTab(state.initialPage);\n\n if (state.initialPage === 'overview') {\n state.activeTopTab = 'overview';\n return;\n }\n if (!Array.isArray(state.jobTypes) || state.jobTypes.length === 0) {\n state.activeTopTab = 'overview';\n return;\n }\n state.selectedJobType = String(state.jobTypes[0] || '').trim();\n state.activeTopTab = topTabKeyForJobType(state.selectedJobType);\n }\n\n function ensureActiveNavigation() {\n state.activeSubTab = normalizeSubTab(state.activeSubTab);\n\n if (state.selectedJobType && !hasJobType(state.selectedJobType)) {\n state.selectedJobType = '';\n state.descriptor = null;\n state.config = null;\n }\n\n var topTabJobType = parseJobTypeFromTopTab(state.activeTopTab);\n if (topTabJobType && !hasJobType(topTabJobType)) {\n state.activeTopTab = 'overview';\n topTabJobType = '';\n }\n\n if (state.activeTopTab === 'overview') {\n if (!state.selectedJobType && Array.isArray(state.jobTypes) && state.jobTypes.length > 0) {\n state.selectedJobType = String(state.jobTypes[0] || '').trim();\n }\n return;\n }\n\n if (!topTabJobType && state.selectedJobType) {\n topTabJobType = state.selectedJobType;\n }\n if (!topTabJobType && Array.isArray(state.jobTypes) && state.jobTypes.length > 0) {\n topTabJobType = String(state.jobTypes[0] || '').trim();\n }\n if (!topTabJobType) {\n state.activeTopTab = 'overview';\n return;\n }\n\n state.selectedJobType = topTabJobType;\n state.activeTopTab = topTabKeyForJobType(topTabJobType);\n }\n\n function renderTopTabs() {\n var root = document.getElementById('plugin-top-tabs');\n if (!root) {\n return;\n }\n\n var html = '' +\n '<li class=\"nav-item\">' +\n '<button type=\"button\" class=\"nav-link\" data-plugin-top-tab=\"overview\">' +\n '<i class=\"fas fa-chart-line me-1\"></i>Overview' +\n '</button>' +\n '</li>';\n\n var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : [];\n for (var i = 0; i < jobTypes.length; i++) {\n var jobType = String(jobTypes[i] || '').trim();\n if (!jobType) {\n continue;\n }\n html += '' +\n '<li class=\"nav-item\">' +\n '<button type=\"button\" class=\"nav-link\" data-plugin-top-tab=\"' + escapeHtml(topTabKeyForJobType(jobType)) + '\">' +\n escapeHtml(jobType) +\n '</button>' +\n '</li>';\n }\n root.innerHTML = html;\n }\n\n function renderNavigationState() {\n var selectedJobTypeLabel = document.getElementById('plugin-selected-job-type');\n if (selectedJobTypeLabel) {\n selectedJobTypeLabel.textContent = state.selectedJobType || '-';\n }\n\n var isOverview = state.activeTopTab === 'overview';\n var subtabRow = document.getElementById('plugin-subtab-row');\n if (subtabRow) {\n subtabRow.classList.toggle('d-none', isOverview);\n }\n\n var topTabs = document.querySelectorAll('#plugin-top-tabs [data-plugin-top-tab]');\n for (var i = 0; i < topTabs.length; i++) {\n var topTab = topTabs[i];\n var topKey = String(topTab.getAttribute('data-plugin-top-tab') || '');\n topTab.classList.toggle('active', topKey === state.activeTopTab);\n }\n\n var subTabs = document.querySelectorAll('#plugin-subtabs [data-plugin-subtab]');\n for (var j = 0; j < subTabs.length; j++) {\n var subTab = subTabs[j];\n var subKey = String(subTab.getAttribute('data-plugin-subtab') || '');\n subTab.classList.toggle('active', !isOverview && subKey === state.activeSubTab);\n }\n\n function toggleSection(sectionName, visible) {\n var section = document.querySelector('.plugin-section-' + sectionName);\n if (!section) {\n return;\n }\n section.classList.toggle('plugin-section-active', !!visible);\n }\n\n toggleSection('overview', isOverview);\n toggleSection('configuration', !isOverview && state.activeSubTab === 'configuration');\n toggleSection('detection', !isOverview && state.activeSubTab === 'detection');\n toggleSection('queue', !isOverview && state.activeSubTab === 'queue');\n toggleSection('execution', !isOverview && state.activeSubTab === 'execution');\n }\n\n function renderJobLink(jobID) {\n var normalized = String(jobID || '').trim();\n if (!normalized) {\n return '<small>-</small>';\n }\n return '<button type=\"button\" class=\"btn btn-link btn-sm p-0 align-baseline plugin-job-link\" data-job-id=\"' + escapeHtml(normalized) + '\">' + escapeHtml(normalized) + '</button>';\n }\n\n function toPrettyJson(value) {\n if (value === null || value === undefined || value === '') {\n return '';\n }\n if (typeof value === 'string') {\n return value;\n }\n try {\n return JSON.stringify(value, null, 2);\n } catch (e) {\n return String(value);\n }\n }\n\n function clonePlainObject(value) {\n if (!value || typeof value !== 'object') {\n return null;\n }\n try {\n return JSON.parse(JSON.stringify(value));\n } catch (e) {\n return null;\n }\n }\n\n function extractExecutionPlan(job) {\n if (!job || !job.parameters) {\n return null;\n }\n var plan = job.parameters.execution_plan;\n if (!plan) {\n return null;\n }\n if (typeof plan === 'string') {\n try {\n plan = JSON.parse(plan);\n } catch (e) {\n return null;\n }\n }\n if (typeof plan !== 'object' || Array.isArray(plan)) {\n return null;\n }\n return plan;\n }\n\n function removeExecutionPlanForDisplay(parameters) {\n var copy = clonePlainObject(parameters);\n if (!copy) {\n return parameters;\n }\n if (copy.execution_plan !== undefined) {\n delete copy.execution_plan;\n }\n if (Object.keys(copy).length === 0) {\n return null;\n }\n return copy;\n }\n\n function parseIntOrDefault(value, fallback) {\n var parsed = parseInt(value, 10);\n if (Number.isNaN(parsed)) {\n return fallback;\n }\n return parsed;\n }\n\n function normalizeShardIDs(value) {\n if (!Array.isArray(value)) {\n return [];\n }\n var out = [];\n for (var i = 0; i < value.length; i++) {\n var parsed = parseIntOrDefault(value[i], -1);\n if (parsed >= 0) {\n out.push(parsed);\n }\n }\n out.sort(function(a, b) { return a - b; });\n return out;\n }\n\n function classifyShardLabel(shardID, dataShardCount) {\n if (shardID < dataShardCount) {\n return {\n kind: 'data',\n label: String(shardID),\n className: 'bg-primary',\n title: 'Data shard ' + shardID,\n };\n }\n var parityIndex = shardID - dataShardCount;\n return {\n kind: 'parity',\n label: String(shardID),\n className: 'bg-warning text-dark',\n title: 'Parity shard ' + shardID + ' (index ' + parityIndex + ')',\n };\n }\n\n function renderShardBadges(shardIDs, dataShardCount) {\n if (!Array.isArray(shardIDs) || shardIDs.length === 0) {\n return '<span class=\"text-muted\">-</span>';\n }\n var html = '';\n for (var i = 0; i < shardIDs.length; i++) {\n var shard = classifyShardLabel(shardIDs[i], dataShardCount);\n html += '<span class=\"badge me-1 ' + shard.className + '\" title=\"' + escapeHtml(shard.title) + '\">' + escapeHtml(shard.label) + '</span>';\n }\n return html;\n }\n\n function renderExecutionPlan(plan) {\n if (!plan || typeof plan !== 'object') {\n return '';\n }\n\n var jobType = String(plan.job_type || '').trim().toLowerCase();\n if (jobType !== 'erasure_coding') {\n var fallbackText = toPrettyJson(plan);\n if (!fallbackText) {\n return '';\n }\n return '<div class=\"mb-3\"><h6>Execution Plan</h6><pre class=\"plugin-job-detail-json\">' + escapeHtml(fallbackText) + '</pre></div>';\n }\n\n var dataShards = parseIntOrDefault(plan.data_shards, 10);\n var parityShards = parseIntOrDefault(plan.parity_shards, 4);\n var totalShards = parseIntOrDefault(plan.total_shards, dataShards + parityShards);\n var sources = Array.isArray(plan.sources) ? plan.sources : [];\n var targets = Array.isArray(plan.targets) ? plan.targets : [];\n var assignments = Array.isArray(plan.shard_assignments) ? plan.shard_assignments : [];\n\n var html = '';\n html += '<div class=\"mb-3\"><h6>Execution Plan</h6>';\n html += '<div class=\"row g-2 mb-2\">' +\n '<div class=\"col-md-3\"><small><strong>Volume:</strong> ' + escapeHtml(textOrDash(plan.volume_id)) + '</small></div>' +\n '<div class=\"col-md-3\"><small><strong>Collection:</strong> ' + escapeHtml(textOrDash(plan.collection)) + '</small></div>' +\n '<div class=\"col-md-3\"><small><strong>Shards:</strong> ' + escapeHtml(String(dataShards)) + '+' + escapeHtml(String(parityShards)) + '</small></div>' +\n '<div class=\"col-md-3\"><small><strong>Total:</strong> ' + escapeHtml(String(totalShards)) + '</small></div>' +\n '</div>';\n\n html += '<div class=\"small text-muted mb-2\">' +\n '<span class=\"badge bg-primary me-1\">Data</span>' +\n '<span class=\"badge bg-warning text-dark me-2\">Parity</span>' +\n 'Source and target placement per shard.' +\n '</div>';\n\n function renderEndpointsTable(title, endpoints) {\n if (!endpoints.length) {\n return '<div class=\"mb-2\"><strong>' + escapeHtml(title) + ':</strong> <span class=\"text-muted\">none</span></div>';\n }\n var table = '<div class=\"table-responsive mb-2\"><table class=\"table table-sm table-striped mb-0\">' +\n '<thead><tr><th>#</th><th>Node</th><th>DC</th><th>Rack</th><th>Volume</th><th>Shards</th></tr></thead><tbody>';\n for (var i = 0; i < endpoints.length; i++) {\n var endpoint = endpoints[i] || {};\n var endpointShards = normalizeShardIDs(endpoint.shard_ids);\n table += '<tr>' +\n '<td>' + escapeHtml(String(i + 1)) + '</td>' +\n '<td><code>' + escapeHtml(textOrDash(endpoint.node)) + '</code></td>' +\n '<td>' + escapeHtml(textOrDash(endpoint.data_center)) + '</td>' +\n '<td>' + escapeHtml(textOrDash(endpoint.rack)) + '</td>' +\n '<td>' + escapeHtml(textOrDash(endpoint.volume_id)) + '</td>' +\n '<td>' + renderShardBadges(endpointShards, dataShards) + '</td>' +\n '</tr>';\n }\n table += '</tbody></table></div>';\n return '<div class=\"mb-2\"><strong>' + escapeHtml(title) + '</strong></div>' + table;\n }\n\n html += renderEndpointsTable('Sources', sources);\n html += renderEndpointsTable('Targets', targets);\n\n if (assignments.length) {\n html += '<div class=\"mb-2\"><strong>Shard Assignments</strong></div>';\n html += '<div class=\"table-responsive\"><table class=\"table table-sm table-striped mb-0\">' +\n '<thead><tr><th>Shard</th><th>Kind</th><th>Target</th><th>DC</th><th>Rack</th></tr></thead><tbody>';\n for (var j = 0; j < assignments.length; j++) {\n var assignment = assignments[j] || {};\n var shardID = parseIntOrDefault(assignment.shard_id, -1);\n if (shardID < 0) {\n continue;\n }\n var shard = classifyShardLabel(shardID, dataShards);\n html += '<tr>' +\n '<td><span class=\"badge ' + shard.className + '\">' + escapeHtml(shard.label) + '</span></td>' +\n '<td>' + escapeHtml(textOrDash(assignment.kind || shard.kind)) + '</td>' +\n '<td><code>' + escapeHtml(textOrDash(assignment.target_node)) + '</code></td>' +\n '<td>' + escapeHtml(textOrDash(assignment.target_data_center)) + '</td>' +\n '<td>' + escapeHtml(textOrDash(assignment.target_rack)) + '</td>' +\n '</tr>';\n }\n html += '</tbody></table></div>';\n }\n\n html += '</div>';\n return html;\n }\n\n async function openJobDetail(jobID) {\n var normalizedJobID = String(jobID || '').trim();\n if (!normalizedJobID) {\n return;\n }\n\n var modalElement = document.getElementById('plugin-job-detail-modal');\n var contentRoot = document.getElementById('plugin-job-detail-content');\n if (!modalElement || !contentRoot) {\n return;\n }\n\n var modal;\n if (window.bootstrap && window.bootstrap.Modal) {\n modal = window.bootstrap.Modal.getOrCreateInstance(modalElement);\n }\n if (modal) {\n modal.show();\n }\n\n contentRoot.innerHTML = '<div class=\"text-muted\">Loading job detail...</div>';\n try {\n var detail = await pluginRequest('GET', '/api/plugin/jobs/' + encodePath(normalizedJobID) + '/detail?activity_limit=500&related_limit=20');\n var job = (detail && detail.job) ? detail.job : {};\n var runRecord = detail && detail.run_record ? detail.run_record : null;\n var activities = (detail && Array.isArray(detail.activities)) ? detail.activities : [];\n var relatedJobs = (detail && Array.isArray(detail.related_jobs)) ? detail.related_jobs : [];\n var html = '';\n\n html += '<div class=\"row g-3 mb-3\">' +\n '<div class=\"col-lg-8\">' +\n '<div><strong>Job ID:</strong> ' + escapeHtml(textOrDash(job.job_id || normalizedJobID)) + '</div>' +\n '<div><strong>Type:</strong> ' + escapeHtml(textOrDash(job.job_type)) + '</div>' +\n '<div><strong>State:</strong> ' + escapeHtml(textOrDash(job.state)) + '</div>' +\n '<div><strong>Worker:</strong> ' + escapeHtml(textOrDash(job.worker_id)) + '</div>' +\n '<div><strong>Updated:</strong> ' + escapeHtml(parseTime(job.updated_at) || '-') + '</div>' +\n '</div>' +\n '<div class=\"col-lg-4\">' +\n '<div><strong>Attempt:</strong> ' + escapeHtml(textOrDash(job.attempt)) + '</div>' +\n '<div><strong>Progress:</strong> ' + escapeHtml(String(Math.round(Number(job.progress || 0)))) + '%</div>' +\n '<div><strong>Created:</strong> ' + escapeHtml(parseTime(job.created_at) || '-') + '</div>' +\n '<div><strong>Completed:</strong> ' + escapeHtml(parseTime(job.completed_at) || '-') + '</div>' +\n '</div>' +\n '</div>';\n\n if (job.summary) {\n html += '<div class=\"mb-2\"><strong>Summary:</strong> ' + escapeHtml(String(job.summary)) + '</div>';\n }\n if (job.detail) {\n html += '<div class=\"mb-2\"><strong>Detail:</strong> ' + escapeHtml(String(job.detail)) + '</div>';\n }\n if (job.message) {\n html += '<div class=\"mb-2\"><strong>Message:</strong> ' + escapeHtml(String(job.message)) + '</div>';\n }\n if (job.error_message) {\n html += '<div class=\"mb-2 text-danger\"><strong>Error:</strong> ' + escapeHtml(String(job.error_message)) + '</div>';\n }\n if (job.result_summary) {\n html += '<div class=\"mb-2\"><strong>Result:</strong> ' + escapeHtml(String(job.result_summary)) + '</div>';\n }\n\n var executionPlan = extractExecutionPlan(job);\n if (executionPlan) {\n html += renderExecutionPlan(executionPlan);\n }\n\n var parametersText = toPrettyJson(removeExecutionPlanForDisplay(job.parameters));\n if (parametersText) {\n html += '<div class=\"mb-3\"><h6>Parameters</h6><pre class=\"plugin-job-detail-json\">' + escapeHtml(parametersText) + '</pre></div>';\n }\n var labelsText = toPrettyJson(job.labels);\n if (labelsText) {\n html += '<div class=\"mb-3\"><h6>Labels</h6><pre class=\"plugin-job-detail-json\">' + escapeHtml(labelsText) + '</pre></div>';\n }\n var outputsText = toPrettyJson(job.result_output_values);\n if (outputsText) {\n html += '<div class=\"mb-3\"><h6>Result Output Values</h6><pre class=\"plugin-job-detail-json\">' + escapeHtml(outputsText) + '</pre></div>';\n }\n\n if (runRecord) {\n html += '<div class=\"mb-3\"><h6>Run Record</h6>' +\n '<div><strong>Outcome:</strong> ' + escapeHtml(textOrDash(runRecord.outcome)) + '</div>' +\n '<div><strong>Duration:</strong> ' + escapeHtml(formatDurationMs(runRecord.duration_ms)) + '</div>' +\n '<div><strong>Completed:</strong> ' + escapeHtml(parseTime(runRecord.completed_at) || '-') + '</div>' +\n '<div><strong>Message:</strong> ' + escapeHtml(textOrDash(runRecord.message)) + '</div>' +\n '</div>';\n }\n\n html += '<h6 class=\"mt-3\">Activity Timeline</h6>';\n if (!activities.length) {\n html += '<div class=\"text-muted mb-3\">No activity records.</div>';\n } else {\n html += '<div class=\"table-responsive mb-3\"><table class=\"table table-sm table-striped\">' +\n '<thead><tr><th>Time</th><th>Stage</th><th>Source</th><th>Worker</th><th>Message</th></tr></thead><tbody>';\n for (var i = 0; i < activities.length; i++) {\n var activity = activities[i] || {};\n html += '<tr>' +\n '<td><small>' + escapeHtml(parseTime(activity.occurred_at) || '-') + '</small></td>' +\n '<td>' + escapeHtml(textOrDash(activity.stage)) + '</td>' +\n '<td>' + escapeHtml(textOrDash(activity.source)) + '</td>' +\n '<td>' + escapeHtml(textOrDash(activity.worker_id)) + '</td>' +\n '<td><small>' + escapeHtml(textOrDash(activity.message)) + '</small></td>' +\n '</tr>';\n }\n html += '</tbody></table></div>';\n }\n\n if (relatedJobs.length) {\n html += '<h6>Related Jobs</h6><div class=\"table-responsive\"><table class=\"table table-sm table-hover\">' +\n '<thead><tr><th>Job ID</th><th>State</th><th>Worker</th><th>Updated</th></tr></thead><tbody>';\n for (var j = 0; j < relatedJobs.length; j++) {\n var related = relatedJobs[j] || {};\n html += '<tr>' +\n '<td><small>' + escapeHtml(textOrDash(related.job_id)) + '</small></td>' +\n '<td>' + escapeHtml(textOrDash(related.state)) + '</td>' +\n '<td>' + escapeHtml(textOrDash(related.worker_id)) + '</td>' +\n '<td><small>' + escapeHtml(parseTime(related.updated_at) || '-') + '</small></td>' +\n '</tr>';\n }\n html += '</tbody></table></div>';\n }\n\n contentRoot.innerHTML = html;\n } catch (e) {\n contentRoot.innerHTML = '<div class=\"alert alert-danger mb-0\">Failed to load job detail: ' + escapeHtml(e.message) + '</div>';\n }\n }\n\n function formatDurationMs(value) {\n var ms = Number(value || 0);\n if (!Number.isFinite(ms) || ms <= 0) {\n return '-';\n }\n if (ms < 1000) {\n return ms + 'ms';\n }\n var sec = Math.floor(ms / 1000);\n var remainMs = ms % 1000;\n if (sec < 60) {\n return sec + '.' + String(Math.floor(remainMs / 100)) + 's';\n }\n var min = Math.floor(sec / 60);\n var remainSec = sec % 60;\n return min + 'm ' + remainSec + 's';\n }\n\n function getWorkerField(worker, name) {\n if (!worker) {\n return null;\n }\n if (worker[name] !== undefined) {\n return worker[name];\n }\n var camel = name.charAt(0).toUpperCase() + name.slice(1);\n if (worker[camel] !== undefined) {\n return worker[camel];\n }\n return null;\n }\n\n function renderStatus() {\n var allJobs = Array.isArray(state.allJobs) ? state.allJobs : [];\n var allActivities = Array.isArray(state.allActivities) ? state.allActivities : [];\n\n var activeCount = allJobs.filter(function(job) {\n var st = String(job.state || '').toLowerCase();\n return st === 'job_state_pending' || st === 'job_state_assigned' || st === 'job_state_running' || st === 'pending' || st === 'assigned' || st === 'running' || st === 'in_progress';\n }).length;\n\n document.getElementById('plugin-status-workers').textContent = String(state.workers.length);\n document.getElementById('plugin-status-active-jobs').textContent = String(activeCount);\n document.getElementById('plugin-status-activities').textContent = String(allActivities.length);\n }\n\n function renderJobTypeSummary() {\n var tbody = document.getElementById('plugin-jobtype-summary-body');\n if (!tbody) {\n return;\n }\n\n var activeByType = {};\n var activityByType = {};\n\n var allJobs = Array.isArray(state.allJobs) ? state.allJobs : [];\n var allActivities = Array.isArray(state.allActivities) ? state.allActivities : [];\n\n for (var i = 0; i < allJobs.length; i++) {\n var job = allJobs[i] || {};\n var jobType = String(job.job_type || '').trim();\n if (!jobType) {\n continue;\n }\n var st = String(job.state || '').toLowerCase();\n var isActive = st === 'job_state_pending' || st === 'job_state_assigned' || st === 'job_state_running' || st === 'pending' || st === 'assigned' || st === 'running' || st === 'in_progress';\n if (!isActive) {\n continue;\n }\n if (!activeByType[jobType]) {\n activeByType[jobType] = 0;\n }\n activeByType[jobType]++;\n }\n\n for (var j = 0; j < allActivities.length; j++) {\n var activity = allActivities[j] || {};\n var activityJobType = String(activity.job_type || '').trim();\n if (!activityJobType) {\n continue;\n }\n if (!activityByType[activityJobType]) {\n activityByType[activityJobType] = 0;\n }\n activityByType[activityJobType]++;\n }\n\n var known = Array.isArray(state.jobTypes) ? state.jobTypes.slice() : [];\n var seen = {};\n var jobTypes = [];\n\n for (var k = 0; k < known.length; k++) {\n var knownType = String(known[k] || '').trim();\n if (!knownType || seen[knownType]) {\n continue;\n }\n seen[knownType] = true;\n jobTypes.push(knownType);\n }\n\n Object.keys(activeByType).forEach(function(jobType) {\n if (!seen[jobType]) {\n seen[jobType] = true;\n jobTypes.push(jobType);\n }\n });\n Object.keys(activityByType).forEach(function(jobType) {\n if (!seen[jobType]) {\n seen[jobType] = true;\n jobTypes.push(jobType);\n }\n });\n\n jobTypes.sort();\n\n if (jobTypes.length === 0) {\n tbody.innerHTML = '<tr><td colspan=\\\"3\\\" class=\\\"text-muted text-center py-3\\\">No job types discovered</td></tr>';\n return;\n }\n\n var rows = '';\n for (var m = 0; m < jobTypes.length; m++) {\n var rowType = jobTypes[m];\n rows += '<tr>' +\n '<td>' + escapeHtml(rowType) + '</td>' +\n '<td><span class=\\\"badge bg-warning text-dark\\\">' + escapeHtml(String(activeByType[rowType] || 0)) + '</span></td>' +\n '<td><span class=\\\"badge bg-info\\\">' + escapeHtml(String(activityByType[rowType] || 0)) + '</span></td>' +\n '</tr>';\n }\n tbody.innerHTML = rows;\n }\n\n function computeEffectiveExecutionCapacity(globalExec, perWorkerExec, executorWorkers) {\n var capacities = [];\n if (globalExec > 0) {\n capacities.push(globalExec);\n }\n var workerCapacity = perWorkerExec > 0 && executorWorkers > 0 ? (perWorkerExec * executorWorkers) : 0;\n if (workerCapacity > 0) {\n capacities.push(workerCapacity);\n }\n if (!capacities.length) {\n return 0;\n }\n var effective = capacities[0];\n for (var i = 1; i < capacities.length; i++) {\n if (capacities[i] < effective) {\n effective = capacities[i];\n }\n }\n return effective;\n }\n\n function renderSchedulerStates() {\n var tbody = document.getElementById('plugin-scheduler-table-body');\n if (!tbody) {\n return;\n }\n\n var states = Array.isArray(state.schedulerStates) ? state.schedulerStates : [];\n if (!states.length) {\n tbody.innerHTML = '<tr><td colspan=\"10\" class=\"text-muted text-center py-3\">No scheduler state available</td></tr>';\n return;\n }\n\n var rows = '';\n for (var i = 0; i < states.length; i++) {\n var item = states[i] || {};\n var enabled = !!item.enabled;\n var inFlight = !!item.detection_in_flight;\n var detector = item.detector_available ? textOrDash(item.detector_worker_id) : 'No detector';\n var intervalSeconds = Number(item.detection_interval_seconds || 0);\n var intervalText = intervalSeconds > 0 ? (String(intervalSeconds) + 's') : '-';\n var globalExec = Number(item.global_execution_concurrency || 0);\n var perWorkerExec = Number(item.per_worker_execution_concurrency || 0);\n var executorWorkers = Number(item.executor_worker_count || 0);\n var effectiveExec = computeEffectiveExecutionCapacity(globalExec, perWorkerExec, executorWorkers);\n var globalExecText = enabled ? String(globalExec) : '-';\n var perWorkerExecText = enabled ? String(perWorkerExec) : '-';\n var executorWorkersText = enabled ? String(executorWorkers) : '-';\n var effectiveExecText = enabled ? String(effectiveExec) : '-';\n\n var enabledBadge = enabled ? '<span class=\"badge bg-success\">Enabled</span>' : '<span class=\"badge bg-secondary\">Disabled</span>';\n var inFlightBadge = inFlight ? '<span class=\"badge bg-warning text-dark\">Yes</span>' : '<span class=\"badge bg-light text-dark\">No</span>';\n var policyErrorHtml = '';\n if (item.policy_error) {\n policyErrorHtml = '<div><small class=\"text-danger\">' + escapeHtml(String(item.policy_error)) + '</small></div>';\n }\n\n rows += '<tr>' +\n '<td>' + escapeHtml(textOrDash(item.job_type)) + policyErrorHtml + '</td>' +\n '<td>' + enabledBadge + '</td>' +\n '<td><small>' + escapeHtml(detector) + '</small></td>' +\n '<td>' + inFlightBadge + '</td>' +\n '<td><small>' + escapeHtml(parseTime(item.next_detection_at) || '-') + '</small></td>' +\n '<td><small>' + escapeHtml(intervalText) + '</small></td>' +\n '<td><small>' + escapeHtml(globalExecText) + '</small></td>' +\n '<td><small>' + escapeHtml(perWorkerExecText) + '</small></td>' +\n '<td><small>' + escapeHtml(executorWorkersText) + '</small></td>' +\n '<td><small>' + escapeHtml(effectiveExecText) + '</small></td>' +\n '</tr>';\n }\n\n tbody.innerHTML = rows;\n }\n\n function renderWorkers() {\n var tbody = document.getElementById('plugin-workers-table-body');\n if (!state.workers.length) {\n tbody.innerHTML = '<tr><td colspan=\"4\" class=\"text-muted text-center py-3\">No connected workers</td></tr>';\n return;\n }\n\n var rows = '';\n for (var i = 0; i < state.workers.length; i++) {\n var worker = state.workers[i];\n var workerID = textOrDash(getWorkerField(worker, 'workerID'));\n var address = textOrDash(getWorkerField(worker, 'address'));\n var caps = getWorkerField(worker, 'capabilities') || {};\n var capBadges = [];\n\n Object.keys(caps).sort().forEach(function(jobType) {\n var cap = caps[jobType] || {};\n var labels = [];\n if (cap.can_detect) {\n labels.push('D');\n }\n if (cap.can_execute) {\n labels.push('E');\n }\n var text = jobType;\n if (labels.length) {\n text += ' (' + labels.join('/') + ')';\n }\n capBadges.push('<span class=\"badge bg-secondary plugin-capability-badge\">' + escapeHtml(text) + '</span>');\n });\n\n if (!capBadges.length) {\n capBadges.push('<span class=\"text-muted\">-</span>');\n }\n\n var heartbeat = getWorkerField(worker, 'heartbeat') || {};\n var detUsed = Number(heartbeat.detection_slots_used || heartbeat.DetectionSlotsUsed || 0);\n var detTotal = Number(heartbeat.detection_slots_total || heartbeat.DetectionSlotsTotal || 0);\n var execUsed = Number(heartbeat.execution_slots_used || heartbeat.ExecutionSlotsUsed || 0);\n var execTotal = Number(heartbeat.execution_slots_total || heartbeat.ExecutionSlotsTotal || 0);\n var runningWork = heartbeat.running_work || heartbeat.RunningWork || [];\n var queuedByType = heartbeat.queued_jobs_by_type || heartbeat.QueuedJobsByType || {};\n var queuedTotal = 0;\n Object.keys(queuedByType || {}).forEach(function(jobType) {\n var queuedCount = Number(queuedByType[jobType] || 0);\n if (Number.isFinite(queuedCount) && queuedCount > 0) {\n queuedTotal += queuedCount;\n }\n });\n var loadLines = [];\n loadLines.push('Detect: ' + String(detUsed) + '/' + String(detTotal));\n loadLines.push('Execute: ' + String(execUsed) + '/' + String(execTotal));\n loadLines.push('Running: ' + String(Array.isArray(runningWork) ? runningWork.length : 0));\n loadLines.push('Queued: ' + String(queuedTotal));\n\n rows += '<tr>' +\n '<td><div class=\"fw-semibold\">' + escapeHtml(workerID) + '</div></td>' +\n '<td><small>' + escapeHtml(address) + '</small></td>' +\n '<td>' + capBadges.join('') + '</td>' +\n '<td><small>' + escapeHtml(loadLines.join(' | ')) + '</small></td>' +\n '</tr>';\n }\n\n tbody.innerHTML = rows;\n }\n\n function normalizeJobStateForFilter(value) {\n var normalized = String(value || '').toLowerCase();\n if (normalized.indexOf('job_state_') === 0) {\n normalized = normalized.slice('job_state_'.length);\n }\n return normalized;\n }\n\n function matchesExecutionStateFilter(jobState, selectedFilter) {\n var target = normalizeJobStateForFilter(selectedFilter);\n if (!target) {\n return true;\n }\n var actual = normalizeJobStateForFilter(jobState);\n if (actual === target) {\n return true;\n }\n if (target === 'running' && actual === 'in_progress') {\n return true;\n }\n return false;\n }\n\n function isDetectionActivitySource(source) {\n var normalized = String(source || '').toLowerCase();\n if (!normalized) {\n return false;\n }\n if (normalized === 'worker_detection') {\n return true;\n }\n return normalized.indexOf('detector') >= 0;\n }\n\n function renderExecutionJobs() {\n var tbody = document.getElementById('plugin-execution-jobs-table-body');\n if (!tbody) {\n return;\n }\n\n var filteredJobs = [];\n var allJobs = Array.isArray(state.allJobs) ? state.allJobs : [];\n for (var i = 0; i < allJobs.length; i++) {\n var job = allJobs[i] || {};\n var jobType = String(job.job_type || '').trim();\n if (state.selectedJobType && jobType !== state.selectedJobType) {\n continue;\n }\n if (!matchesExecutionStateFilter(job.state, state.monitorExecutionJobState)) {\n continue;\n }\n filteredJobs.push(job);\n }\n\n if (!filteredJobs.length) {\n tbody.innerHTML = '<tr><td colspan=\"7\" class=\"text-muted text-center py-3\">No execution jobs tracked yet</td></tr>';\n return;\n }\n\n var rows = '';\n for (var j = 0; j < filteredJobs.length; j++) {\n var executionJob = filteredJobs[j] || {};\n var progress = Number(executionJob.progress || 0);\n if (!Number.isFinite(progress) || progress < 0) {\n progress = 0;\n }\n if (progress > 100) {\n progress = 100;\n }\n\n var barClass = 'bg-info';\n var stateText = String(executionJob.state || 'unknown').toLowerCase();\n if (stateText.indexOf('failed') >= 0) {\n barClass = 'bg-danger';\n } else if (stateText.indexOf('succeed') >= 0 || stateText.indexOf('complete') >= 0) {\n barClass = 'bg-success';\n } else if (stateText.indexOf('run') >= 0 || stateText.indexOf('progress') >= 0) {\n barClass = 'bg-warning';\n }\n\n rows += '<tr>' +\n '<td>' + renderJobLink(executionJob.job_id) + '</td>' +\n '<td>' + escapeHtml(textOrDash(executionJob.job_type)) + '</td>' +\n '<td><span class=\"badge bg-light text-dark\">' + escapeHtml(textOrDash(executionJob.state)) + '</span></td>' +\n '<td class=\"plugin-job-progress\"><div class=\"progress\" style=\"height: 14px;\"><div class=\"progress-bar ' + barClass + '\" role=\"progressbar\" style=\"width:' + progress + '%\">' + Math.round(progress) + '%</div></div></td>' +\n '<td><small>' + escapeHtml(textOrDash(executionJob.worker_id)) + '</small></td>' +\n '<td><small>' + escapeHtml(parseTime(executionJob.updated_at) || '-') + '</small></td>' +\n '<td><small>' + escapeHtml(textOrDash(executionJob.message)) + '</small></td>' +\n '</tr>';\n }\n\n tbody.innerHTML = rows;\n }\n\n function renderQueueJobs() {\n var tbody = document.getElementById('plugin-queue-jobs-table-body');\n if (!tbody) {\n return;\n }\n\n var filteredJobs = [];\n var allJobs = Array.isArray(state.allJobs) ? state.allJobs : [];\n for (var i = 0; i < allJobs.length; i++) {\n var queueJob = allJobs[i] || {};\n var queueJobType = String(queueJob.job_type || '').trim();\n if (state.selectedJobType && queueJobType !== state.selectedJobType) {\n continue;\n }\n var normalizedState = normalizeJobStateForFilter(queueJob.state);\n if (normalizedState !== 'pending' && normalizedState !== 'assigned' && normalizedState !== 'running' && normalizedState !== 'in_progress') {\n continue;\n }\n filteredJobs.push(queueJob);\n }\n\n if (!filteredJobs.length) {\n tbody.innerHTML = '<tr><td colspan=\"7\" class=\"text-muted text-center py-3\">No queued/active jobs</td></tr>';\n return;\n }\n\n var rows = '';\n for (var j = 0; j < filteredJobs.length; j++) {\n var queueItem = filteredJobs[j] || {};\n var progress = Number(queueItem.progress || 0);\n if (!Number.isFinite(progress) || progress < 0) {\n progress = 0;\n }\n if (progress > 100) {\n progress = 100;\n }\n\n var barClass = 'bg-warning';\n var queueStateText = String(queueItem.state || 'unknown').toLowerCase();\n if (queueStateText.indexOf('run') >= 0 || queueStateText.indexOf('progress') >= 0) {\n barClass = 'bg-info';\n }\n\n rows += '<tr>' +\n '<td>' + renderJobLink(queueItem.job_id) + '</td>' +\n '<td>' + escapeHtml(textOrDash(queueItem.job_type)) + '</td>' +\n '<td><span class=\"badge bg-light text-dark\">' + escapeHtml(textOrDash(queueItem.state)) + '</span></td>' +\n '<td class=\"plugin-job-progress\"><div class=\"progress\" style=\"height: 14px;\"><div class=\"progress-bar ' + barClass + '\" role=\"progressbar\" style=\"width:' + progress + '%\">' + Math.round(progress) + '%</div></div></td>' +\n '<td><small>' + escapeHtml(textOrDash(queueItem.worker_id)) + '</small></td>' +\n '<td><small>' + escapeHtml(parseTime(queueItem.updated_at) || '-') + '</small></td>' +\n '<td><small>' + escapeHtml(textOrDash(queueItem.message)) + '</small></td>' +\n '</tr>';\n }\n\n tbody.innerHTML = rows;\n }\n\n function renderDetectionJobs() {\n var tbody = document.getElementById('plugin-detection-table-body');\n if (!tbody) {\n return;\n }\n\n var filteredActivities = [];\n var allActivities = Array.isArray(state.allActivities) ? state.allActivities : [];\n for (var i = 0; i < allActivities.length; i++) {\n var activity = allActivities[i] || {};\n var jobType = String(activity.job_type || '').trim();\n if (state.selectedJobType && jobType !== state.selectedJobType) {\n continue;\n }\n if (!isDetectionActivitySource(activity.source)) {\n continue;\n }\n filteredActivities.push(activity);\n }\n\n if (!filteredActivities.length) {\n tbody.innerHTML = '<tr><td colspan=\"7\" class=\"text-muted text-center py-3\">No detection jobs tracked yet</td></tr>';\n return;\n }\n\n var rows = '';\n for (var j = 0; j < filteredActivities.length; j++) {\n var detectionActivity = filteredActivities[j] || {};\n rows += '<tr>' +\n '<td><small>' + escapeHtml(parseTime(detectionActivity.occurred_at) || '-') + '</small></td>' +\n '<td>' + escapeHtml(textOrDash(detectionActivity.job_type)) + '</td>' +\n '<td><small>' + escapeHtml(textOrDash(detectionActivity.request_id)) + '</small></td>' +\n '<td><small>' + escapeHtml(textOrDash(detectionActivity.worker_id)) + '</small></td>' +\n '<td>' + escapeHtml(textOrDash(detectionActivity.stage)) + '</td>' +\n '<td><span class=\"badge bg-light text-dark\">' + escapeHtml(textOrDash(detectionActivity.source)) + '</span></td>' +\n '<td><small>' + escapeHtml(textOrDash(detectionActivity.message)) + '</small></td>' +\n '</tr>';\n }\n\n tbody.innerHTML = rows;\n }\n\n function renderExecutionActivities() {\n var tbody = document.getElementById('plugin-execution-activities-table-body');\n if (!tbody) {\n return;\n }\n\n var filteredActivities = [];\n var allActivities = Array.isArray(state.allActivities) ? state.allActivities : [];\n for (var i = 0; i < allActivities.length; i++) {\n var activity = allActivities[i] || {};\n if (isDetectionActivitySource(activity.source)) {\n continue;\n }\n var jobType = String(activity.job_type || '').trim();\n if (state.selectedJobType && jobType !== state.selectedJobType) {\n continue;\n }\n filteredActivities.push(activity);\n }\n\n if (!filteredActivities.length) {\n tbody.innerHTML = '<tr><td colspan=\"6\" class=\"text-muted text-center py-3\">No execution activities yet</td></tr>';\n return;\n }\n\n var rows = '';\n for (var j = 0; j < filteredActivities.length; j++) {\n var executionActivity = filteredActivities[j] || {};\n rows += '<tr>' +\n '<td><small>' + escapeHtml(parseTime(executionActivity.occurred_at) || '-') + '</small></td>' +\n '<td>' + escapeHtml(textOrDash(executionActivity.job_type)) + '</td>' +\n '<td>' + renderJobLink(executionActivity.job_id) + '</td>' +\n '<td><span class=\"badge bg-light text-dark\">' + escapeHtml(textOrDash(executionActivity.source)) + '</span></td>' +\n '<td>' + escapeHtml(textOrDash(executionActivity.stage)) + '</td>' +\n '<td><small>' + escapeHtml(textOrDash(executionActivity.message)) + '</small></td>' +\n '</tr>';\n }\n\n tbody.innerHTML = rows;\n }\n\n function getSelectValueArray(selectEl) {\n if (!selectEl) {\n return [];\n }\n var values = [];\n for (var i = 0; i < selectEl.options.length; i++) {\n if (selectEl.options[i].selected) {\n values.push(selectEl.options[i].value);\n }\n }\n return values;\n }\n\n function configValueToNative(configValue) {\n if (!configValue || typeof configValue !== 'object') {\n return null;\n }\n if (Object.prototype.hasOwnProperty.call(configValue, 'bool_value')) {\n return !!configValue.bool_value;\n }\n if (Object.prototype.hasOwnProperty.call(configValue, 'int64_value')) {\n return configValue.int64_value;\n }\n if (Object.prototype.hasOwnProperty.call(configValue, 'double_value')) {\n return configValue.double_value;\n }\n if (Object.prototype.hasOwnProperty.call(configValue, 'string_value')) {\n return configValue.string_value;\n }\n if (Object.prototype.hasOwnProperty.call(configValue, 'bytes_value')) {\n return configValue.bytes_value;\n }\n if (Object.prototype.hasOwnProperty.call(configValue, 'duration_value')) {\n return configValue.duration_value;\n }\n if (configValue.string_list && Array.isArray(configValue.string_list.values)) {\n return configValue.string_list.values.slice();\n }\n if (configValue.int64_list && Array.isArray(configValue.int64_list.values)) {\n return configValue.int64_list.values.slice();\n }\n if (configValue.double_list && Array.isArray(configValue.double_list.values)) {\n return configValue.double_list.values.slice();\n }\n if (configValue.bool_list && Array.isArray(configValue.bool_list.values)) {\n return configValue.bool_list.values.slice();\n }\n if (configValue.list_value && Array.isArray(configValue.list_value.values)) {\n return configValue.list_value.values.map(function(v) {\n return configValueToNative(v);\n });\n }\n if (configValue.map_value && configValue.map_value.fields) {\n var out = {};\n Object.keys(configValue.map_value.fields).forEach(function(k) {\n out[k] = configValueToNative(configValue.map_value.fields[k]);\n });\n return out;\n }\n return null;\n }\n\n function inferConfigValue(value) {\n if (value === null || value === undefined) {\n return { string_value: '' };\n }\n if (typeof value === 'boolean') {\n return { bool_value: value };\n }\n if (typeof value === 'number') {\n if (Number.isInteger(value)) {\n return { int64_value: String(value) };\n }\n return { double_value: value };\n }\n if (typeof value === 'string') {\n return { string_value: value };\n }\n if (Array.isArray(value)) {\n var listValues = value.map(function(item) {\n return inferConfigValue(item);\n });\n return { list_value: { values: listValues } };\n }\n if (typeof value === 'object') {\n var fields = {};\n Object.keys(value).forEach(function(key) {\n fields[key] = inferConfigValue(value[key]);\n });\n return { map_value: { fields: fields } };\n }\n return { string_value: String(value) };\n }\n\n function nativeToConfigValue(value, fieldType, widget, field) {\n var normalizedFieldType = normalizeEnum(fieldType);\n var normalizedWidget = normalizeEnum(widget);\n\n if (normalizedFieldType === 'CONFIG_FIELD_TYPE_BOOL') {\n return { bool_value: !!value };\n }\n\n if (normalizedFieldType === 'CONFIG_FIELD_TYPE_INT64') {\n if (value === '' || value === null || value === undefined) {\n return null;\n }\n var intValue = parseInt(value, 10);\n if (Number.isNaN(intValue)) {\n throw new Error('Invalid int64 value for field ' + field.name);\n }\n return { int64_value: String(intValue) };\n }\n\n if (normalizedFieldType === 'CONFIG_FIELD_TYPE_DOUBLE') {\n if (value === '' || value === null || value === undefined) {\n return null;\n }\n var doubleValue = parseFloat(value);\n if (Number.isNaN(doubleValue)) {\n throw new Error('Invalid number for field ' + field.name);\n }\n return { double_value: doubleValue };\n }\n\n if (normalizedFieldType === 'CONFIG_FIELD_TYPE_DURATION') {\n if (value === '' || value === null || value === undefined) {\n return null;\n }\n var text = String(value).trim();\n if (!text) {\n return null;\n }\n if (/^\\d+$/.test(text)) {\n text += 's';\n }\n return { duration_value: text };\n }\n\n if (normalizedFieldType === 'CONFIG_FIELD_TYPE_LIST') {\n if (value === '' || value === null || value === undefined) {\n return null;\n }\n\n if (normalizedWidget === 'CONFIG_WIDGET_MULTI_SELECT' && Array.isArray(value)) {\n return { string_list: { values: value } };\n }\n\n if (typeof value === 'string') {\n try {\n var parsed = JSON.parse(value);\n if (!Array.isArray(parsed)) {\n throw new Error('Expected JSON array');\n }\n return inferConfigValue(parsed);\n } catch (e) {\n throw new Error('Invalid JSON list for field ' + field.name + ': ' + e.message);\n }\n }\n return inferConfigValue(value);\n }\n\n if (normalizedFieldType === 'CONFIG_FIELD_TYPE_OBJECT') {\n if (value === '' || value === null || value === undefined) {\n return null;\n }\n if (typeof value === 'string') {\n try {\n var parsedObject = JSON.parse(value);\n if (!parsedObject || typeof parsedObject !== 'object' || Array.isArray(parsedObject)) {\n throw new Error('Expected JSON object');\n }\n return inferConfigValue(parsedObject);\n } catch (e) {\n throw new Error('Invalid JSON object for field ' + field.name + ': ' + e.message);\n }\n }\n return inferConfigValue(value);\n }\n\n if (normalizedFieldType === 'CONFIG_FIELD_TYPE_ENUM') {\n if (normalizedWidget === 'CONFIG_WIDGET_MULTI_SELECT') {\n if (!value || !value.length) {\n return null;\n }\n return { string_list: { values: value } };\n }\n if (value === '' || value === null || value === undefined) {\n return null;\n }\n return { string_value: String(value) };\n }\n\n if (value === '' || value === null || value === undefined) {\n return null;\n }\n\n return { string_value: String(value) };\n }\n\n function toInputID(scope, fieldName) {\n return 'plugin-' + scope + '-field-' + String(fieldName || '').replace(/[^a-zA-Z0-9_-]/g, '_');\n }\n\n function resolveFieldConfigValue(scope, field) {\n if (!field || !field.name) {\n return null;\n }\n var fromConfig = null;\n if (state.config) {\n if (scope === 'admin' && state.config.admin_config_values) {\n fromConfig = state.config.admin_config_values[field.name];\n }\n if (scope === 'worker' && state.config.worker_config_values) {\n fromConfig = state.config.worker_config_values[field.name];\n }\n }\n if (fromConfig) {\n return fromConfig;\n }\n\n var formDefaults = null;\n if (scope === 'admin' && state.descriptor && state.descriptor.admin_config_form && state.descriptor.admin_config_form.default_values) {\n formDefaults = state.descriptor.admin_config_form.default_values[field.name];\n }\n if (scope === 'worker' && state.descriptor && state.descriptor.worker_config_form && state.descriptor.worker_config_form.default_values) {\n formDefaults = state.descriptor.worker_config_form.default_values[field.name];\n }\n if (formDefaults) {\n return formDefaults;\n }\n\n if (scope === 'worker' && state.descriptor && state.descriptor.worker_default_values) {\n return state.descriptor.worker_default_values[field.name] || null;\n }\n\n return null;\n }\n\n function renderFieldInput(scope, field, value) {\n var inputID = toInputID(scope, field.name);\n var normalizedType = normalizeEnum(field.field_type);\n var normalizedWidget = normalizeEnum(field.widget);\n var input;\n\n if (normalizedType === 'CONFIG_FIELD_TYPE_BOOL' || normalizedWidget === 'CONFIG_WIDGET_TOGGLE') {\n input = document.createElement('input');\n input.type = 'checkbox';\n input.className = 'form-check-input';\n input.id = inputID;\n input.checked = !!value;\n } else if ((normalizedType === 'CONFIG_FIELD_TYPE_ENUM' && normalizedWidget === 'CONFIG_WIDGET_MULTI_SELECT') || normalizedWidget === 'CONFIG_WIDGET_MULTI_SELECT') {\n input = document.createElement('select');\n input.className = 'form-select';\n input.id = inputID;\n input.multiple = true;\n var selected = Array.isArray(value) ? value : [];\n var options = Array.isArray(field.options) ? field.options : [];\n for (var i = 0; i < options.length; i++) {\n var option = document.createElement('option');\n option.value = options[i].value || '';\n option.textContent = options[i].label || options[i].value || '';\n if (selected.indexOf(option.value) >= 0) {\n option.selected = true;\n }\n input.appendChild(option);\n }\n } else if (normalizedType === 'CONFIG_FIELD_TYPE_ENUM' || normalizedWidget === 'CONFIG_WIDGET_SELECT') {\n input = document.createElement('select');\n input.className = 'form-select';\n input.id = inputID;\n var blankOption = document.createElement('option');\n blankOption.value = '';\n blankOption.textContent = '-- select --';\n input.appendChild(blankOption);\n var enumOptions = Array.isArray(field.options) ? field.options : [];\n for (var j = 0; j < enumOptions.length; j++) {\n var enumOption = document.createElement('option');\n enumOption.value = enumOptions[j].value || '';\n enumOption.textContent = enumOptions[j].label || enumOptions[j].value || '';\n if (String(value) === enumOption.value) {\n enumOption.selected = true;\n }\n input.appendChild(enumOption);\n }\n } else if (normalizedType === 'CONFIG_FIELD_TYPE_INT64' || normalizedType === 'CONFIG_FIELD_TYPE_DOUBLE' || normalizedWidget === 'CONFIG_WIDGET_NUMBER') {\n input = document.createElement('input');\n input.type = 'number';\n if (normalizedType === 'CONFIG_FIELD_TYPE_DOUBLE') {\n input.step = 'any';\n }\n input.className = 'form-control';\n input.id = inputID;\n if (value !== null && value !== undefined && value !== '') {\n input.value = String(value);\n }\n } else if (normalizedType === 'CONFIG_FIELD_TYPE_LIST' || normalizedType === 'CONFIG_FIELD_TYPE_OBJECT' || normalizedWidget === 'CONFIG_WIDGET_TEXTAREA') {\n input = document.createElement('textarea');\n input.className = 'form-control';\n input.id = inputID;\n input.rows = 4;\n if (normalizedType === 'CONFIG_FIELD_TYPE_LIST' || normalizedType === 'CONFIG_FIELD_TYPE_OBJECT') {\n input.placeholder = normalizedType === 'CONFIG_FIELD_TYPE_LIST' ? '[...]' : '{...}';\n }\n if (value !== null && value !== undefined && value !== '') {\n if (typeof value === 'object') {\n input.value = JSON.stringify(value, null, 2);\n } else {\n input.value = String(value);\n }\n }\n } else {\n input = document.createElement('input');\n input.className = 'form-control';\n input.id = inputID;\n if (normalizedWidget === 'CONFIG_WIDGET_PASSWORD' || field.sensitive) {\n input.type = 'password';\n } else {\n input.type = 'text';\n }\n if (value !== null && value !== undefined && value !== '') {\n input.value = String(value);\n }\n }\n\n if (field.placeholder && input.tagName === 'INPUT') {\n input.placeholder = field.placeholder;\n }\n if (field.placeholder && input.tagName === 'TEXTAREA') {\n input.placeholder = field.placeholder;\n }\n\n if (field.required) {\n input.required = true;\n }\n if (field.read_only) {\n input.disabled = true;\n }\n\n return input;\n }\n\n function renderConfigForm(scope, rootID, form) {\n var root = document.getElementById(rootID);\n if (!root) {\n return;\n }\n\n root.innerHTML = '';\n if (!form || !Array.isArray(form.sections) || form.sections.length === 0) {\n root.innerHTML = '<div class=\"text-muted\">No fields provided by descriptor.</div>';\n return;\n }\n\n for (var s = 0; s < form.sections.length; s++) {\n var section = form.sections[s] || {};\n var sectionCard = document.createElement('div');\n sectionCard.className = 'card mb-3';\n\n var sectionHeader = document.createElement('div');\n sectionHeader.className = 'card-header py-2';\n sectionHeader.innerHTML = '<strong>' + escapeHtml(textOrDash(section.title || section.section_id)) + '</strong>';\n sectionCard.appendChild(sectionHeader);\n\n var sectionBody = document.createElement('div');\n sectionBody.className = 'card-body';\n\n if (section.description) {\n var sectionDescription = document.createElement('p');\n sectionDescription.className = 'text-muted small';\n sectionDescription.textContent = section.description;\n sectionBody.appendChild(sectionDescription);\n }\n\n var fields = Array.isArray(section.fields) ? section.fields : [];\n if (!fields.length) {\n var noFields = document.createElement('div');\n noFields.className = 'text-muted';\n noFields.textContent = 'No fields in this section.';\n sectionBody.appendChild(noFields);\n }\n\n for (var f = 0; f < fields.length; f++) {\n var field = fields[f] || {};\n if (!field.name) {\n continue;\n }\n\n var configValue = resolveFieldConfigValue(scope, field);\n var nativeValue = configValueToNative(configValue);\n\n var wrapper = document.createElement('div');\n wrapper.className = 'mb-3 plugin-field';\n wrapper.dataset.scope = scope;\n wrapper.dataset.fieldName = field.name;\n wrapper.dataset.fieldType = normalizeEnum(field.field_type);\n wrapper.dataset.widget = normalizeEnum(field.widget);\n wrapper.dataset.visibleWhenField = field.visible_when_field || '';\n wrapper.dataset.visibleWhenEquals = JSON.stringify(field.visible_when_equals || null);\n\n var label = document.createElement('label');\n label.className = 'form-label';\n label.setAttribute('for', toInputID(scope, field.name));\n label.innerHTML = escapeHtml(field.label || field.name) + (field.required ? ' <span class=\"text-danger\">*</span>' : '');\n wrapper.appendChild(label);\n\n var input = renderFieldInput(scope, field, nativeValue);\n wrapper.appendChild(input);\n\n if (field.description || field.help_text) {\n var help = document.createElement('div');\n help.className = 'form-text';\n help.textContent = field.help_text || field.description || '';\n wrapper.appendChild(help);\n }\n\n sectionBody.appendChild(wrapper);\n }\n\n sectionCard.appendChild(sectionBody);\n root.appendChild(sectionCard);\n }\n\n root.addEventListener('change', function() {\n applyVisibility(scope);\n });\n root.addEventListener('input', function() {\n applyVisibility(scope);\n });\n\n applyVisibility(scope);\n }\n\n function readRawFieldValue(scope, fieldName) {\n var input = document.getElementById(toInputID(scope, fieldName));\n if (!input) {\n return null;\n }\n if (input.tagName === 'SELECT' && input.multiple) {\n return getSelectValueArray(input);\n }\n if (input.type === 'checkbox') {\n return !!input.checked;\n }\n return input.value;\n }\n\n function compareVisibleValue(currentValue, expectedValueObject) {\n if (!expectedValueObject) {\n return true;\n }\n var expectedNative = configValueToNative(expectedValueObject);\n if (Array.isArray(expectedNative)) {\n if (!Array.isArray(currentValue)) {\n return false;\n }\n if (expectedNative.length !== currentValue.length) {\n return false;\n }\n for (var i = 0; i < expectedNative.length; i++) {\n if (String(expectedNative[i]) !== String(currentValue[i])) {\n return false;\n }\n }\n return true;\n }\n return String(currentValue) === String(expectedNative);\n }\n\n function applyVisibility(scope) {\n var fields = document.querySelectorAll('.plugin-field[data-scope=\"' + scope + '\"]');\n for (var i = 0; i < fields.length; i++) {\n var wrapper = fields[i];\n var dependsOn = wrapper.dataset.visibleWhenField;\n if (!dependsOn) {\n wrapper.classList.remove('plugin-field-hidden');\n continue;\n }\n\n var expectedRaw = null;\n try {\n expectedRaw = JSON.parse(wrapper.dataset.visibleWhenEquals || 'null');\n } catch (e) {\n expectedRaw = null;\n }\n var current = readRawFieldValue(scope, dependsOn);\n var visible = compareVisibleValue(current, expectedRaw);\n if (visible) {\n wrapper.classList.remove('plugin-field-hidden');\n } else {\n wrapper.classList.add('plugin-field-hidden');\n }\n }\n }\n\n function collectConfigValues(scope, form) {\n var out = {};\n if (!form || !Array.isArray(form.sections)) {\n return out;\n }\n\n for (var s = 0; s < form.sections.length; s++) {\n var section = form.sections[s] || {};\n var fields = Array.isArray(section.fields) ? section.fields : [];\n for (var f = 0; f < fields.length; f++) {\n var field = fields[f] || {};\n if (!field.name) {\n continue;\n }\n\n var wrapper = document.querySelector('.plugin-field[data-scope=\"' + scope + '\"][data-field-name=\"' + field.name + '\"]');\n if (wrapper && wrapper.classList.contains('plugin-field-hidden')) {\n continue;\n }\n\n var input = document.getElementById(toInputID(scope, field.name));\n if (!input) {\n continue;\n }\n\n var rawValue;\n if (input.tagName === 'SELECT' && input.multiple) {\n rawValue = getSelectValueArray(input);\n } else if (input.type === 'checkbox') {\n rawValue = !!input.checked;\n } else {\n rawValue = input.value;\n }\n\n if (field.required) {\n var missing = false;\n if (input.type === 'checkbox') {\n missing = false;\n } else if (Array.isArray(rawValue)) {\n missing = rawValue.length === 0;\n } else {\n missing = String(rawValue || '').trim() === '';\n }\n if (missing) {\n throw new Error('Missing required field: ' + (field.label || field.name));\n }\n }\n\n var converted = nativeToConfigValue(rawValue, field.field_type, field.widget, field);\n if (converted !== null) {\n out[field.name] = converted;\n }\n }\n }\n\n return out;\n }\n\n function renderDescriptorSummary() {\n var summary = document.getElementById('plugin-descriptor-summary');\n if (!state.descriptor) {\n summary.innerHTML = 'No descriptor loaded.';\n return;\n }\n\n var descriptor = state.descriptor;\n var html = '';\n html += '<div><strong>' + escapeHtml(textOrDash(descriptor.display_name || descriptor.job_type)) + '</strong></div>';\n html += '<div><small>Job Type: ' + escapeHtml(textOrDash(descriptor.job_type)) + '</small></div>';\n html += '<div><small>Descriptor Version: ' + escapeHtml(textOrDash(descriptor.descriptor_version)) + '</small></div>';\n if (descriptor.description) {\n html += '<div class=\"mt-1\">' + escapeHtml(descriptor.description) + '</div>';\n }\n summary.innerHTML = html;\n }\n\n function fillAdminSettings() {\n var runtimeConfig = (state.config && state.config.admin_runtime) ? state.config.admin_runtime : {};\n var defaults = (state.descriptor && state.descriptor.admin_runtime_defaults) ? state.descriptor.admin_runtime_defaults : {};\n\n function pickNumber(key) {\n if (runtimeConfig && runtimeConfig[key] !== undefined && runtimeConfig[key] !== null) {\n return runtimeConfig[key];\n }\n if (defaults && defaults[key] !== undefined && defaults[key] !== null) {\n return defaults[key];\n }\n return 0;\n }\n\n function pickBool(key) {\n if (runtimeConfig && runtimeConfig[key] !== undefined && runtimeConfig[key] !== null) {\n return !!runtimeConfig[key];\n }\n if (defaults && defaults[key] !== undefined && defaults[key] !== null) {\n return !!defaults[key];\n }\n return true;\n }\n\n document.getElementById('plugin-admin-enabled').checked = pickBool('enabled');\n document.getElementById('plugin-admin-detection-interval').value = String(pickNumber('detection_interval_seconds'));\n document.getElementById('plugin-admin-detection-timeout').value = String(pickNumber('detection_timeout_seconds'));\n document.getElementById('plugin-admin-max-results').value = String(pickNumber('max_jobs_per_detection'));\n document.getElementById('plugin-admin-global-exec').value = String(pickNumber('global_execution_concurrency'));\n document.getElementById('plugin-admin-per-worker-exec').value = String(pickNumber('per_worker_execution_concurrency'));\n document.getElementById('plugin-admin-retry-limit').value = String(pickNumber('retry_limit'));\n document.getElementById('plugin-admin-retry-backoff').value = String(pickNumber('retry_backoff_seconds'));\n }\n\n function collectAdminSettings() {\n function getInt(id) {\n var raw = String(document.getElementById(id).value || '').trim();\n if (!raw) {\n return 0;\n }\n var parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n throw new Error('Invalid number for ' + id);\n }\n return parsed;\n }\n\n return {\n enabled: !!document.getElementById('plugin-admin-enabled').checked,\n detection_interval_seconds: getInt('plugin-admin-detection-interval'),\n detection_timeout_seconds: getInt('plugin-admin-detection-timeout'),\n max_jobs_per_detection: getInt('plugin-admin-max-results'),\n global_execution_concurrency: getInt('plugin-admin-global-exec'),\n per_worker_execution_concurrency: getInt('plugin-admin-per-worker-exec'),\n retry_limit: getInt('plugin-admin-retry-limit'),\n retry_backoff_seconds: getInt('plugin-admin-retry-backoff'),\n };\n }\n\n function renderRunHistory(history) {\n var successBody = document.getElementById('plugin-runs-success-body');\n var errorBody = document.getElementById('plugin-runs-error-body');\n\n var successRuns = (history && Array.isArray(history.successful_runs)) ? history.successful_runs : [];\n var errorRuns = (history && Array.isArray(history.error_runs)) ? history.error_runs : [];\n\n if (!successRuns.length) {\n successBody.innerHTML = '<tr><td colspan=\"4\" class=\"text-muted\">No successful runs</td></tr>';\n } else {\n var successRows = '';\n for (var i = 0; i < successRuns.length; i++) {\n var run = successRuns[i] || {};\n successRows += '<tr>' +\n '<td><small>' + escapeHtml(parseTime(run.completed_at) || '-') + '</small></td>' +\n '<td>' + renderJobLink(run.job_id) + '</td>' +\n '<td><small>' + escapeHtml(textOrDash(run.worker_id)) + '</small></td>' +\n '<td><small>' + escapeHtml(formatDurationMs(run.duration_ms)) + '</small></td>' +\n '</tr>';\n }\n successBody.innerHTML = successRows;\n }\n\n if (!errorRuns.length) {\n errorBody.innerHTML = '<tr><td colspan=\"4\" class=\"text-muted\">No error runs</td></tr>';\n } else {\n var errorRows = '';\n for (var j = 0; j < errorRuns.length; j++) {\n var errorRun = errorRuns[j] || {};\n errorRows += '<tr>' +\n '<td><small>' + escapeHtml(parseTime(errorRun.completed_at) || '-') + '</small></td>' +\n '<td>' + renderJobLink(errorRun.job_id) + '</td>' +\n '<td><small>' + escapeHtml(textOrDash(errorRun.worker_id)) + '</small></td>' +\n '<td><small class=\"text-danger\">' + escapeHtml(textOrDash(errorRun.message)) + '</small></td>' +\n '</tr>';\n }\n errorBody.innerHTML = errorRows;\n }\n }\n\n function renderDetectionResult(data) {\n var root = document.getElementById('plugin-detection-results');\n if (!root) {\n return;\n }\n\n function normalizeProposals(payload) {\n if (!payload || typeof payload !== 'object') {\n return [];\n }\n if (Array.isArray(payload.proposals)) {\n return payload.proposals;\n }\n if (payload.proposals && typeof payload.proposals === 'object') {\n return Object.keys(payload.proposals).map(function(key) {\n return payload.proposals[key];\n }).filter(function(item) {\n return !!item;\n });\n }\n return [];\n }\n\n function proposalID(proposal) {\n return proposal.proposal_id || proposal.proposalId || proposal.id || '';\n }\n\n function proposalSummary(proposal) {\n return proposal.summary || proposal.title || proposalID(proposal) || 'Proposal';\n }\n\n function proposalDetail(proposal) {\n return proposal.detail || proposal.reason || proposal.message || '-';\n }\n\n function normalizeActivities(payload) {\n if (!payload || typeof payload !== 'object') {\n return [];\n }\n if (Array.isArray(payload.activities)) {\n return payload.activities;\n }\n return [];\n }\n\n function encodeDetails(details) {\n if (!details || typeof details !== 'object') {\n return '';\n }\n var text = '';\n try {\n text = JSON.stringify(details);\n } catch (e) {\n text = '';\n }\n if (!text) {\n return '';\n }\n if (text.length > 360) {\n text = text.slice(0, 360) + '...';\n }\n return text;\n }\n\n if (!data) {\n root.innerHTML = '<div class=\"text-muted\">No results.</div>';\n return;\n }\n\n var proposals = normalizeProposals(data);\n var activities = normalizeActivities(data);\n var responseCount = Number(data.count || 0);\n if (!Number.isFinite(responseCount) || responseCount < 0) {\n responseCount = proposals.length;\n }\n var requestID = data.request_id || data.requestId || '';\n var detectorWorkerID = data.detector_worker_id || data.detectorWorkerId || '';\n var totalProposals = Number(data.total_proposals || 0);\n if (!Number.isFinite(totalProposals) || totalProposals < 0) {\n totalProposals = responseCount;\n }\n\n var html = '';\n html += '<div class=\"mb-2\"><strong>Detection Run</strong></div>';\n if (requestID) {\n html += '<div><small>request_id: ' + escapeHtml(String(requestID)) + '</small></div>';\n }\n if (detectorWorkerID) {\n html += '<div><small>detector: ' + escapeHtml(String(detectorWorkerID)) + '</small></div>';\n }\n html += '<div><small>proposals: ' + escapeHtml(String(responseCount)) + ' / total: ' + escapeHtml(String(totalProposals)) + '</small></div>';\n if (data.error) {\n html += '<div class=\"mt-1 text-danger\"><small>' + escapeHtml(String(data.error)) + '</small></div>';\n }\n\n if (!proposals.length) {\n html += '<div class=\"mt-2 text-muted\">No proposals returned.</div>';\n } else {\n html += '<div class=\"mt-2 mb-1\"><strong>' + escapeHtml(String(proposals.length)) + '</strong> proposal(s)</div>';\n for (var i = 0; i < proposals.length; i++) {\n var proposal = proposals[i] || {};\n var pID = proposalID(proposal);\n var pSummary = proposalSummary(proposal);\n var pDetail = proposalDetail(proposal);\n var pPriority = proposal.priority;\n if (pPriority === null || pPriority === undefined || pPriority === '') {\n pPriority = '-';\n }\n html += '<div class=\"plugin-detection-item\">' +\n '<div class=\"fw-semibold\">' + escapeHtml(textOrDash(pSummary)) + '</div>' +\n '<div><small>proposal_id: ' + escapeHtml(textOrDash(pID)) + '</small></div>' +\n '<div><small>priority: ' + escapeHtml(textOrDash(pPriority)) + '</small></div>' +\n '<div><small>detail: ' + escapeHtml(textOrDash(pDetail)) + '</small></div>' +\n '</div>';\n }\n }\n\n if (activities.length) {\n html += '<hr class=\"my-2\"/>';\n html += '<div class=\"mb-2\"><strong>Detection Process</strong></div>';\n for (var j = 0; j < activities.length; j++) {\n var activity = activities[j] || {};\n var activityStage = activity.stage || '-';\n var activitySource = activity.source || '-';\n var activityMessage = activity.message || '-';\n var activityTime = parseTime(activity.occurred_at) || '-';\n var detailsText = encodeDetails(activity.details);\n html += '<div class=\"plugin-detection-item\">' +\n '<div class=\"fw-semibold\">' + escapeHtml(textOrDash(activityStage)) + '</div>' +\n '<div><small>source: ' + escapeHtml(textOrDash(activitySource)) + ' | at: ' + escapeHtml(activityTime) + '</small></div>' +\n '<div><small>' + escapeHtml(textOrDash(activityMessage)) + '</small></div>';\n if (detailsText) {\n html += '<div><small class=\"text-muted\">details: ' + escapeHtml(detailsText) + '</small></div>';\n }\n html += '</div>';\n }\n }\n\n root.innerHTML = html;\n }\n\n function getSelectedJobType() {\n return String(state.selectedJobType || '').trim();\n }\n\n function resetJobTypePanels() {\n state.loadedJobType = '';\n state.descriptor = null;\n state.config = null;\n document.getElementById('plugin-descriptor-summary').textContent = 'No descriptor loaded.';\n document.getElementById('plugin-admin-config-form').innerHTML = '<div class=\"text-muted\">No admin form loaded.</div>';\n document.getElementById('plugin-worker-config-form').innerHTML = '<div class=\"text-muted\">No worker form loaded.</div>';\n document.getElementById('plugin-config-updated-at').textContent = 'Not loaded';\n renderRunHistory(null);\n renderDetectionResult(null);\n }\n\n async function loadDescriptorAndConfig(jobType, forceRefresh) {\n if (!jobType) {\n return;\n }\n\n state.selectedJobType = jobType;\n renderNavigationState();\n\n var summary = document.getElementById('plugin-descriptor-summary');\n summary.textContent = 'Loading descriptor and config...';\n\n try {\n if (forceRefresh) {\n var refreshURL = '/api/plugin/job-types/' + encodePath(jobType) + '/schema?force_refresh=true';\n state.descriptor = await pluginRequest('POST', refreshURL, null);\n } else {\n try {\n state.descriptor = await pluginRequest('GET', '/api/plugin/job-types/' + encodePath(jobType) + '/descriptor');\n } catch (descriptorLoadError) {\n state.descriptor = await pluginRequest('POST', '/api/plugin/job-types/' + encodePath(jobType) + '/schema', null);\n }\n }\n } catch (schemaError) {\n state.loadedJobType = '';\n state.descriptor = null;\n summary.innerHTML = '<span class=\"text-danger\">Failed to load descriptor: ' + escapeHtml(schemaError.message) + '</span>';\n document.getElementById('plugin-admin-config-form').innerHTML = '<div class=\"text-muted\">No admin form loaded.</div>';\n document.getElementById('plugin-worker-config-form').innerHTML = '<div class=\"text-muted\">No worker form loaded.</div>';\n throw schemaError;\n }\n\n try {\n state.config = await pluginRequest('GET', '/api/plugin/job-types/' + encodePath(jobType) + '/config');\n } catch (configError) {\n state.config = null;\n notify('Failed to load config: ' + configError.message, 'error');\n }\n\n renderDescriptorSummary();\n fillAdminSettings();\n\n if (state.descriptor) {\n renderConfigForm('admin', 'plugin-admin-config-form', state.descriptor.admin_config_form || {});\n renderConfigForm('worker', 'plugin-worker-config-form', state.descriptor.worker_config_form || {});\n }\n\n if (state.config && state.config.updated_at) {\n document.getElementById('plugin-config-updated-at').textContent = 'Updated: ' + (parseTime(state.config.updated_at) || state.config.updated_at);\n } else {\n document.getElementById('plugin-config-updated-at').textContent = 'Not saved yet';\n }\n state.loadedJobType = jobType;\n\n await loadRunHistory(jobType);\n\n if (state.lastDetectionByJobType && state.lastDetectionByJobType[jobType]) {\n renderDetectionResult(state.lastDetectionByJobType[jobType]);\n } else {\n renderDetectionResult(null);\n }\n }\n\n async function loadRunHistory(jobType) {\n if (!jobType) {\n renderRunHistory(null);\n return;\n }\n try {\n var history = await pluginRequest('GET', '/api/plugin/job-types/' + encodePath(jobType) + '/runs');\n renderRunHistory(history);\n } catch (e) {\n renderRunHistory(null);\n notify('Failed to load run history: ' + e.message, 'error');\n }\n }\n\n async function saveConfig() {\n var jobType = state.selectedJobType || getSelectedJobType();\n if (!jobType) {\n notify('Select a job type first.', 'error');\n return;\n }\n if (!state.descriptor) {\n notify('Load descriptor before saving config.', 'error');\n return;\n }\n\n var payload;\n try {\n payload = {\n job_type: jobType,\n descriptor_version: state.descriptor.descriptor_version || 0,\n admin_runtime: collectAdminSettings(),\n admin_config_values: collectConfigValues('admin', state.descriptor.admin_config_form || {}),\n worker_config_values: collectConfigValues('worker', state.descriptor.worker_config_form || {}),\n };\n } catch (err) {\n notify(err.message, 'error');\n return;\n }\n\n try {\n state.config = await pluginRequest('PUT', '/api/plugin/job-types/' + encodePath(jobType) + '/config', payload);\n document.getElementById('plugin-config-updated-at').textContent = 'Updated: ' + (parseTime(state.config.updated_at) || state.config.updated_at || 'just now');\n notify('Config saved for ' + jobType, 'success');\n } catch (e) {\n notify('Failed to save config: ' + e.message, 'error');\n }\n }\n\n function getMaxResults() {\n var raw = String(document.getElementById('plugin-admin-max-results').value || '').trim();\n if (!raw) {\n return 0;\n }\n var parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed) || parsed < 0) {\n return 0;\n }\n return parsed;\n }\n\n async function runDetection() {\n var jobType = state.selectedJobType || getSelectedJobType();\n if (!jobType) {\n notify('Select a job type first.', 'error');\n return;\n }\n\n try {\n var data = await pluginRequest('POST', '/api/plugin/job-types/' + encodePath(jobType) + '/detect', {\n max_results: getMaxResults(),\n });\n state.lastDetectionByJobType[jobType] = data;\n renderDetectionResult(data);\n } catch (e) {\n if (e && e.responseData) {\n state.lastDetectionByJobType[jobType] = e.responseData;\n renderDetectionResult(e.responseData);\n }\n notify('Detection failed: ' + e.message, 'error');\n return;\n }\n\n try {\n await refreshJobsAndActivities();\n } catch (refreshErr) {\n notify('Detection succeeded, but monitoring refresh failed: ' + refreshErr.message, 'error');\n }\n }\n\n async function runWorkflow() {\n var jobType = state.selectedJobType || getSelectedJobType();\n if (!jobType) {\n notify('Select a job type first.', 'error');\n return;\n }\n\n try {\n var data = await pluginRequest('POST', '/api/plugin/job-types/' + encodePath(jobType) + '/run', {\n max_results: getMaxResults(),\n });\n var resultRoot = document.getElementById('plugin-detection-results');\n resultRoot.innerHTML = '<div class=\"mb-2\"><strong>Workflow completed</strong></div>' +\n '<div>Detected: <strong>' + escapeHtml(String(data.detected_count || 0)) + '</strong></div>' +\n '<div>Ready to Execute: <strong>' + escapeHtml(String(data.ready_to_execute_count || 0)) + '</strong></div>' +\n '<div>Skipped (Already Active): <strong>' + escapeHtml(String(data.skipped_active_count || 0)) + '</strong></div>' +\n '<div>Executed: <strong>' + escapeHtml(String(data.executed_count || 0)) + '</strong></div>' +\n '<div class=\"text-success\">Success: ' + escapeHtml(String(data.success_count || 0)) + '</div>' +\n '<div class=\"text-danger\">Errors: ' + escapeHtml(String(data.error_count || 0)) + '</div>';\n await loadRunHistory(jobType);\n await refreshJobsAndActivities();\n } catch (e) {\n notify('Workflow run failed: ' + e.message, 'error');\n }\n }\n\n async function refreshJobsAndActivities() {\n var executionStateFilter = document.getElementById('plugin-monitor-job-state-filter');\n\n state.monitorExecutionJobState = executionStateFilter ? String(executionStateFilter.value || '').trim() : '';\n\n var allJobsPromise = pluginRequest('GET', '/api/plugin/jobs?limit=500');\n var allActivitiesPromise = pluginRequest('GET', '/api/plugin/activities?limit=500');\n var schedulerPromise = pluginRequest('GET', '/api/plugin/scheduler-states');\n\n var allJobs = await allJobsPromise;\n var allActivities = await allActivitiesPromise;\n var schedulerStates = await schedulerPromise;\n\n state.jobs = Array.isArray(allJobs) ? allJobs : [];\n state.activities = Array.isArray(allActivities) ? allActivities : [];\n state.allJobs = state.jobs;\n state.allActivities = state.activities;\n state.schedulerStates = Array.isArray(schedulerStates) ? schedulerStates : [];\n renderQueueJobs();\n renderDetectionJobs();\n renderExecutionJobs();\n renderExecutionActivities();\n renderSchedulerStates();\n renderStatus();\n renderJobTypeSummary();\n }\n\n async function refreshAll() {\n try {\n var previousSelectedJobType = state.selectedJobType;\n\n var workers = await pluginRequest('GET', '/api/plugin/workers');\n state.workers = Array.isArray(workers) ? workers : [];\n renderWorkers();\n\n var jobTypes = await pluginRequest('GET', '/api/plugin/job-types');\n state.jobTypes = Array.isArray(jobTypes) ? jobTypes : [];\n applyInitialNavigation();\n ensureActiveNavigation();\n renderTopTabs();\n renderNavigationState();\n\n if (!state.selectedJobType) {\n resetJobTypePanels();\n } else if (state.loadedJobType !== state.selectedJobType || previousSelectedJobType !== state.selectedJobType) {\n try {\n await loadDescriptorAndConfig(state.selectedJobType, false);\n } catch (loadError) {\n // Already rendered in UI.\n }\n }\n\n await refreshJobsAndActivities();\n } catch (e) {\n notify('Refresh failed: ' + e.message, 'error');\n }\n }\n\n function bindActions() {\n var monitorStateFilter = document.getElementById('plugin-monitor-job-state-filter');\n if (monitorStateFilter) {\n monitorStateFilter.addEventListener('change', function() {\n state.monitorExecutionJobState = String(monitorStateFilter.value || '').trim();\n renderExecutionJobs();\n });\n }\n\n document.getElementById('plugin-refresh-all-btn').addEventListener('click', function() {\n refreshAll();\n });\n\n document.getElementById('plugin-refresh-schema-btn').addEventListener('click', async function() {\n var jobType = getSelectedJobType();\n if (!jobType) {\n notify('Select a job type first.', 'error');\n return;\n }\n try {\n await loadDescriptorAndConfig(jobType, true);\n notify('Schema refreshed for ' + jobType, 'success');\n } catch (e) {\n notify('Schema refresh failed: ' + e.message, 'error');\n }\n });\n\n document.getElementById('plugin-save-config-btn').addEventListener('click', function() {\n saveConfig();\n });\n\n document.getElementById('plugin-trigger-detection-btn').addEventListener('click', function() {\n runDetection();\n });\n\n document.getElementById('plugin-run-workflow-btn').addEventListener('click', function() {\n runWorkflow();\n });\n\n page.addEventListener('click', async function(event) {\n var target = event.target;\n if (!target) {\n return;\n }\n\n var topTab = target.closest('[data-plugin-top-tab]');\n if (topTab) {\n event.preventDefault();\n var topTabKey = String(topTab.getAttribute('data-plugin-top-tab') || '').trim();\n if (!topTabKey) {\n return;\n }\n if (topTabKey === 'overview') {\n state.activeTopTab = 'overview';\n renderNavigationState();\n return;\n }\n\n var jobType = parseJobTypeFromTopTab(topTabKey);\n if (!jobType) {\n return;\n }\n state.activeTopTab = topTabKey;\n state.selectedJobType = jobType;\n ensureActiveNavigation();\n renderNavigationState();\n if (state.loadedJobType !== jobType) {\n try {\n await loadDescriptorAndConfig(jobType, false);\n } catch (e) {\n // Already rendered in UI.\n }\n } else {\n if (state.lastDetectionByJobType && state.lastDetectionByJobType[jobType]) {\n renderDetectionResult(state.lastDetectionByJobType[jobType]);\n } else {\n renderDetectionResult(null);\n }\n await loadRunHistory(jobType);\n }\n renderQueueJobs();\n renderDetectionJobs();\n renderExecutionJobs();\n renderExecutionActivities();\n return;\n }\n\n var subTab = target.closest('[data-plugin-subtab]');\n if (subTab) {\n event.preventDefault();\n var subTabKey = normalizeSubTab(subTab.getAttribute('data-plugin-subtab'));\n state.activeSubTab = subTabKey;\n renderNavigationState();\n return;\n }\n\n var trigger = target.closest('.plugin-job-link');\n if (!trigger) {\n return;\n }\n event.preventDefault();\n var jobID = trigger.getAttribute('data-job-id');\n if (!jobID) {\n return;\n }\n openJobDetail(jobID);\n });\n }\n\n async function init() {\n bindActions();\n renderTopTabs();\n ensureActiveNavigation();\n renderNavigationState();\n await refreshAll();\n\n state.refreshTimer = setInterval(function() {\n refreshAll();\n }, 3000);\n }\n\n window.addEventListener('beforeunload', function() {\n if (state.refreshTimer) {\n clearInterval(state.refreshTimer);\n state.refreshTimer = null;\n }\n });\n\n init();\n })();\n </script>")
|
|
if templ_7745c5c3_Err != nil {
|
|
return templ_7745c5c3_Err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
var _ = templruntime.GeneratedTemplate
|