feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -3,6 +3,85 @@ import { api } from './client';
afterEach(() => {
vi.restoreAllMocks();
window.sessionStorage.clear();
});
test('authenticated requests use the session-only bearer token', async () => {
window.sessionStorage.setItem('vehicle-platform.access-token', 'operator-secret-token');
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { name: 'operator-a', role: 'operator', authMode: 'enforce' } }) } as Response);
await api.session();
const [, init] = fetchMock.mock.calls[0];
expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer operator-secret-token');
expect(window.localStorage.getItem('vehicle-platform.access-token')).toBeNull();
});
test('durable alert APIs keep versioned actions, rules and notification reads explicit', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-alert', timestamp: 1 }) } as Response);
await api.alertEventsV2({ status: 'unprocessed', limit: 20, offset: 0 });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'unprocessed', limit: 20, offset: 0 }) });
await api.actOnAlertV2('alert 1', { version: 2, action: 'acknowledge', note: '已确认' });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/events/alert%201/actions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 2, action: 'acknowledge', note: '已确认' }) });
await api.readAlertNotificationsV2([7, 8]);
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/alerts/notifications/read', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [7, 8] }) });
});
test('access APIs post one shared filter contract and version threshold updates', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ data: { items: [], total: 0, limit: 50, offset: 0 }, traceId: 'trace-access', timestamp: 1783094400000 })
} as Response);
const query = { protocol: 'JT808', onlineState: 'offline', limit: 50, offset: 0 };
await api.accessVehicles(query);
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/vehicles', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
});
const unresolved = { protocol: 'JT808', limit: 20, offset: 0 };
await api.accessUnresolvedIdentities(unresolved);
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/unresolved-identities', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(unresolved)
});
await api.updateAccessThresholds({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/thresholds', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] })
});
});
test('vehicle profile API uses encoded VIN and optimistic version updates', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-profile', timestamp: 1 }) } as Response);
const input = { modelName: '氢燃料重卡', vehicleType: '重卡', companyName: '示范物流', operationStatus: 'active' as const, accessProvider: '车厂平台', firstAccessAt: '2026-07-01T08:30', runtimeSeconds: 3600, version: 2 };
await api.updateVehicleProfile('VIN 001', input);
expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicles/VIN%20001/profile', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
});
});
test('latest telemetry uses the encoded vehicle identity path', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { categories: [], values: [] }, traceId: 'trace-telemetry', timestamp: 1 }) } as Response);
await api.latestTelemetry('粤A 001');
expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicles/%E7%B2%A4A%20001/telemetry/latest', undefined);
});
test('vehicle profile sync posts an explicit dry-run and conflict policy contract', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {}, traceId: 'trace-profile-sync', timestamp: 1 }) } as Response);
const input = {
sourceSystem: 'oem-tsp', sourceVersion: 'snapshot-1', conflictPolicy: 'preserve' as const, dryRun: true,
items: [{ vin: 'VIN001', modelName: '车型一', vehicleType: '重卡', companyName: '示范物流', operationStatus: 'active' as const, accessProvider: '车厂平台', firstAccessAt: '', runtimeSeconds: null }]
};
await api.syncVehicleProfiles(input);
expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicle-profiles/sync', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
});
});
test('trackPlayback preserves the bounded V2 query contract', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ data: { vin: 'VIN001', points: [], events: [], sources: [], summary: { pointCount: 0 } }, traceId: 'trace-test', timestamp: 1783094400000 })
} as Response);
await api.trackPlayback(new URLSearchParams({ keyword: '粤AG18312', maxPoints: '1200' }));
expect(fetchMock).toHaveBeenCalledWith('/api/v2/tracks?keyword=%E7%B2%A4AG18312&maxPoints=1200', undefined);
});
test('rawFramesQuery posts structured JSON instead of URL query strings', async () => {

View File

@@ -1,10 +1,33 @@
import type {
ApiEnvelope,
AccessQuery,
AccessSummary,
AccessThresholdConfig,
AccessThresholdUpdate,
AccessUnresolvedIdentity,
AccessUnresolvedIdentityQuery,
AccessVehicleRow,
AlertAction,
AlertEvent,
AlertNotification,
AlertQuery,
AlertRule,
AlertRuleInput,
AlertSummary,
DailyMileageRow,
DashboardSummary,
HistoryLocationRow,
HistoryDataResponse,
HistorySeriesResponse,
HistoryExportJob,
HistoryExportRequest,
HistoryMetricCatalog,
MetricCatalog,
LatestTelemetryResponse,
MileageSummary,
MapReverseGeocode,
MonitorMapResponse,
MonitorSummary,
OnlineStatisticsSummary,
OnlineVehicleStatusRow,
OpsHealth,
@@ -15,15 +38,22 @@ import type {
RawFrameRow,
RealtimeLocationRow,
SourceReadinessPlan,
SessionInfo,
TrackPlaybackResponse,
VehicleRealtimeRow,
VehicleCoverageRow,
VehicleCoverageSummary,
VehicleDetail,
VehicleProfile,
VehicleProfileInput,
VehicleProfileSyncRequest,
VehicleProfileSyncResult,
VehicleIdentityResolution,
VehicleServiceOverview,
VehicleServiceSummary,
VehicleRow
} from './types';
import { getAccessToken } from '../v2/auth/session';
export type RawFrameQuery = {
keyword?: string;
@@ -54,7 +84,9 @@ type ApiErrorEnvelope = {
};
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(path, init);
const token = getAccessToken();
const requestInit = token ? { ...init, headers: withAuthorization(init?.headers, token) } : init;
const response = await fetch(path, requestInit);
if (!response.ok) {
throw new Error(await responseErrorMessage(response));
}
@@ -62,6 +94,12 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
return envelope.data;
}
function withAuthorization(headers: HeadersInit | undefined, token: string) {
const authorized = new Headers(headers);
authorized.set('Authorization', `Bearer ${token}`);
return authorized;
}
async function responseErrorMessage(response: Response) {
try {
const envelope = (await response.json()) as ApiErrorEnvelope;
@@ -85,12 +123,66 @@ function withTraceID(message: string, traceID?: string) {
}
export const api = {
session: () => request<SessionInfo>('/api/v2/session'),
monitorSummary: (params = new URLSearchParams()) => request<MonitorSummary>(`/api/v2/monitor/summary?${params.toString()}`),
monitorMap: (params = new URLSearchParams()) => request<MonitorMapResponse>(`/api/v2/monitor/map?${params.toString()}`),
trackPlayback: (params = new URLSearchParams()) => request<TrackPlaybackResponse>(`/api/v2/tracks?${params.toString()}`),
metricCatalog: () => request<MetricCatalog>('/api/v2/metrics'),
historyMetricCatalog: () => request<HistoryMetricCatalog>('/api/v2/history/metrics'),
historyData: (params = new URLSearchParams()) => request<HistoryDataResponse>(`/api/v2/history/query?${params.toString()}`),
historySeries: (params = new URLSearchParams()) => request<HistorySeriesResponse>(`/api/v2/history/series?${params.toString()}`),
historyExports: () => request<HistoryExportJob[]>('/api/v2/exports'),
createHistoryExport: (query: HistoryExportRequest) => request<HistoryExportJob>('/api/v2/exports', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
accessSummary: (query: AccessQuery) => request<AccessSummary>('/api/v2/access/summary', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
accessVehicles: (query: AccessQuery) => request<Page<AccessVehicleRow>>('/api/v2/access/vehicles', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
accessUnresolvedIdentities: (query: AccessUnresolvedIdentityQuery) => request<Page<AccessUnresolvedIdentity>>('/api/v2/access/unresolved-identities', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
accessThresholds: () => request<AccessThresholdConfig>('/api/v2/access/thresholds'),
updateAccessThresholds: (update: AccessThresholdUpdate) => request<AccessThresholdConfig>('/api/v2/access/thresholds', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(update)
}),
alertSummaryV2: (query: AlertQuery) => request<AlertSummary>('/api/v2/alerts/summary', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
alertEventsV2: (query: AlertQuery) => request<Page<AlertEvent>>('/api/v2/alerts/events', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
alertEventV2: (id: string) => request<AlertEvent>(`/api/v2/alerts/events/${encodeURIComponent(id)}`),
actOnAlertV2: (id: string, action: Pick<AlertAction, 'action' | 'note'> & { version: number; actor?: string }) => request<AlertEvent>(`/api/v2/alerts/events/${encodeURIComponent(id)}/actions`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(action)
}),
alertRulesV2: () => request<AlertRule[]>('/api/v2/alerts/rules'),
saveAlertRuleV2: (input: AlertRuleInput) => request<AlertRule>(input.version > 0 ? `/api/v2/alerts/rules/${encodeURIComponent(input.id)}` : '/api/v2/alerts/rules', {
method: input.version > 0 ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
setAlertRuleEnabledV2: (id: string, update: { version: number; enabled: boolean; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/enabled`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(update)
}),
alertNotificationsV2: (params = new URLSearchParams()) => request<Page<AlertNotification>>(`/api/v2/alerts/notifications?${params.toString()}`),
readAlertNotificationsV2: (ids: number[]) => request<{ updated: number }>('/api/v2/alerts/notifications/read', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids })
}),
dashboardSummary: () => request<DashboardSummary>('/api/dashboard/summary'),
vehicles: (params = new URLSearchParams()) => request<Page<VehicleRow>>(`/api/vehicles?${params.toString()}`),
vehicleResolve: (params = new URLSearchParams()) => request<VehicleIdentityResolution>(`/api/vehicles/resolve?${params.toString()}`),
vehicleCoverage: (params = new URLSearchParams()) => request<Page<VehicleCoverageRow>>(`/api/vehicles/coverage?${params.toString()}`),
vehicleCoverageSummary: (params = new URLSearchParams()) => request<VehicleCoverageSummary>(`/api/vehicles/coverage/summary?${params.toString()}`),
vehicleDetail: (params = new URLSearchParams()) => request<VehicleDetail>(`/api/vehicle-service?${params.toString()}`),
vehicleProfile: (vin: string) => request<VehicleProfile>(`/api/v2/vehicles/${encodeURIComponent(vin)}/profile`),
latestTelemetry: (vin: string) => request<LatestTelemetryResponse>(`/api/v2/vehicles/${encodeURIComponent(vin)}/telemetry/latest`),
updateVehicleProfile: (vin: string, input: VehicleProfileInput) => request<VehicleProfile>(`/api/v2/vehicles/${encodeURIComponent(vin)}/profile`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
syncVehicleProfiles: (input: VehicleProfileSyncRequest) => request<VehicleProfileSyncResult>('/api/v2/vehicle-profiles/sync', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
vehicleServiceSummary: () => request<VehicleServiceSummary>('/api/vehicle-service/summary'),
vehicleServiceOverview: (params = new URLSearchParams()) => request<VehicleServiceOverview>(`/api/vehicle-service/overview?${params.toString()}`),
vehicleServiceOverviews: (query: VehicleOverviewBatchQuery) => request<Page<VehicleServiceOverview>>('/api/vehicle-service/overviews', {

View File

@@ -4,6 +4,247 @@ export interface ApiEnvelope<T> {
timestamp: number;
}
export interface SessionInfo {
name: string;
role: 'viewer' | 'operator' | 'admin';
authMode: 'disabled' | 'enforce';
}
export interface MonitorSummary {
totalVehicles: number;
onlineVehicles: number;
offlineVehicles: number;
drivingVehicles: number;
idleVehicles: number;
alertVehicles: number;
unknownVehicles: number;
activeToday: number;
frameToday: number;
alertDataAvailable: boolean;
truncated: boolean;
asOf: string;
}
export interface MonitorMapPoint {
vin: string;
plate: string;
protocol: string;
protocols: string[];
longitude: number;
latitude: number;
speedKmh: number;
socPercent: number;
totalMileageKm: number;
lastSeen: string;
status: 'driving' | 'idle' | 'offline' | 'unknown';
}
export interface MonitorMapCluster {
id: string;
longitude: number;
latitude: number;
count: number;
online: number;
offline: number;
driving: number;
idle: number;
unknown: number;
}
export interface MonitorMapResponse {
mode: 'clusters' | 'mixed' | 'points';
zoom: number;
total: number;
truncated: boolean;
points: MonitorMapPoint[];
clusters: MonitorMapCluster[];
asOf: string;
}
export interface TrackPlaybackResponse {
vin: string;
plate: string;
points: HistoryLocationRow[];
events: TrackPlaybackEvent[];
sources: TrackPlaybackSource[];
segments: TrackSegment[];
stops: TrackStop[];
summary: TrackPlaybackSummary;
coverage: TrackCoverage;
quality: TrackQuality;
total: number;
truncated: boolean;
sampled: boolean;
asOf: string;
}
export interface TrackPlaybackSummary {
startTime: string;
endTime: string;
distanceKm: number;
durationSeconds: number;
averageSpeedKmh: number;
maximumSpeedKmh: number;
pointCount: number;
movingSeconds: number;
stoppedSeconds: number;
stopCount: number;
segmentCount: number;
}
export interface TrackPlaybackEvent {
index: number;
sampledIndex: number;
type: 'start' | 'end' | 'stop' | 'acceleration' | 'braking' | 'source_switch' | string;
title: string;
time: string;
speedKmh: number;
socPercent: number;
socAvailable: boolean;
directionDeg?: number;
alarmFlag?: number;
longitude: number;
latitude: number;
}
export interface TrackPlaybackSource {
protocol: string;
pointCount: number;
startTime: string;
endTime: string;
}
export interface TrackCoverage {
requestedStart: string;
requestedEnd: string;
actualStart: string;
actualEnd: string;
totalPoints: number;
fetchedPoints: number;
processedPoints: number;
returnedPoints: number;
complete: boolean;
limitReasons: string[];
evidence: string;
}
export interface TrackSegment {
index: number;
type: 'moving' | 'stopped' | 'gap' | 'point' | string;
title: string;
startTime: string;
endTime: string;
durationSeconds: number;
distanceKm: number;
pointCount: number;
startIndex: number;
endIndex: number;
sampledStartIndex: number;
sampledEndIndex: number;
}
export interface TrackStop {
index: number;
startTime: string;
endTime: string;
durationSeconds: number;
pointCount: number;
longitude: number;
latitude: number;
sampledIndex: number;
evidence: string;
}
export interface TrackQuality {
status: 'good' | 'warning' | string;
selectedProtocol: string;
rawPoints: number;
validPoints: number;
alternateSourcePoints: number;
invalidCoordinatePoints: number;
duplicatePoints: number;
driftPoints: number;
sourceSwitches: number;
largeGapCount: number;
maximumGapSeconds: number;
evidence: string;
}
export interface HistoryMetricCatalog {
categories: HistoryDataCategory[];
metrics: HistoryMetricDefinition[];
}
export interface MetricCatalog { metrics: MetricDefinition[]; asOf: string; }
export interface MetricDefinition {
key: string; label: string; description: string; unit: string; category: string; valueType: 'numeric' | 'boolean';
protocols: string[]; sourceFields: Record<string, string>; searchable: boolean; chartable: boolean; alertable: boolean;
}
export interface HistoryDataCategory { key: 'location' | 'raw' | 'mileage' | string; label: string; }
export interface HistoryMetricDefinition { key: string; label: string; unit: string; category: string; valueType: string; defaultVisible: boolean; }
export interface HistoryDataRow { id: string; vin: string; plate: string; protocol: string; deviceTime: string; serverTime: string; quality: string; evidenceId?: string; values: Record<string, unknown>; }
export interface HistoryDataSummary { resultRows: number; vehicleCount: number; sources: string[]; queryDurationMs: number; }
export interface HistoryDataResponse { category: string; columns: HistoryMetricDefinition[]; rows: HistoryDataRow[]; summary: HistoryDataSummary; total: number; limit: number; offset: number; asOf: string; }
export interface HistorySeriesPoint { time: string; value: number | null; min: number | null; max: number | null; count: number; }
export interface HistorySeries { vin: string; plate: string; protocol: string; metric: string; label: string; unit: string; aggregation: 'avg' | 'last' | string; points: HistorySeriesPoint[]; }
export interface HistorySeriesSummary { rawPointCount: number; bucketCount: number; returnedPointCount: number; seriesCount: number; grainSeconds: number; targetPoints: number; expectedBucketCount: number; missingBucketCount: number; queryDurationMs: number; complete: boolean; evidence: string; }
export interface HistorySeriesResponse { metrics: HistoryMetricDefinition[]; series: HistorySeries[]; summary: HistorySeriesSummary; dateFrom: string; dateTo: string; asOf: string; }
export interface HistoryExportRequest { keywords: string[]; category: string; protocol?: string; dateFrom?: string; dateTo?: string; metrics: string[]; format: 'csv'; }
export interface HistoryExportJob { id: string; name: string; status: 'queued' | 'running' | 'completed' | 'failed'; progress: number; format: string; category: string; keywords: string[]; rowCount: number; totalRows: number; processedRows: number; fileSizeBytes: number; error?: string; downloadUrl?: string; createdAt: string; updatedAt: string; completedAt?: string; evidence: string; }
export interface AccessQuery { keyword?: string; protocol?: string; oem?: string; model?: string; provider?: string; firstSeenFrom?: string; firstSeenTo?: string; latestSeenFrom?: string; latestSeenTo?: string; onlineState?: string; delayState?: string; limit?: number; offset?: number; }
export interface AccessUnresolvedIdentityQuery { keyword?: string; protocol?: string; limit?: number; offset?: number; }
export interface AccessUnresolvedIdentity {
id: string; protocol: string; identifierMasked: string; plate: string; manufacturer: string; sourceEndpoint: string;
firstRegisteredAt: string; latestRegisteredAt: string; latestAuthenticatedAt: string; latestSeenAt: string;
freshnessSec: number; issueCode: string; recommendedAction: string;
}
export interface AccessVehicleRow {
vin: string; plate: string; oem: string; model: string; company: string; protocol: string; provider: string; source: string;
firstSeenAt: string; latestEventAt: string; latestReceivedAt: string; reportIntervalSec: number | null;
dataDelaySec: number | null; freshnessSec: number | null; onlineState: 'online' | 'offline' | 'never_reported' | 'unknown';
thresholdSec: number; latestMessageType: string; latestEventId: string; latestError: string; delayAbnormal: boolean;
firstSeenEvidence: string; firstSeenSource: string; reportIntervalEvidence: string; reportSampleCount: number;
}
export interface AccessDistribution { name: string; total: number; online: number; onlineRate: number; }
export interface AccessSummary {
totalVehicles: number; onlineVehicles: number; offlineVehicles: number; longOfflineVehicles: number;
neverReported: number; unknownVehicles: number; delayAbnormal: number; reportedToday: number; onlineRate: number;
protocols: AccessDistribution[]; oems: AccessDistribution[]; asOf: string; thresholdVersion: number;
}
export interface AccessProtocolThreshold { protocol: string; thresholdSec: number; }
export interface AccessThresholdAudit { version: number; actor: string; changedAt: string; summary: string; }
export interface AccessThresholdConfig {
version: number; defaultThresholdSec: number; delayThresholdSec: number; longOfflineSec: number;
protocols: AccessProtocolThreshold[]; updatedBy: string; updatedAt: string; audit: AccessThresholdAudit[];
}
export interface AccessThresholdUpdate {
version: number; defaultThresholdSec: number; delayThresholdSec: number; longOfflineSec: number;
protocols: AccessProtocolThreshold[]; actor?: string;
}
export type AlertSeverity = 'critical' | 'major' | 'minor';
export type AlertStatus = 'unprocessed' | 'processing' | 'recovered' | 'closed' | 'ignored';
export interface AlertQuery { keyword?: string; severity?: string; status?: string; ruleId?: string; protocol?: string; dateFrom?: string; dateTo?: string; limit?: number; offset?: number; }
export interface AlertSummary { active: number; unprocessed: number; processing: number; recovered: number; closed: number; ignored: number; unreadNotifications: number; asOf: string; }
export interface AlertAction { id: number; action: string; fromStatus: string; toStatus: string; actor: string; note: string; createdAt: string; }
export interface AlertEvent {
id: string; ruleId: string; ruleName: string; ruleVersion: number; severity: AlertSeverity; status: AlertStatus;
vin: string; plate: string; protocol: string; metric: string; operator: string; triggerValue: number; threshold: number; thresholdHigh: number;
unit: string; durationSec: number; location: string; longitude?: number; latitude?: number; sourceEventId: string;
eventAt: string; receivedAt: string; triggeredAt: string; recoveredAt: string; handler: string; version: number; actions?: AlertAction[];
}
export interface AlertRule {
id: string; name: string; description: string; severity: AlertSeverity; valueType: 'numeric' | 'boolean'; metric: string;
operator: string; threshold: number; thresholdHigh: number; booleanThreshold?: boolean; durationSec: number; recoveryOperator: string;
recoveryThreshold: number; repeatIntervalSec: number; scopeProtocols: string[]; scopeVins: string[]; scopeOems: string[]; scopeModels: string[]; scopeCompanies: string[];
notificationChannels: string[]; enabled: boolean; version: number; createdBy: string; updatedBy: string; createdAt: string; updatedAt: string;
}
export interface AlertRuleInput extends Omit<AlertRule, 'createdBy' | 'updatedBy' | 'createdAt' | 'updatedAt'> { actor?: string; }
export interface AlertNotification { id: number; eventId: string; title: string; content: string; severity: AlertSeverity; channel: string; read: boolean; createdAt: string; readAt: string; }
export interface ProtocolStat {
protocol: string;
online: number;
@@ -94,6 +335,7 @@ export interface VehicleDetail {
lookupResolved: boolean;
resolution?: VehicleIdentityResolution;
identity?: VehicleRow;
profile?: VehicleProfile;
realtimeSummary?: VehicleRealtimeRow;
serviceStatus?: VehicleServiceStatus;
serviceOverview?: VehicleServiceOverview;
@@ -107,6 +349,76 @@ export interface VehicleDetail {
quality: Page<QualityIssueRow>;
}
export interface VehicleProfile {
vin: string;
modelName: string;
vehicleType: string;
companyName: string;
operationStatus: 'unknown' | 'active' | 'inactive' | 'maintenance' | 'retired';
accessProvider: string;
firstAccessAt: string;
runtimeSeconds: number | null;
sourceSystem: string;
sourceVersion: string;
syncedAt: string;
version: number;
updatedBy: string;
updatedAt: string;
completeness: number;
missingFields: string[];
}
export interface VehicleProfileInput {
modelName: string;
vehicleType: string;
companyName: string;
operationStatus: VehicleProfile['operationStatus'];
accessProvider: string;
firstAccessAt: string;
runtimeSeconds: number | null;
version: number;
}
export interface VehicleProfileSyncItem {
vin: string;
modelName: string;
vehicleType: string;
companyName: string;
operationStatus: VehicleProfile['operationStatus'];
accessProvider: string;
firstAccessAt: string;
runtimeSeconds: number | null;
}
export interface VehicleProfileSyncRequest {
sourceSystem: string;
sourceVersion: string;
conflictPolicy: 'preserve' | 'overwrite';
dryRun: boolean;
items: VehicleProfileSyncItem[];
}
export interface VehicleProfileSyncItemResult {
vin: string;
status: 'created' | 'updated' | 'unchanged' | 'conflict_source' | 'conflict_source_version' | 'missing_vehicle';
previousSource?: string;
previousVersion?: string;
profileVersion?: number;
}
export interface VehicleProfileSyncResult {
sourceSystem: string;
sourceVersion: string;
dryRun: boolean;
received: number;
created: number;
updated: number;
unchanged: number;
conflicted: number;
missing: number;
items: VehicleProfileSyncItemResult[];
}
export interface VehicleSourceConsistency {
sourceCount: number;
onlineSourceCount: number;
@@ -220,6 +532,9 @@ export interface VehicleRealtimeRow {
}
export interface HistoryLocationRow extends RealtimeLocationRow {
socAvailable: boolean;
directionDeg?: number;
alarmFlag?: number;
deviceTime: string;
serverTime: string;
}
@@ -233,9 +548,24 @@ export interface RawFrameRow {
deviceTime: string;
serverTime: string;
rawSizeBytes: number;
parseStatus?: string;
parseError?: string;
sourceEndpoint?: string;
parsedFields?: Record<string, unknown>;
}
export interface LatestTelemetryCategory { key: string; label: string; count: number; }
export interface LatestTelemetryValue {
key: string; sourceField: string; label: string; description?: string; unit: string; category: string;
valueType: string; value: unknown; protocol: string; sourceEndpoint?: string; frameId: string;
deviceTime: string; serverTime: string; quality: 'good' | 'stale' | 'warning'; qualityReason: string;
freshnessSeconds: number; dataDelaySeconds?: number;
}
export interface LatestTelemetryResponse {
vin: string; categories: LatestTelemetryCategory[]; values: LatestTelemetryValue[]; asOf: string;
staleAfterSeconds: number; scannedFrames: number; evidence: string;
}
export interface DailyMileageRow {
vin: string;
plate: string;
@@ -401,6 +731,7 @@ export interface CapacityMetrics {
}
export interface RuntimeInfo {
dataMode?: 'mock' | 'production' | string;
requestTimeoutMs: number;
amapWebJsConfigured?: boolean;
amapApiConfigured?: boolean;