feat(platform): expose service status on vehicle rows
This commit is contained in:
@@ -35,6 +35,18 @@ func TestHandlerVehicles(t *testing.T) {
|
||||
if !strings.Contains(rec.Body.String(), "LB9A32A24R0LS1426") {
|
||||
t.Fatalf("response missing vehicle: %s", rec.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Data Page[VehicleRow] `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String())
|
||||
}
|
||||
if len(body.Data.Items) == 0 || body.Data.Items[0].ServiceStatus == nil {
|
||||
t.Fatalf("vehicle row should include canonical vehicle service status: %s", rec.Body.String())
|
||||
}
|
||||
if body.Data.Items[0].ServiceStatus.Status != "degraded" {
|
||||
t.Fatalf("vehicle row should expose degraded status, got %+v", body.Data.Items[0].ServiceStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerVehicleCoverage(t *testing.T) {
|
||||
|
||||
@@ -61,9 +61,43 @@ func (m *MockStore) DashboardSummary(context.Context) (DashboardSummary, error)
|
||||
|
||||
func (m *MockStore) Vehicles(_ context.Context, query url.Values) (Page[VehicleRow], error) {
|
||||
items := filterVehicles(m.vehicles, query)
|
||||
for index := range items {
|
||||
items[index].ServiceStatus = m.vehicleRowServiceStatus(items[index])
|
||||
}
|
||||
return page(items, query), nil
|
||||
}
|
||||
|
||||
func (m *MockStore) vehicleRowServiceStatus(row VehicleRow) *VehicleServiceStatus {
|
||||
status := VehicleCoverageRow{
|
||||
VIN: row.VIN,
|
||||
Plate: row.Plate,
|
||||
Phone: row.Phone,
|
||||
OEM: row.OEM,
|
||||
LastSeen: row.LastSeen,
|
||||
BindingStatus: "bound",
|
||||
}
|
||||
if strings.TrimSpace(row.VIN) == "" {
|
||||
status.BindingStatus = "unbound"
|
||||
}
|
||||
for _, vehicle := range m.vehicles {
|
||||
if vehicle.VIN != row.VIN {
|
||||
continue
|
||||
}
|
||||
if !containsString(status.Protocols, vehicle.Protocol) {
|
||||
status.Protocols = append(status.Protocols, vehicle.Protocol)
|
||||
status.SourceCount = len(status.Protocols)
|
||||
}
|
||||
if vehicle.Online {
|
||||
status.Online = true
|
||||
status.OnlineSourceCount++
|
||||
}
|
||||
if vehicle.LastSeen > status.LastSeen {
|
||||
status.LastSeen = vehicle.LastSeen
|
||||
}
|
||||
}
|
||||
return buildVehicleCoverageServiceStatus(status)
|
||||
}
|
||||
|
||||
func (m *MockStore) VehicleCoverage(_ context.Context, query url.Values) (Page[VehicleCoverageRow], error) {
|
||||
vehicles := filterVehicles(m.vehicles, query)
|
||||
byVIN := map[string]*VehicleCoverageRow{}
|
||||
|
||||
@@ -37,15 +37,16 @@ type LinkHealth struct {
|
||||
}
|
||||
|
||||
type VehicleRow struct {
|
||||
VIN string `json:"vin"`
|
||||
Plate string `json:"plate"`
|
||||
Phone string `json:"phone"`
|
||||
OEM string `json:"oem"`
|
||||
Protocol string `json:"protocol"`
|
||||
Online bool `json:"online"`
|
||||
LastSeen string `json:"lastSeen"`
|
||||
LocationText string `json:"locationText"`
|
||||
BindingScore int `json:"bindingScore"`
|
||||
VIN string `json:"vin"`
|
||||
Plate string `json:"plate"`
|
||||
Phone string `json:"phone"`
|
||||
OEM string `json:"oem"`
|
||||
Protocol string `json:"protocol"`
|
||||
Online bool `json:"online"`
|
||||
LastSeen string `json:"lastSeen"`
|
||||
LocationText string `json:"locationText"`
|
||||
BindingScore int `json:"bindingScore"`
|
||||
ServiceStatus *VehicleServiceStatus `json:"serviceStatus,omitempty"`
|
||||
}
|
||||
|
||||
type VehicleCoverageRow struct {
|
||||
|
||||
@@ -32,13 +32,20 @@ func buildVehicleListSQL(query url.Values) SQLQuery {
|
||||
fromSQL := `FROM vehicle_realtime_snapshot s ` +
|
||||
`LEFT JOIN vehicle_identity_binding b ON b.vin = s.vin ` +
|
||||
`LEFT JOIN vehicle_realtime_location l ON l.vin = s.vin AND l.protocol = s.protocol ` +
|
||||
`LEFT JOIN (` +
|
||||
`SELECT vin, COUNT(DISTINCT protocol) AS source_count, ` +
|
||||
`COUNT(DISTINCT CASE WHEN updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN protocol END) AS online_source_count ` +
|
||||
`FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> '' GROUP BY vin` +
|
||||
`) vs ON vs.vin = s.vin ` +
|
||||
`WHERE ` + strings.Join(where, " AND ")
|
||||
return SQLQuery{
|
||||
Text: `SELECT s.vin, COALESCE(NULLIF(s.plate, ''), b.plate, '') AS plate, COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem, s.protocol, ` +
|
||||
`CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN 1 ELSE 0 END AS online, ` +
|
||||
`COALESCE(DATE_FORMAT(s.updated_at, '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` +
|
||||
`COALESCE(CONCAT(l.longitude, ',', l.latitude), '') AS location_text, ` +
|
||||
`CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 100 ELSE 60 END AS binding_score ` +
|
||||
`CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 100 ELSE 60 END AS binding_score, ` +
|
||||
`COALESCE(vs.source_count, 0) AS source_count, COALESCE(vs.online_source_count, 0) AS online_source_count, ` +
|
||||
`CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 'bound' ELSE 'unbound' END AS binding_status ` +
|
||||
fromSQL + ` ORDER BY s.updated_at DESC, s.vin ASC, s.protocol ASC LIMIT ? OFFSET ?`,
|
||||
Args: args,
|
||||
CountText: `SELECT COUNT(*) ` + fromSQL,
|
||||
|
||||
@@ -124,10 +124,25 @@ func (s *ProductionStore) Vehicles(ctx context.Context, query url.Values) (Page[
|
||||
for rows.Next() {
|
||||
var row VehicleRow
|
||||
var online int
|
||||
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &row.Protocol, &online, &row.LastSeen, &row.LocationText, &row.BindingScore); err != nil {
|
||||
var sourceCount int
|
||||
var onlineSourceCount int
|
||||
var bindingStatus string
|
||||
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &row.Protocol, &online, &row.LastSeen, &row.LocationText, &row.BindingScore, &sourceCount, &onlineSourceCount, &bindingStatus); err != nil {
|
||||
return Page[VehicleRow]{}, err
|
||||
}
|
||||
row.Online = online == 1
|
||||
row.ServiceStatus = buildVehicleCoverageServiceStatus(VehicleCoverageRow{
|
||||
VIN: row.VIN,
|
||||
Plate: row.Plate,
|
||||
Phone: row.Phone,
|
||||
OEM: row.OEM,
|
||||
Protocols: []string{row.Protocol},
|
||||
SourceCount: sourceCount,
|
||||
OnlineSourceCount: onlineSourceCount,
|
||||
Online: onlineSourceCount > 0,
|
||||
LastSeen: row.LastSeen,
|
||||
BindingStatus: bindingStatus,
|
||||
})
|
||||
items = append(items, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface VehicleRow {
|
||||
lastSeen: string;
|
||||
locationText: string;
|
||||
bindingScore: number;
|
||||
serviceStatus?: VehicleServiceStatus;
|
||||
}
|
||||
|
||||
export interface VehicleCoverageRow {
|
||||
|
||||
Reference in New Issue
Block a user