feat: 时间同步阈值可配置,系统页添加本机时间卡片
This commit is contained in:
parent
aa20bdaeb1
commit
257f3ac034
@ -20,6 +20,7 @@ type Config struct {
|
||||
MediaRepoPath string `json:"media_repo_path,omitempty"` // explicit import-only source; not used for runtime rendering
|
||||
AlarmRetentionDays int `json:"alarm_retention_days"` // auto-delete alarms older than N days, 0=keep forever
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
TimeSyncThresholdSec int `json:"time_sync_threshold_sec"`
|
||||
DeviceAliases map[string]string `json:"device_aliases,omitempty"`
|
||||
path string
|
||||
}
|
||||
|
||||
@ -442,7 +442,11 @@ func (s *TaskService) executeOnDevice(task *models.Task, did string) {
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff <= 5 {
|
||||
threshold := int64(s.cfg.TimeSyncThresholdSec)
|
||||
if threshold <= 0 {
|
||||
threshold = 5
|
||||
}
|
||||
if diff <= threshold {
|
||||
s.updateDeviceStatus(task.ID, did, models.TaskSuccess, 1.0, "时间已同步")
|
||||
s.appendAuditLog(task, did, models.TaskSuccess, "")
|
||||
return
|
||||
|
||||
@ -117,6 +117,7 @@ type PageData struct {
|
||||
SystemAlarmRetention int
|
||||
SystemConcurrency int
|
||||
SystemCompanyName string
|
||||
TimeSyncThreshold int
|
||||
AlarmFilterDevices []string
|
||||
AlarmFilterRuleTypes []string
|
||||
AlarmDeviceNames map[string]string
|
||||
@ -4400,6 +4401,10 @@ func (u *UI) renderSystemPage(w http.ResponseWriter, r *http.Request, status int
|
||||
if u.cfg != nil {
|
||||
data.SystemAlarmRetention = u.cfg.AlarmRetentionDays
|
||||
data.SystemCompanyName = u.cfg.CompanyName
|
||||
data.TimeSyncThreshold = u.cfg.TimeSyncThresholdSec
|
||||
if data.TimeSyncThreshold <= 0 {
|
||||
data.TimeSyncThreshold = 5
|
||||
}
|
||||
}
|
||||
u.render(w, r, "system", data)
|
||||
}
|
||||
@ -4422,6 +4427,10 @@ func (u *UI) actionSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
u.cfg.CompanyName = company
|
||||
changed = true
|
||||
}
|
||||
if tval, err := strconv.Atoi(strings.TrimSpace(r.FormValue("time_sync_threshold_sec"))); err == nil && tval >= 1 && u.cfg != nil {
|
||||
u.cfg.TimeSyncThresholdSec = tval
|
||||
changed = true
|
||||
}
|
||||
if changed && u.cfg != nil && strings.TrimSpace(u.configPath) != "" {
|
||||
if data, err := json.MarshalIndent(u.cfg, "", " "); err == nil {
|
||||
os.WriteFile(u.configPath, data, 0o644)
|
||||
@ -5292,6 +5301,10 @@ func (u *UI) deviceOverviewPageData(r *http.Request, selectedIDs []string, errMs
|
||||
|
||||
// Check time drift
|
||||
now := time.Now().UnixMilli()
|
||||
threshold := int64(5) // default
|
||||
if u.cfg != nil && u.cfg.TimeSyncThresholdSec > 0 {
|
||||
threshold = int64(u.cfg.TimeSyncThresholdSec)
|
||||
}
|
||||
for i := range rows {
|
||||
dev := rows[i].Device
|
||||
if dev == nil || !dev.Online || u.agent == nil {
|
||||
@ -5313,6 +5326,12 @@ func (u *UI) deviceOverviewPageData(r *http.Request, selectedIDs []string, errMs
|
||||
}
|
||||
}
|
||||
|
||||
// Common threshold for time drift display
|
||||
if threshold <= 0 {
|
||||
threshold = 5
|
||||
}
|
||||
// (threshold used in template comparison: gt .TimeDriftSec $threshold)
|
||||
|
||||
data := PageData{
|
||||
Title: "设备管理",
|
||||
Devices: devices,
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
{{if $allOffline}}
|
||||
<button type="submit" id="cleanup-btn" name="action" value="cleanup_offline" class="danger" style="display:none" onclick="var n=document.querySelectorAll('input[name=device_id]:checked').length;return confirm('确认清理选中的 '+n+' 台离线设备及其全部关联数据?此操作不可恢复。')">清理选中离线设备</button>
|
||||
{{end}}
|
||||
{{$drift := false}}{{range .DeviceRows}}{{if and (gt .TimeDriftSec 5) (hasString $.SelectedDeviceIDs .Device.DeviceID)}}{{$drift = true}}{{end}}{{end}}
|
||||
{{$drift := false}}{{range .DeviceRows}}{{if and (gt .TimeDriftSec $.TimeSyncThreshold) (hasString $.SelectedDeviceIDs .Device.DeviceID)}}{{$drift = true}}{{end}}{{end}}
|
||||
{{if $drift}}
|
||||
<button type="submit" name="action" value="time_sync" class="secondary">⏱ 时间同步</button>
|
||||
{{end}}
|
||||
@ -97,7 +97,7 @@
|
||||
<span class="pill bad">未知</span>
|
||||
{{end}}
|
||||
{{if .IntegrationStale}}<span class="pill warn">需重新下发</span>{{end}}
|
||||
{{if gt .TimeDriftSec 5}}<span class="pill bad">⏱ 偏差{{.TimeDriftSec}}秒</span>{{end}}
|
||||
{{if gt .TimeDriftSec $.TimeSyncThreshold}}<span class="pill bad">⏱ 偏差{{.TimeDriftSec}}秒</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -23,6 +23,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="title-with-icon">{{icon "heartbeat"}}<span>本机时间</span></h2>
|
||||
<div class="info-list compact-list">
|
||||
<div><span>当前时间</span><strong id="local-time">--</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="card">
|
||||
<h2 class="title-with-icon">{{icon "heartbeat"}}<span>服务状态</span></h2>
|
||||
@ -40,6 +47,7 @@
|
||||
<div class="field-grid">
|
||||
<label><span>告警保留天数</span><input type="number" name="alarm_retention_days" value="{{.SystemAlarmRetention}}" min="0" /><div class="form-hint">0=永久保留</div></label>
|
||||
<label><span>公司名称</span><input type="text" name="company_name" value="{{.SystemCompanyName}}" placeholder="将在页面底部显示" /></label>
|
||||
<label><span>时间同步阈值(秒)</span><input type="number" name="time_sync_threshold_sec" value="{{.TimeSyncThreshold}}" min="1" /><div class="form-hint">偏差超此值显示角标,默认5秒</div></label>
|
||||
</div>
|
||||
<div class="actions" style="margin-top:12px">
|
||||
<button type="submit" class="secondary">保存配置</button>
|
||||
@ -58,7 +66,6 @@
|
||||
svcEl.style.color = "";
|
||||
apiEl.textContent = "检测中…";
|
||||
apiEl.style.color = "";
|
||||
|
||||
fetch("/health")
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(t) {
|
||||
@ -74,7 +81,6 @@
|
||||
svcEl.textContent = "无法连接";
|
||||
svcEl.style.color = "var(--danger-soft-text)";
|
||||
});
|
||||
|
||||
fetch("/openapi.json")
|
||||
.then(function(r) {
|
||||
if (r.ok) {
|
||||
@ -90,9 +96,21 @@
|
||||
apiEl.style.color = "var(--danger-soft-text)";
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("btn-refresh").addEventListener("click", check);
|
||||
check();
|
||||
|
||||
function updateTime() {
|
||||
var now = new Date();
|
||||
var s = now.getFullYear() + '-' +
|
||||
String(now.getMonth()+1).padStart(2,'0') + '-' +
|
||||
String(now.getDate()).padStart(2,'0') + ' ' +
|
||||
String(now.getHours()).padStart(2,'0') + ':' +
|
||||
String(now.getMinutes()).padStart(2,'0') + ':' +
|
||||
String(now.getSeconds()).padStart(2,'0');
|
||||
document.getElementById('local-time').textContent = s;
|
||||
}
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user