更新UI
This commit is contained in:
@@ -20,6 +20,7 @@ func (s *Store) DashboardOverview(limit int) (map[string]any, error) {
|
||||
healthCounts, _ := s.groupCounts("source_endpoints", "last_status")
|
||||
recentChecks, _ := s.RecentSourceChecks(limit)
|
||||
recentCalls, _ := s.RecentSourceCalls(limit)
|
||||
averageLatency, _ := s.AverageSourceLatencyBuckets(limit)
|
||||
audit, _ := s.ListAuditLogs(10)
|
||||
return map[string]any{
|
||||
"ok": true,
|
||||
@@ -34,12 +35,94 @@ func (s *Store) DashboardOverview(limit int) (map[string]any, error) {
|
||||
"feedbackStatus": statusCounts,
|
||||
"sourceHealth": healthCounts,
|
||||
"heartbeats": recentChecks,
|
||||
"averageLatency": averageLatency,
|
||||
"clientCalls": recentCalls,
|
||||
"database": s.Status(),
|
||||
"audit": audit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) AverageSourceLatencyBuckets(limit int) ([]map[string]any, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 80
|
||||
}
|
||||
rows, err := s.query(`SELECT checked_at, latency_ms, status FROM endpoint_health_checks ORDER BY checked_at DESC, id DESC LIMIT ?`, limit*4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
type bucket struct {
|
||||
label string
|
||||
total int
|
||||
count int
|
||||
ok int
|
||||
latest string
|
||||
}
|
||||
order := []string{}
|
||||
buckets := map[string]*bucket{}
|
||||
for rows.Next() {
|
||||
var checkedAt, status string
|
||||
var latency int
|
||||
if err := rows.Scan(&checkedAt, &latency, &status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
label := latencyBucketLabel(checkedAt)
|
||||
if label == "" {
|
||||
label = checkedAt
|
||||
}
|
||||
item, ok := buckets[label]
|
||||
if !ok {
|
||||
item = &bucket{label: label, latest: checkedAt}
|
||||
buckets[label] = item
|
||||
order = append(order, label)
|
||||
}
|
||||
item.total += latency
|
||||
item.count++
|
||||
if status == "ok" || status == "redirected" {
|
||||
item.ok++
|
||||
}
|
||||
if checkedAt > item.latest {
|
||||
item.latest = checkedAt
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []map[string]any{}
|
||||
for i := len(order) - 1; i >= 0; i-- {
|
||||
item := buckets[order[i]]
|
||||
if item == nil || item.count == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"label": item.label,
|
||||
"averageLatency": item.total / item.count,
|
||||
"avgLatencyMs": item.total / item.count,
|
||||
"sampleCount": item.count,
|
||||
"healthyCount": item.ok,
|
||||
"checkedAt": item.latest,
|
||||
})
|
||||
}
|
||||
if len(out) > limit {
|
||||
out = out[len(out)-limit:]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func latencyBucketLabel(value string) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
if len(value) >= 16 {
|
||||
return value[:16]
|
||||
}
|
||||
return value
|
||||
}
|
||||
return parsed.UTC().Format("01-02 15:04")
|
||||
}
|
||||
|
||||
func (s *Store) RecentSourceChecks(limit int) ([]map[string]any, error) {
|
||||
rows, err := s.query(`SELECT h.id, h.source_db_id, COALESCE(e.source_id, ''), COALESCE(e.name, ''), h.status, h.latency_ms, h.error, h.checked_at
|
||||
FROM endpoint_health_checks h LEFT JOIN source_endpoints e ON e.id = h.source_db_id
|
||||
|
||||
@@ -3,6 +3,8 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Store) UpsertSource(item Source) (Source, error) {
|
||||
@@ -165,6 +167,65 @@ func (s *Store) RecordSourceCheck(sourceDBID int64, status string, latency int,
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) SourceHealthHistory(sourceDBIDs []int64, limit int) (map[int64][]map[string]any, error) {
|
||||
if len(sourceDBIDs) == 0 {
|
||||
return map[int64][]map[string]any{}, nil
|
||||
}
|
||||
if limit <= 0 || limit > 48 {
|
||||
limit = 16
|
||||
}
|
||||
placeholders := make([]string, 0, len(sourceDBIDs))
|
||||
args := make([]any, 0, len(sourceDBIDs)+1)
|
||||
for _, id := range sourceDBIDs {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, id)
|
||||
}
|
||||
if len(placeholders) == 0 {
|
||||
return map[int64][]map[string]any{}, nil
|
||||
}
|
||||
args = append(args, limit*len(placeholders))
|
||||
rows, err := s.query(fmt.Sprintf(`SELECT source_db_id, status, latency_ms, checked_at
|
||||
FROM endpoint_health_checks
|
||||
WHERE source_db_id IN (%s)
|
||||
ORDER BY checked_at DESC, id DESC LIMIT ?`, strings.Join(placeholders, ",")), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[int64][]map[string]any{}
|
||||
for rows.Next() {
|
||||
var sourceDBID int64
|
||||
var status, checkedAt string
|
||||
var latency int
|
||||
if err := rows.Scan(&sourceDBID, &status, &latency, &checkedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out[sourceDBID]) >= limit {
|
||||
continue
|
||||
}
|
||||
out[sourceDBID] = append(out[sourceDBID], map[string]any{
|
||||
"status": status,
|
||||
"latencyMs": latency,
|
||||
"latency_ms": latency,
|
||||
"checkedAt": checkedAt,
|
||||
"checked_at": checkedAt,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for id, items := range out {
|
||||
for left, right := 0, len(items)-1; left < right; left, right = left+1, right-1 {
|
||||
items[left], items[right] = items[right], items[left]
|
||||
}
|
||||
out[id] = items
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) RecordSourceCall(call SourceCall) error {
|
||||
if call.CreatedAt == "" {
|
||||
call.CreatedAt = Now()
|
||||
|
||||
Reference in New Issue
Block a user