feat: alarm page date filter, pagination, auto-cleanup
- 告警页增加日期范围筛选(默认今天),50 条/页分页 - AlarmCollector.GetFiltered 支持 from/to/limit/offset - alarm_retention_days 配置项(默认 30 天),超期自动清理
This commit is contained in:
parent
65eb988905
commit
74ca578483
@ -74,6 +74,9 @@ func main() {
|
||||
log.Printf("load persisted tasks: %v", err)
|
||||
}
|
||||
alarmCollector := service.NewAlarmCollector(store.DB(), agentClient, regSvc)
|
||||
if cfg.AlarmRetentionDays > 0 {
|
||||
alarmCollector.SetRetention(cfg.AlarmRetentionDays)
|
||||
}
|
||||
alarmCollector.Start()
|
||||
tplSvc := service.NewTemplateService(cfg)
|
||||
h := api.NewHandler(discoSvc, regSvc, agentClient, taskSvc, tplSvc)
|
||||
|
||||
@ -18,6 +18,7 @@ type Config struct {
|
||||
DBPath string `json:"db_path,omitempty"`
|
||||
LogDir string `json:"log_dir,omitempty"`
|
||||
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
|
||||
DeviceAliases map[string]string `json:"device_aliases,omitempty"`
|
||||
path string
|
||||
}
|
||||
|
||||
@ -27,11 +27,12 @@ type AlarmRecord struct {
|
||||
}
|
||||
|
||||
type AlarmCollector struct {
|
||||
db *sql.DB
|
||||
agent *AgentClient
|
||||
registry *RegistryService
|
||||
mu sync.Mutex
|
||||
lastID string // last alarm ID seen, to avoid duplicates
|
||||
db *sql.DB
|
||||
agent *AgentClient
|
||||
registry *RegistryService
|
||||
mu sync.Mutex
|
||||
lastID string // last alarm ID seen, to avoid duplicates
|
||||
retentionDays int // auto-delete alarms older than N days, 0=keep
|
||||
}
|
||||
|
||||
func NewAlarmCollector(db *sql.DB, agent *AgentClient, registry *RegistryService) *AlarmCollector {
|
||||
@ -40,6 +41,32 @@ func NewAlarmCollector(db *sql.DB, agent *AgentClient, registry *RegistryService
|
||||
|
||||
func (c *AlarmCollector) Start() {
|
||||
go c.poll()
|
||||
go c.cleanupLoop()
|
||||
}
|
||||
|
||||
// SetRetention configures alarm retention in days. 0 = keep forever.
|
||||
func (c *AlarmCollector) SetRetention(days int) {
|
||||
c.retentionDays = days
|
||||
}
|
||||
|
||||
func (c *AlarmCollector) cleanupLoop() {
|
||||
c.cleanupOldAlarms()
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
c.cleanupOldAlarms()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AlarmCollector) cleanupOldAlarms() {
|
||||
if c.db == nil || c.retentionDays <= 0 {
|
||||
return
|
||||
}
|
||||
cutoff := time.Now().AddDate(0, 0, -c.retentionDays).Format(time.RFC3339)
|
||||
_, err := c.db.Exec(`DELETE FROM alarm_records WHERE collected_at < ?`, cutoff)
|
||||
if err != nil {
|
||||
log.Printf("alarm cleanup: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AlarmCollector) poll() {
|
||||
@ -183,6 +210,46 @@ ON CONFLICT(id) DO NOTHING
|
||||
}
|
||||
}
|
||||
|
||||
// GetFiltered returns alarm records within a date range, with pagination.
|
||||
// Returns records and total count (ignoring limit/offset for count).
|
||||
func (c *AlarmCollector) GetFiltered(from, to string, limit, offset int) ([]AlarmRecord, int) {
|
||||
if c.db == nil {
|
||||
return nil, 0
|
||||
}
|
||||
fromTS := from + "T00:00:00"
|
||||
toTS := to + "T23:59:59"
|
||||
|
||||
// Count total
|
||||
var total int
|
||||
c.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM alarm_records
|
||||
WHERE timestamp >= ? AND timestamp <= ?
|
||||
`, fromTS, toTS).Scan(&total)
|
||||
|
||||
// Fetch page
|
||||
rows, err := c.db.Query(`
|
||||
SELECT id, device_id, channel, timestamp, rule_name, rule_type, object_label, confidence, snapshot_url, clip_url, duration_ms, collected_at
|
||||
FROM alarm_records
|
||||
WHERE timestamp >= ? AND timestamp <= ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, fromTS, toTS, limit, offset)
|
||||
if err != nil {
|
||||
return nil, total
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var alarms []AlarmRecord
|
||||
for rows.Next() {
|
||||
var a AlarmRecord
|
||||
if err := rows.Scan(&a.ID, &a.DeviceID, &a.Channel, &a.Timestamp, &a.RuleName, &a.RuleType, &a.ObjectLabel, &a.Confidence, &a.SnapshotURL, &a.ClipURL, &a.DurationMs, &a.CollectedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
alarms = append(alarms, a)
|
||||
}
|
||||
return alarms, total
|
||||
}
|
||||
|
||||
// GetRecent returns the most recent N alarm records, newest first.
|
||||
func (c *AlarmCollector) GetRecent(limit int) []AlarmRecord {
|
||||
if c.db == nil || limit <= 0 {
|
||||
|
||||
@ -83,6 +83,11 @@ type PageData struct {
|
||||
StandardResources []storage.StandardResourceRecord
|
||||
ResourceStatusBoard *service.ResourceStatusBoard
|
||||
AlarmRecords []service.AlarmRecord
|
||||
TotalAlarmCount int
|
||||
TotalAlarmPages int
|
||||
CurrentPage int
|
||||
SelectedFrom string
|
||||
SelectedTo string
|
||||
TodayAlarmCount int
|
||||
DeviceMetrics []DeviceMetric
|
||||
ConsoleDevices []ConsoleDeviceData
|
||||
@ -491,6 +496,9 @@ func NewUI(discovery *service.DiscoveryService, registry *service.RegistryServic
|
||||
}
|
||||
return v
|
||||
},
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
}).ParseFS(uiFS, "ui/templates/*.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1897,8 +1905,40 @@ func (u *UI) pageDiagnostics(w http.ResponseWriter, r *http.Request) {
|
||||
func (u *UI) pageAlarms(w http.ResponseWriter, r *http.Request) {
|
||||
u.ensureDevicesLoaded()
|
||||
data := PageData{Title: "告警中心", Devices: u.registry.GetDevices()}
|
||||
|
||||
// Parse date filter: ?date=2026-05-12 or ?from=...&to=... (default: today)
|
||||
q := r.URL.Query()
|
||||
today := time.Now().Format("2006-01-02")
|
||||
from := strings.TrimSpace(q.Get("from"))
|
||||
to := strings.TrimSpace(q.Get("to"))
|
||||
date := strings.TrimSpace(q.Get("date"))
|
||||
if date != "" {
|
||||
from = date
|
||||
to = date
|
||||
}
|
||||
if from == "" {
|
||||
from = today
|
||||
}
|
||||
if to == "" {
|
||||
to = today
|
||||
}
|
||||
// Parse page: ?page=N (default 1, 50 per page)
|
||||
page := 1
|
||||
if p, err := strconv.Atoi(q.Get("page")); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
perPage := 50
|
||||
|
||||
data.SelectedFrom = from
|
||||
data.SelectedTo = to
|
||||
data.CurrentPage = page
|
||||
|
||||
if u.alarmCollector != nil {
|
||||
data.AlarmRecords = u.alarmCollector.GetRecent(100)
|
||||
data.AlarmRecords, data.TotalAlarmCount = u.alarmCollector.GetFiltered(from, to, perPage, (page-1)*perPage)
|
||||
}
|
||||
// Calculate total pages
|
||||
if data.TotalAlarmCount > 0 {
|
||||
data.TotalAlarmPages = (data.TotalAlarmCount + perPage - 1) / perPage
|
||||
}
|
||||
u.render(w, r, "alarms", data)
|
||||
}
|
||||
|
||||
@ -9,6 +9,14 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="get" action="/ui/alarms" style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
|
||||
<input type="date" name="from" value="{{.SelectedFrom}}" style="width:150px;font-size:12px">
|
||||
<span class="muted small">至</span>
|
||||
<input type="date" name="to" value="{{.SelectedTo}}" style="width:150px;font-size:12px">
|
||||
<button class="btn" style="font-size:12px;padding:4px 12px">筛选</button>
|
||||
<a href="/ui/alarms" class="btn ghost" style="font-size:11px">今天</a>
|
||||
</form>
|
||||
|
||||
<div class="table-wrap">
|
||||
{{if .AlarmRecords}}
|
||||
<table>
|
||||
@ -28,14 +36,14 @@
|
||||
<tr>
|
||||
<td class="mono small">{{.Timestamp}}</td>
|
||||
<td>
|
||||
<a href="/ui/devices/{{.DeviceID}}">{{.DeviceID}}</a>
|
||||
<a href="/ui/devices/{{.DeviceID}}">{{shortID .DeviceID}}</a>
|
||||
</td>
|
||||
<td>{{.Channel}}</td>
|
||||
<td>
|
||||
<span class="pill warn">{{.RuleName}}</span>
|
||||
</td>
|
||||
<td>{{if .ObjectLabel}}{{.ObjectLabel}}{{else}}-{{end}}</td>
|
||||
<td>{{if .Confidence}}{{.Confidence}}{{else}}-{{end}}</td>
|
||||
<td>{{if .Confidence}}{{printf "%.2f" .Confidence}}{{else}}-{{end}}</td>
|
||||
<td>
|
||||
{{if .SnapshotURL}}
|
||||
<a href="{{.SnapshotURL}}" target="_blank" class="btn ghost">查看截图</a>
|
||||
@ -48,10 +56,20 @@
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{if gt .TotalAlarmPages 1}}
|
||||
<div style="margin-top:12px;display:flex;gap:6px;align-items:center">
|
||||
<span class="muted small">{{.TotalAlarmCount}} 条记录,{{.TotalAlarmPages}} 页</span>
|
||||
{{$from := $.SelectedFrom}}{{$to := $.SelectedTo}}
|
||||
{{range $p := loop .TotalAlarmPages}}
|
||||
{{$pn := add $p 1}}
|
||||
<a href="/ui/alarms?from={{$from}}&to={{$to}}&page={{$pn}}" class="btn {{if eq $pn $.CurrentPage}}primary{{else}}ghost{{end}}" style="font-size:11px;padding:2px 8px">{{$pn}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<div class="empty-title">暂无告警记录</div>
|
||||
<div class="muted">设备尚未上报告警,或告警收集服务尚未启动。</div>
|
||||
<div class="muted">所选日期范围内没有告警,或设备尚未上报。</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@ -10,5 +10,6 @@
|
||||
"log_dir": "data/logs",
|
||||
"device_aliases": {
|
||||
"d12a4719c91641df91b76ab271280797": "盒子A"
|
||||
}
|
||||
},
|
||||
"alarm_retention_days": 30
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user