feat: 时间同步阈值可配置,系统页添加本机时间卡片

This commit is contained in:
tian 2026-07-26 13:53:22 +08:00
parent aa20bdaeb1
commit 257f3ac034
5 changed files with 48 additions and 6 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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}}