feat(channel-monitor): redesign user dashboard as card grid
Reference check-cx UI: INTELLIGENCE MONITOR hero + 3-column card grid with 60-point timeline bars. Backend: - Add PrimaryPingLatencyMs + Timeline[60] to UserMonitorView - ListRecentHistoryForMonitors: batch CTE + ROW_NUMBER() window query - indexLatestByModel / indexAvailabilityByModel helpers Frontend: - 7 new components: ProviderIcon, MonitorMetricPair, MonitorAvailabilityRow, MonitorTimeline, MonitorHero, MonitorCard, MonitorCardGrid - ChannelStatusView 381→~180 lines (delegated to subcomponents) - AbortController reload concurrency protection - HSL 0-120° availability color mapping - Replace emoji with Icon component (bolt / globe) - i18n: monitorCommon.* shared namespace, channelStatus.hero.* Bump VERSION to 0.1.114.24
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
|
||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
@@ -22,15 +24,26 @@ func NewChannelMonitorUserHandler(monitorService *service.ChannelMonitorService)
|
||||
// --- Response ---
|
||||
|
||||
type channelMonitorUserListItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
GroupName string `json:"group_name"`
|
||||
PrimaryModel string `json:"primary_model"`
|
||||
PrimaryStatus string `json:"primary_status"`
|
||||
PrimaryLatencyMs *int `json:"primary_latency_ms"`
|
||||
Availability7d float64 `json:"availability_7d"`
|
||||
ExtraModels []dto.ChannelMonitorExtraModelStatus `json:"extra_models"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
GroupName string `json:"group_name"`
|
||||
PrimaryModel string `json:"primary_model"`
|
||||
PrimaryStatus string `json:"primary_status"`
|
||||
PrimaryLatencyMs *int `json:"primary_latency_ms"`
|
||||
PrimaryPingLatencyMs *int `json:"primary_ping_latency_ms"`
|
||||
Availability7d float64 `json:"availability_7d"`
|
||||
ExtraModels []dto.ChannelMonitorExtraModelStatus `json:"extra_models"`
|
||||
Timeline []channelMonitorUserTimelinePoint `json:"timeline"`
|
||||
}
|
||||
|
||||
// channelMonitorUserTimelinePoint 主模型最近一次检测的 timeline 点。
|
||||
// 仅用于用户视图 list 响应,admin 视图不使用。
|
||||
type channelMonitorUserTimelinePoint struct {
|
||||
Status string `json:"status"`
|
||||
LatencyMs *int `json:"latency_ms"`
|
||||
PingLatencyMs *int `json:"ping_latency_ms"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
}
|
||||
|
||||
type channelMonitorUserDetailResponse struct {
|
||||
@@ -60,16 +73,27 @@ func userMonitorViewToItem(v *service.UserMonitorView) channelMonitorUserListIte
|
||||
LatencyMs: e.LatencyMs,
|
||||
})
|
||||
}
|
||||
timeline := make([]channelMonitorUserTimelinePoint, 0, len(v.Timeline))
|
||||
for _, p := range v.Timeline {
|
||||
timeline = append(timeline, channelMonitorUserTimelinePoint{
|
||||
Status: p.Status,
|
||||
LatencyMs: p.LatencyMs,
|
||||
PingLatencyMs: p.PingLatencyMs,
|
||||
CheckedAt: p.CheckedAt.UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return channelMonitorUserListItem{
|
||||
ID: v.ID,
|
||||
Name: v.Name,
|
||||
Provider: v.Provider,
|
||||
GroupName: v.GroupName,
|
||||
PrimaryModel: v.PrimaryModel,
|
||||
PrimaryStatus: v.PrimaryStatus,
|
||||
PrimaryLatencyMs: v.PrimaryLatencyMs,
|
||||
Availability7d: v.Availability7d,
|
||||
ExtraModels: extras,
|
||||
ID: v.ID,
|
||||
Name: v.Name,
|
||||
Provider: v.Provider,
|
||||
GroupName: v.GroupName,
|
||||
PrimaryModel: v.PrimaryModel,
|
||||
PrimaryStatus: v.PrimaryStatus,
|
||||
PrimaryLatencyMs: v.PrimaryLatencyMs,
|
||||
PrimaryPingLatencyMs: v.PrimaryPingLatencyMs,
|
||||
Availability7d: v.Availability7d,
|
||||
ExtraModels: extras,
|
||||
Timeline: timeline,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ func (r *channelMonitorRepository) ListHistory(ctx context.Context, monitorID in
|
||||
func (r *channelMonitorRepository) ListLatestPerModel(ctx context.Context, monitorID int64) ([]*service.ChannelMonitorLatest, error) {
|
||||
const q = `
|
||||
SELECT DISTINCT ON (model)
|
||||
model, status, latency_ms, checked_at
|
||||
model, status, latency_ms, ping_latency_ms, checked_at
|
||||
FROM channel_monitor_histories
|
||||
WHERE monitor_id = $1
|
||||
ORDER BY model, checked_at DESC
|
||||
@@ -257,19 +257,27 @@ func (r *channelMonitorRepository) ListLatestPerModel(ctx context.Context, monit
|
||||
out := make([]*service.ChannelMonitorLatest, 0)
|
||||
for rows.Next() {
|
||||
l := &service.ChannelMonitorLatest{}
|
||||
var latency sql.NullInt64
|
||||
if err := rows.Scan(&l.Model, &l.Status, &latency, &l.CheckedAt); err != nil {
|
||||
var latency, ping sql.NullInt64
|
||||
if err := rows.Scan(&l.Model, &l.Status, &latency, &ping, &l.CheckedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan latest row: %w", err)
|
||||
}
|
||||
if latency.Valid {
|
||||
v := int(latency.Int64)
|
||||
l.LatencyMs = &v
|
||||
}
|
||||
assignNullInt(&l.LatencyMs, latency)
|
||||
assignNullInt(&l.PingLatencyMs, ping)
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// assignNullInt 把 sql.NullInt64 解包到 *int 指针目标(valid 才分配新 int)。
|
||||
// 集中实现避免 latency / ping 两处重复 if latency.Valid { v := int(...) ... } 模板。
|
||||
func assignNullInt(dst **int, n sql.NullInt64) {
|
||||
if !n.Valid {
|
||||
return
|
||||
}
|
||||
v := int(n.Int64)
|
||||
*dst = &v
|
||||
}
|
||||
|
||||
// ComputeAvailability 计算指定窗口内每个模型的可用率与平均延迟。
|
||||
// "可用" = status IN (operational, degraded)。
|
||||
func (r *channelMonitorRepository) ComputeAvailability(ctx context.Context, monitorID int64, windowDays int) ([]*service.ChannelMonitorAvailability, error) {
|
||||
@@ -338,7 +346,7 @@ func (r *channelMonitorRepository) ListLatestForMonitorIDs(ctx context.Context,
|
||||
}
|
||||
const q = `
|
||||
SELECT DISTINCT ON (monitor_id, model)
|
||||
monitor_id, model, status, latency_ms, checked_at
|
||||
monitor_id, model, status, latency_ms, ping_latency_ms, checked_at
|
||||
FROM channel_monitor_histories
|
||||
WHERE monitor_id = ANY($1)
|
||||
ORDER BY monitor_id, model, checked_at DESC
|
||||
@@ -352,14 +360,12 @@ func (r *channelMonitorRepository) ListLatestForMonitorIDs(ctx context.Context,
|
||||
for rows.Next() {
|
||||
var monitorID int64
|
||||
l := &service.ChannelMonitorLatest{}
|
||||
var latency sql.NullInt64
|
||||
if err := rows.Scan(&monitorID, &l.Model, &l.Status, &latency, &l.CheckedAt); err != nil {
|
||||
var latency, ping sql.NullInt64
|
||||
if err := rows.Scan(&monitorID, &l.Model, &l.Status, &latency, &ping, &l.CheckedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan latest batch row: %w", err)
|
||||
}
|
||||
if latency.Valid {
|
||||
v := int(latency.Int64)
|
||||
l.LatencyMs = &v
|
||||
}
|
||||
assignNullInt(&l.LatencyMs, latency)
|
||||
assignNullInt(&l.PingLatencyMs, ping)
|
||||
out[monitorID] = append(out[monitorID], l)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -368,6 +374,107 @@ func (r *channelMonitorRepository) ListLatestForMonitorIDs(ctx context.Context,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListRecentHistoryForMonitors 为多个 monitor 批量取各自"指定模型"最近 N 条历史(按 checked_at DESC,最新在前)。
|
||||
// primaryModels[monitorID] 指定该监控要过滤的模型名;monitor 不在 primaryModels 中的记录不返回。
|
||||
// 通过 CTE + unnest(两个 int8/text 数组) 构造 (monitor_id, model) 白名单,
|
||||
// 再用 ROW_NUMBER() OVER (PARTITION BY monitor_id) 取各自前 N 条。
|
||||
//
|
||||
// 返回值:map[monitorID] -> []*ChannelMonitorHistoryEntry(不含 message,减少网络开销)。
|
||||
// 空 ids / 空 primaryModels 返回空 map,不报错。
|
||||
func (r *channelMonitorRepository) ListRecentHistoryForMonitors(
|
||||
ctx context.Context,
|
||||
ids []int64,
|
||||
primaryModels map[int64]string,
|
||||
perMonitorLimit int,
|
||||
) (map[int64][]*service.ChannelMonitorHistoryEntry, error) {
|
||||
out := make(map[int64][]*service.ChannelMonitorHistoryEntry, len(ids))
|
||||
pairIDs, pairModels := buildMonitorModelPairs(ids, primaryModels)
|
||||
if len(pairIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
perMonitorLimit = clampTimelineLimit(perMonitorLimit)
|
||||
|
||||
const q = `
|
||||
WITH targets AS (
|
||||
SELECT unnest($1::bigint[]) AS monitor_id,
|
||||
unnest($2::text[]) AS model
|
||||
),
|
||||
ranked AS (
|
||||
SELECT h.monitor_id,
|
||||
h.status,
|
||||
h.latency_ms,
|
||||
h.ping_latency_ms,
|
||||
h.checked_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY h.monitor_id ORDER BY h.checked_at DESC) AS rn
|
||||
FROM channel_monitor_histories h
|
||||
JOIN targets t
|
||||
ON t.monitor_id = h.monitor_id AND t.model = h.model
|
||||
)
|
||||
SELECT monitor_id, status, latency_ms, ping_latency_ms, checked_at
|
||||
FROM ranked
|
||||
WHERE rn <= $3
|
||||
ORDER BY monitor_id, checked_at DESC
|
||||
`
|
||||
rows, err := r.db.QueryContext(ctx, q, pq.Array(pairIDs), pq.Array(pairModels), perMonitorLimit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query recent history batch: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
for rows.Next() {
|
||||
var monitorID int64
|
||||
entry := &service.ChannelMonitorHistoryEntry{}
|
||||
var latency, ping sql.NullInt64
|
||||
if err := rows.Scan(&monitorID, &entry.Status, &latency, &ping, &entry.CheckedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan recent history row: %w", err)
|
||||
}
|
||||
assignNullInt(&entry.LatencyMs, latency)
|
||||
assignNullInt(&entry.PingLatencyMs, ping)
|
||||
out[monitorID] = append(out[monitorID], entry)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// buildMonitorModelPairs 基于 ids 过滤出有效的 (monitor_id, model) 对,model 为空时跳过。
|
||||
// 保证两个数组长度一致且一一对应,供 unnest 展开。
|
||||
func buildMonitorModelPairs(ids []int64, primaryModels map[int64]string) ([]int64, []string) {
|
||||
if len(ids) == 0 || len(primaryModels) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
pairIDs := make([]int64, 0, len(ids))
|
||||
pairModels := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
model, ok := primaryModels[id]
|
||||
if !ok || strings.TrimSpace(model) == "" {
|
||||
continue
|
||||
}
|
||||
pairIDs = append(pairIDs, id)
|
||||
pairModels = append(pairModels, model)
|
||||
}
|
||||
return pairIDs, pairModels
|
||||
}
|
||||
|
||||
// timelineLimit* 批量 timeline 查询的 perMonitorLimit 夹紧范围。
|
||||
// 下限 1 表示至少返回最近一条;上限 200 控制单次响应体与 SQL 内存占用(ROW_NUMBER 窗口上限)。
|
||||
const (
|
||||
timelineLimitMin = 1
|
||||
timelineLimitMax = 200
|
||||
)
|
||||
|
||||
// clampTimelineLimit 把 perMonitorLimit 夹紧到 [timelineLimitMin, timelineLimitMax],避免非法值或超大查询。
|
||||
func clampTimelineLimit(n int) int {
|
||||
if n < timelineLimitMin {
|
||||
return timelineLimitMin
|
||||
}
|
||||
if n > timelineLimitMax {
|
||||
return timelineLimitMax
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// ComputeAvailabilityForMonitors 一次性计算多个监控在某个窗口内的每模型可用率与平均延迟。
|
||||
func (r *channelMonitorRepository) ComputeAvailabilityForMonitors(ctx context.Context, ids []int64, windowDays int) (map[int64][]*service.ChannelMonitorAvailability, error) {
|
||||
out := make(map[int64][]*service.ChannelMonitorAvailability, len(ids))
|
||||
|
||||
@@ -49,7 +49,12 @@ func (s *ChannelMonitorService) BatchMonitorStatusSummary(
|
||||
}
|
||||
|
||||
// ListUserView 用户只读视图:列出所有 enabled 监控的概览。
|
||||
// 使用批量聚合接口避免 N+1:1 次查 monitors,1 次查 latest(所有 monitor),1 次查 availability。
|
||||
// 使用批量聚合接口避免 N+1:
|
||||
//
|
||||
// 1 次查 monitors;
|
||||
// 1 次批量 latest(含 ping_latency_ms);
|
||||
// 1 次批量 7d availability;
|
||||
// 1 次批量 timeline(主模型最近 N 条)。
|
||||
func (s *ChannelMonitorService) ListUserView(ctx context.Context) ([]*UserMonitorView, error) {
|
||||
monitors, err := s.repo.ListEnabled(ctx)
|
||||
if err != nil {
|
||||
@@ -59,6 +64,21 @@ func (s *ChannelMonitorService) ListUserView(ctx context.Context) ([]*UserMonito
|
||||
return []*UserMonitorView{}, nil
|
||||
}
|
||||
|
||||
ids, primaryByID, extrasByID := collectMonitorIndexes(monitors)
|
||||
summaries := s.BatchMonitorStatusSummary(ctx, ids, primaryByID, extrasByID)
|
||||
latestMap := s.batchLatest(ctx, ids)
|
||||
timelineMap := s.batchTimeline(ctx, ids, primaryByID)
|
||||
|
||||
views := make([]*UserMonitorView, 0, len(monitors))
|
||||
for _, m := range monitors {
|
||||
primaryLatest := pickLatest(latestMap[m.ID], m.PrimaryModel)
|
||||
views = append(views, buildUserViewFromSummary(m, summaries[m.ID], primaryLatest, timelineMap[m.ID]))
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
// collectMonitorIndexes 把 monitors 列表按 ID 展开为聚合查询所需的三个索引结构。
|
||||
func collectMonitorIndexes(monitors []*ChannelMonitor) ([]int64, map[int64]string, map[int64][]string) {
|
||||
ids := make([]int64, 0, len(monitors))
|
||||
primaryByID := make(map[int64]string, len(monitors))
|
||||
extrasByID := make(map[int64][]string, len(monitors))
|
||||
@@ -67,14 +87,44 @@ func (s *ChannelMonitorService) ListUserView(ctx context.Context) ([]*UserMonito
|
||||
primaryByID[m.ID] = m.PrimaryModel
|
||||
extrasByID[m.ID] = m.ExtraModels
|
||||
}
|
||||
summaries := s.BatchMonitorStatusSummary(ctx, ids, primaryByID, extrasByID)
|
||||
return ids, primaryByID, extrasByID
|
||||
}
|
||||
|
||||
views := make([]*UserMonitorView, 0, len(monitors))
|
||||
for _, m := range monitors {
|
||||
summary := summaries[m.ID]
|
||||
views = append(views, buildUserViewFromSummary(m, summary))
|
||||
// batchLatest 批量取 latest per model,失败仅日志(与现有 BatchMonitorStatusSummary 一致,不阻断列表渲染)。
|
||||
func (s *ChannelMonitorService) batchLatest(ctx context.Context, ids []int64) map[int64][]*ChannelMonitorLatest {
|
||||
latestMap, err := s.repo.ListLatestForMonitorIDs(ctx, ids)
|
||||
if err != nil {
|
||||
slog.Warn("channel_monitor: user view batch latest failed", "error", err)
|
||||
return map[int64][]*ChannelMonitorLatest{}
|
||||
}
|
||||
return views, nil
|
||||
return latestMap
|
||||
}
|
||||
|
||||
// batchTimeline 批量取每个 monitor 主模型最近 monitorTimelineMaxPoints 条历史。
|
||||
func (s *ChannelMonitorService) batchTimeline(
|
||||
ctx context.Context,
|
||||
ids []int64,
|
||||
primaryByID map[int64]string,
|
||||
) map[int64][]*ChannelMonitorHistoryEntry {
|
||||
timelineMap, err := s.repo.ListRecentHistoryForMonitors(ctx, ids, primaryByID, monitorTimelineMaxPoints)
|
||||
if err != nil {
|
||||
slog.Warn("channel_monitor: user view batch timeline failed", "error", err)
|
||||
return map[int64][]*ChannelMonitorHistoryEntry{}
|
||||
}
|
||||
return timelineMap
|
||||
}
|
||||
|
||||
// pickLatest 从 latest 切片中挑出指定 model 对应项,未命中返回 nil。
|
||||
func pickLatest(rows []*ChannelMonitorLatest, model string) *ChannelMonitorLatest {
|
||||
if model == "" {
|
||||
return nil
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.Model == model {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserDetail 用户只读视图:单个监控详情(每个模型 7d/15d/30d 可用率与平均延迟)。
|
||||
@@ -170,9 +220,15 @@ func buildStatusSummary(
|
||||
return summary
|
||||
}
|
||||
|
||||
// buildUserViewFromSummary 用预聚合好的 MonitorStatusSummary 装填 UserMonitorView(无 IO)。
|
||||
func buildUserViewFromSummary(m *ChannelMonitor, summary MonitorStatusSummary) *UserMonitorView {
|
||||
return &UserMonitorView{
|
||||
// buildUserViewFromSummary 用预聚合好的 MonitorStatusSummary + 主模型 latest + timeline 装填 UserMonitorView(无 IO)。
|
||||
// primaryLatest 可能为 nil(该监控尚无历史);timelineEntries 可能为空。
|
||||
func buildUserViewFromSummary(
|
||||
m *ChannelMonitor,
|
||||
summary MonitorStatusSummary,
|
||||
primaryLatest *ChannelMonitorLatest,
|
||||
timelineEntries []*ChannelMonitorHistoryEntry,
|
||||
) *UserMonitorView {
|
||||
view := &UserMonitorView{
|
||||
ID: m.ID,
|
||||
Name: m.Name,
|
||||
Provider: m.Provider,
|
||||
@@ -182,7 +238,26 @@ func buildUserViewFromSummary(m *ChannelMonitor, summary MonitorStatusSummary) *
|
||||
PrimaryLatencyMs: summary.PrimaryLatencyMs,
|
||||
Availability7d: summary.Availability7d,
|
||||
ExtraModels: summary.ExtraModels,
|
||||
Timeline: buildTimelinePoints(timelineEntries),
|
||||
}
|
||||
if primaryLatest != nil {
|
||||
view.PrimaryPingLatencyMs = primaryLatest.PingLatencyMs
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// buildTimelinePoints 把 history entry 裁剪为 timeline 点(去除 message/ID/Model,减小响应体)。
|
||||
func buildTimelinePoints(entries []*ChannelMonitorHistoryEntry) []UserMonitorTimelinePoint {
|
||||
out := make([]UserMonitorTimelinePoint, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
out = append(out, UserMonitorTimelinePoint{
|
||||
Status: e.Status,
|
||||
LatencyMs: e.LatencyMs,
|
||||
PingLatencyMs: e.PingLatencyMs,
|
||||
CheckedAt: e.CheckedAt,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// mergeModelDetails 合并 latest + availability 三个窗口为 ModelDetail 列表。
|
||||
|
||||
@@ -65,6 +65,9 @@ const (
|
||||
// MonitorHistoryMaxLimit 历史查询最大返回条数(handler 层共享)。
|
||||
MonitorHistoryMaxLimit = 1000
|
||||
|
||||
// monitorTimelineMaxPoints 用户视图 timeline 每个监控最多返回的历史点数。
|
||||
monitorTimelineMaxPoints = 60
|
||||
|
||||
// monitorEndpointResolveTimeout validateEndpoint 解析 hostname 的最长耗时。
|
||||
monitorEndpointResolveTimeout = 5 * time.Second
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ type ChannelMonitorRepository interface {
|
||||
// 批量聚合(admin/user list 用,避免 N+1)
|
||||
ListLatestForMonitorIDs(ctx context.Context, ids []int64) (map[int64][]*ChannelMonitorLatest, error)
|
||||
ComputeAvailabilityForMonitors(ctx context.Context, ids []int64, windowDays int) (map[int64][]*ChannelMonitorAvailability, error)
|
||||
// ListRecentHistoryForMonitors 批量取多个 monitor 各自主模型(primaryModels[monitorID])最近 perMonitorLimit 条历史。
|
||||
// 返回的 entry 已按 checked_at DESC 排序(最新在前),不含 message 字段。
|
||||
ListRecentHistoryForMonitors(ctx context.Context, ids []int64, primaryModels map[int64]string, perMonitorLimit int) (map[int64][]*ChannelMonitorHistoryEntry, error)
|
||||
}
|
||||
|
||||
// ChannelMonitorService 渠道监控管理服务。
|
||||
|
||||
@@ -72,15 +72,25 @@ type CheckResult struct {
|
||||
|
||||
// UserMonitorView 用户只读视图:监控概览(含主模型最近状态 + 7d 可用率 + 附加模型最近状态)。
|
||||
type UserMonitorView struct {
|
||||
ID int64
|
||||
Name string
|
||||
Provider string
|
||||
GroupName string
|
||||
PrimaryModel string
|
||||
PrimaryStatus string
|
||||
PrimaryLatencyMs *int
|
||||
Availability7d float64 // 0-100
|
||||
ExtraModels []ExtraModelStatus
|
||||
ID int64
|
||||
Name string
|
||||
Provider string
|
||||
GroupName string
|
||||
PrimaryModel string
|
||||
PrimaryStatus string
|
||||
PrimaryLatencyMs *int
|
||||
PrimaryPingLatencyMs *int // 主模型最近一次 ping 延迟
|
||||
Availability7d float64 // 0-100
|
||||
ExtraModels []ExtraModelStatus
|
||||
Timeline []UserMonitorTimelinePoint // 主模型最近 N 个历史点(按 checked_at DESC,最新在前)
|
||||
}
|
||||
|
||||
// UserMonitorTimelinePoint 用户视图 timeline 单点数据(去除 message 以减小响应体)。
|
||||
type UserMonitorTimelinePoint struct {
|
||||
Status string `json:"status"`
|
||||
LatencyMs *int `json:"latency_ms"`
|
||||
PingLatencyMs *int `json:"ping_latency_ms"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
|
||||
// ExtraModelStatus 附加模型最近一次状态。
|
||||
@@ -134,10 +144,11 @@ type ChannelMonitorHistoryEntry struct {
|
||||
|
||||
// ChannelMonitorLatest 最近一次检测的简明信息(用于 UserMonitorView 聚合)。
|
||||
type ChannelMonitorLatest struct {
|
||||
Model string
|
||||
Status string
|
||||
LatencyMs *int
|
||||
CheckedAt time.Time
|
||||
Model string
|
||||
Status string
|
||||
LatencyMs *int
|
||||
PingLatencyMs *int
|
||||
CheckedAt time.Time
|
||||
}
|
||||
|
||||
// ChannelMonitorAvailability 单个模型在某窗口内的可用率与平均延迟(用于 UserMonitorDetail 聚合)。
|
||||
|
||||
Reference in New Issue
Block a user