mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-14 15:58:52 +01:00
admin: expose per-job-type detection interval in plugin UI (#8552)
* admin: expose per-job-type detection interval in plugin UI The detection_interval_seconds field was not editable in the admin UI. collectAdminSettings() silently preserved the existing value, making it impossible for users to change how often a job type checks for new work. Users would change the global "Sleep Between Iterations" setting expecting it to control job scheduling frequency, but that only controls the scheduler loop's idle polling rate. Add a "Detection Interval (s)" input to the per-job-type admin settings form so users can actually configure it. Fixes #8549 * admin: remove global Sleep Between Iterations setting Now that per-job-type detection intervals are exposed in the UI, the global IdleSleepSeconds setting is redundant and confusing. It only controlled the scheduler loop's idle polling rate, which is always overridden by earliestNextDetectionAt() when job types exist. Replace the three usages with simpler alternatives: - Scheduler loop sleep: use defaultSchedulerIdleSleep constant - Initial delay for new job types: use policy.DetectionInterval/2 (more logical since it's already per-job-type) - Status fallback: use the constant The API endpoints are kept for backward compatibility but the UI no longer exposes or calls them. * admin: restore configurable idle sleep in scheduler loop The EC integration test sets idle_sleep_seconds=1 via the scheduler config API so the scheduler wakes quickly after workers connect. The previous commit replaced this with a hardcoded 613s constant, causing the scheduler to sleep through the entire test window. Restore GetSchedulerConfig().IdleSleepDuration() in the scheduler loop and status reporting. The UI removal of the setting is still correct — the API endpoint remains for programmatic use (e.g., tests). * admin: cap first-run initial delay to 5s instead of DetectionInterval/2 The initial delay for first-run job types was set to policy.DetectionInterval/2, which creates unbounded first-run latency (e.g., 1 hour for vacuum with a 2-hour detection interval). A small fixed 5-second delay provides sufficient stagger without penalizing startup time.
This commit is contained in:
1 parent
e25558e4d8
commit
961c270aba
3 files changed
+12
-109
No files matched your search
@@ -112,7 +112,6 @@ func (r *Plugin) runSchedulerIteration() bool {
|
||||
}
|
||||
|
||||
active := make(map[string]struct{}, len(jobTypes))
|
||||
schedulerIdleSleep := r.GetSchedulerConfig().IdleSleepDuration()
|
||||
hadJobs := false
|
||||
|
||||
for _, jobType := range jobTypes {
|
||||
@@ -129,7 +128,7 @@ func (r *Plugin) runSchedulerIteration() bool {
|
||||
}
|
||||
initialDelay := time.Duration(0)
|
||||
if runInfo := r.snapshotSchedulerRun(jobType); runInfo.lastRunStartedAt.IsZero() {
|
||||
initialDelay = schedulerIdleSleep / 2
|
||||
initialDelay = 5 * time.Second
|
||||
}
|
||||
if !r.markDetectionDue(jobType, policy.DetectionInterval, initialDelay) {
|
||||
continue
|
||||
|
||||
@@ -156,14 +156,7 @@ templ Plugin(page string) {
|
||||
<small class="text-muted">Global</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label" for="plugin-scheduler-idle-sleep-overview">Sleep Between Iterations (s)</label>
|
||||
<input type="number" class="form-control" id="plugin-scheduler-idle-sleep-overview" min="0"/>
|
||||
<div class="form-text">Used when no jobs are detected.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary" id="plugin-save-scheduler-btn-overview">
|
||||
<i class="fas fa-save me-1"></i>Save Scheduler Settings
|
||||
</button>
|
||||
<p class="text-muted mb-0">Detection intervals are configured per job type in the job type settings below.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,6 +267,11 @@ templ Plugin(page string) {
|
||||
<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 class="form-text">How often to check for new work.</div>
|
||||
</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"/>
|
||||
@@ -311,14 +309,7 @@ templ Plugin(page string) {
|
||||
<h5 class="mb-0"><i class="fas fa-clock me-2"></i>Scheduler Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="plugin-scheduler-idle-sleep">Sleep Between Iterations (s)</label>
|
||||
<input type="number" class="form-control" id="plugin-scheduler-idle-sleep" min="0"/>
|
||||
<div class="form-text">Used when no jobs are detected.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary" id="plugin-save-scheduler-btn">
|
||||
<i class="fas fa-save me-1"></i>Save Scheduler Settings
|
||||
</button>
|
||||
<p class="text-muted mb-0">Detection intervals are configured per job type in the settings above.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -632,8 +623,6 @@ templ Plugin(page string) {
|
||||
activities: [],
|
||||
schedulerStates: [],
|
||||
schedulerStatus: null,
|
||||
schedulerConfig: null,
|
||||
schedulerConfigLoaded: false,
|
||||
allJobs: [],
|
||||
allActivities: [],
|
||||
loadedJobType: '',
|
||||
@@ -2461,6 +2450,7 @@ templ Plugin(page string) {
|
||||
}
|
||||
|
||||
document.getElementById('plugin-admin-enabled').checked = pickBool('enabled');
|
||||
document.getElementById('plugin-admin-detection-interval').value = String(pickNumber('detection_interval_seconds'));
|
||||
document.getElementById('plugin-admin-detection-timeout').value = String(pickNumber('detection_timeout_seconds'));
|
||||
document.getElementById('plugin-admin-max-runtime').value = String(pickNumber('job_type_max_runtime_seconds'));
|
||||
document.getElementById('plugin-admin-max-results').value = String(pickNumber('max_jobs_per_detection'));
|
||||
@@ -2471,9 +2461,6 @@ templ Plugin(page string) {
|
||||
}
|
||||
|
||||
function collectAdminSettings() {
|
||||
var existingRuntime = (state.config && state.config.admin_runtime) ? state.config.admin_runtime : {};
|
||||
var existingDetectionInterval = Number(existingRuntime.detection_interval_seconds || 0);
|
||||
|
||||
function getInt(id) {
|
||||
var raw = String(document.getElementById(id).value || '').trim();
|
||||
if (!raw) {
|
||||
@@ -2488,7 +2475,7 @@ templ Plugin(page string) {
|
||||
|
||||
return {
|
||||
enabled: !!document.getElementById('plugin-admin-enabled').checked,
|
||||
detection_interval_seconds: existingDetectionInterval,
|
||||
detection_interval_seconds: getInt('plugin-admin-detection-interval'),
|
||||
detection_timeout_seconds: getInt('plugin-admin-detection-timeout'),
|
||||
job_type_max_runtime_seconds: getInt('plugin-admin-max-runtime'),
|
||||
max_jobs_per_detection: getInt('plugin-admin-max-results'),
|
||||
@@ -2806,75 +2793,6 @@ templ Plugin(page string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSchedulerConfig(forceRefresh) {
|
||||
if (state.schedulerConfigLoaded && !forceRefresh) {
|
||||
return;
|
||||
}
|
||||
var idleInputs = [
|
||||
document.getElementById('plugin-scheduler-idle-sleep'),
|
||||
document.getElementById('plugin-scheduler-idle-sleep-overview'),
|
||||
].filter(Boolean);
|
||||
if (idleInputs.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var cfg = await pluginRequest('GET', '/api/plugin/scheduler-config');
|
||||
state.schedulerConfig = cfg || {};
|
||||
state.schedulerConfigLoaded = true;
|
||||
var idleSeconds = Number((cfg && cfg.idle_sleep_seconds) || 0);
|
||||
idleInputs.forEach(function(input) {
|
||||
input.value = idleSeconds > 0 ? String(idleSeconds) : '';
|
||||
});
|
||||
} catch (e) {
|
||||
notify('Failed to load scheduler config: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSchedulerConfig(sourceInput) {
|
||||
var idleInputs = [
|
||||
document.getElementById('plugin-scheduler-idle-sleep'),
|
||||
document.getElementById('plugin-scheduler-idle-sleep-overview'),
|
||||
].filter(Boolean);
|
||||
if (idleInputs.length === 0) {
|
||||
return;
|
||||
}
|
||||
var raw = '';
|
||||
if (sourceInput) {
|
||||
raw = String(sourceInput.value || '').trim();
|
||||
}
|
||||
if (!raw) {
|
||||
for (var i = 0; i < idleInputs.length; i++) {
|
||||
if (idleInputs[i] === sourceInput) {
|
||||
continue;
|
||||
}
|
||||
var candidate = String(idleInputs[i].value || '').trim();
|
||||
if (candidate) {
|
||||
raw = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var parsed = raw ? parseInt(raw, 10) : 0;
|
||||
if (Number.isNaN(parsed) || parsed < 0) {
|
||||
notify('Invalid idle sleep value', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var updated = await pluginRequest('PUT', '/api/plugin/scheduler-config', {
|
||||
idle_sleep_seconds: parsed,
|
||||
});
|
||||
state.schedulerConfig = updated || {};
|
||||
state.schedulerConfigLoaded = true;
|
||||
var idleSeconds = Number((updated && updated.idle_sleep_seconds) || 0);
|
||||
idleInputs.forEach(function(input) {
|
||||
input.value = idleSeconds > 0 ? String(idleSeconds) : '';
|
||||
});
|
||||
notify('Scheduler settings saved', 'success');
|
||||
} catch (e) {
|
||||
notify('Failed to save scheduler config: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function getMaxResults() {
|
||||
var raw = String(document.getElementById('plugin-admin-max-results').value || '').trim();
|
||||
if (!raw) {
|
||||
@@ -3051,19 +2969,6 @@ templ Plugin(page string) {
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
var saveSchedulerBtn = document.getElementById('plugin-save-scheduler-btn');
|
||||
if (saveSchedulerBtn) {
|
||||
saveSchedulerBtn.addEventListener('click', function() {
|
||||
saveSchedulerConfig(document.getElementById('plugin-scheduler-idle-sleep'));
|
||||
});
|
||||
}
|
||||
var saveSchedulerBtnOverview = document.getElementById('plugin-save-scheduler-btn-overview');
|
||||
if (saveSchedulerBtnOverview) {
|
||||
saveSchedulerBtnOverview.addEventListener('click', function() {
|
||||
saveSchedulerConfig(document.getElementById('plugin-scheduler-idle-sleep-overview'));
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('plugin-trigger-detection-btn').addEventListener('click', function() {
|
||||
runDetection();
|
||||
});
|
||||
@@ -3148,7 +3053,6 @@ templ Plugin(page string) {
|
||||
ensureActiveNavigation();
|
||||
renderNavigationState();
|
||||
await refreshAll();
|
||||
await loadSchedulerConfig(false);
|
||||
|
||||
state.refreshTimer = setInterval(function() {
|
||||
refreshAll();
|
||||
|
||||
@@ -40,13 +40,13 @@ func Plugin(page string) templ.Component {
|
||||
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: `view/app/plugin.templ`, Line: 10, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `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\">Sequential scheduler with per-job runtime limits</small></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Enabled</th><th>Detector</th><th>In Flight</th><th>Max Runtime</th><th>Exec Global</th><th>Exec/Worker</th><th>Executor Workers</th><th>Effective Exec</th><th>Last Run</th></tr></thead> <tbody id=\"plugin-scheduler-table-body\"><tr><td colspan=\"10\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-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-sliders-h me-2\"></i>Scheduler Settings</h5><small class=\"text-muted\">Global</small></div><div class=\"card-body\"><div class=\"mb-2\"><label class=\"form-label\" for=\"plugin-scheduler-idle-sleep-overview\">Sleep Between Iterations (s)</label> <input type=\"number\" class=\"form-control\" id=\"plugin-scheduler-idle-sleep-overview\" min=\"0\"><div class=\"form-text\">Used when no jobs are detected.</div></div><button type=\"button\" class=\"btn btn-outline-primary\" id=\"plugin-save-scheduler-btn-overview\"><i class=\"fas fa-save me-1\"></i>Save Scheduler Settings</button></div></div></div><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-hourglass-half me-2\"></i>Next Run</h5><small class=\"text-muted\">Line truncated
|
||||
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\">Sequential scheduler with per-job runtime limits</small></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-sm table-hover mb-0\"><thead class=\"table-light\"><tr><th>Job Type</th><th>Enabled</th><th>Detector</th><th>In Flight</th><th>Max Runtime</th><th>Exec Global</th><th>Exec/Worker</th><th>Executor Workers</th><th>Effective Exec</th><th>Last Run</th></tr></thead> <tbody id=\"plugin-scheduler-table-body\"><tr><td colspan=\"10\" class=\"text-muted text-center py-3\">Loading...</td></tr></tbody></table></div></div></div></div></div><div class=\"row mb-4\"><div class=\"col-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-sliders-h me-2\"></i>Scheduler Settings</h5><small class=\"text-muted\">Global</small></div><div class=\"card-body\"><p class=\"text-muted mb-0\">Detection intervals are configured per job type in the job type settings below.</p></div></div></div><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-hourglass-half me-2\"></i>Next Run</h5><small class=\"text-muted\">Scheduler</small></div><div class=\"card-body\"><div class=\"h5 mb-1 plugin-scheduler-next-run\">-</div><div class=\"text-muted small plugin-scheduler-next-run-meta\">Not scheduled</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-usLine truncated
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user