feat: build vehicle data platform and production pipeline
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user