feat: expand vehicle data platform capabilities

This commit is contained in:
lingniu
2026-07-27 16:46:15 +08:00
parent e3a1f80f86
commit 3c4bece72c
650 changed files with 62155 additions and 2552 deletions
@@ -40,6 +40,93 @@ test('history export downloads use bearer authentication and retain the server f
expect(new Headers(init?.headers).get('Authorization')).toBe('Bearer customer-export-token');
});
test('reconciliation directory and current-filter export use dedicated scoped contracts', async () => {
const payload = new Blob(['quality'], { type: 'text/csv' });
const fetchMock = vi.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce({ ok: true, json: async () => ({ data: [] }) } as Response)
.mockResolvedValueOnce({
ok: true,
headers: new Headers({ 'X-Export-Name': encodeURIComponent('质量差异_20260723.csv') }),
blob: async () => payload
} as Response);
const controller = new AbortController();
await api.reconciliationAssignees('定位 运维', controller.signal);
const exported = await api.downloadReconciliationIssues({ status: 'active', owner: '定位运维组' }, controller.signal);
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/v2/reconciliation/assignees?search=%E5%AE%9A%E4%BD%8D+%E8%BF%90%E7%BB%B4', boundedRequest());
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/v2/reconciliation/issues/export', expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'active', owner: '定位运维组' }),
signal: expect.any(AbortSignal)
}));
expect(exported.filename).toBe('质量差异_20260723.csv');
expect(exported.blob.size).toBe(7);
});
test('history export lifecycle actions use explicit scoped endpoints', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { succeeded: [], skipped: [] } }) } as Response);
await api.rebuildHistoryExport('expired task');
await api.archiveHistoryExport('completed task');
await api.restoreHistoryExport('completed task');
await api.batchHistoryExports({ ids: ['completed task', 'failed-task'], action: 'archive' });
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/v2/exports/expired%20task/rebuild', boundedRequest({ method: 'POST' }));
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/v2/exports/completed%20task/archive', boundedRequest({ method: 'POST' }));
expect(fetchMock).toHaveBeenNthCalledWith(3, '/api/v2/exports/completed%20task/restore', boundedRequest({ method: 'POST' }));
expect(fetchMock).toHaveBeenNthCalledWith(4, '/api/v2/exports/batch', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: ['completed task', 'failed-task'], action: 'archive' })
}));
});
test('history cleanup automation uses versioned policy and approval endpoints', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: { policy: {}, runs: [] } }) } as Response);
await api.historyExportCleanupAutomation();
await api.updateHistoryExportCleanupAutomation({ enabled: true, olderThanDays: 180, intervalDays: 7, approvalWindowHours: 48, expectedRevision: 3 });
await api.approveHistoryExportCleanupAutomation('cleanup run', { expectedRevision: 4, reason: '已核对影响范围' });
await api.rejectHistoryExportCleanupAutomation('cleanup run', { expectedRevision: 5, reason: '需要延后复核' });
await api.retryHistoryExportCleanupAutomation('cleanup run', { expectedRevision: 6, reason: '故障已修复,恢复执行' });
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/v2/exports/cleanup/automation', boundedRequest());
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/v2/exports/cleanup/automation', boundedRequest({
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: true, olderThanDays: 180, intervalDays: 7, approvalWindowHours: 48, expectedRevision: 3 })
}));
expect(fetchMock).toHaveBeenNthCalledWith(3, '/api/v2/exports/cleanup/automation/cleanup%20run/approve', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expectedRevision: 4, reason: '已核对影响范围' })
}));
expect(fetchMock).toHaveBeenNthCalledWith(4, '/api/v2/exports/cleanup/automation/cleanup%20run/reject', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expectedRevision: 5, reason: '需要延后复核' })
}));
expect(fetchMock).toHaveBeenNthCalledWith(5, '/api/v2/exports/cleanup/automation/cleanup%20run/retry', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ expectedRevision: 6, reason: '故障已修复,恢复执行' })
}));
});
test('history export pagination and account preferences use durable v2 contracts', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {} }) } as Response);
const controller = new AbortController();
const params = new URLSearchParams({ search: 'VIN 001', status: 'failed', limit: '20', offset: '20' });
await api.historyExportPage(params, controller.signal);
await api.historyPreferences(controller.signal);
await api.updateHistoryPreferences({
retentionDays: 30,
fieldViews: [{ id: 'location:交付核验', name: '交付核验', category: 'location', keys: ['speedKmh'] }]
});
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/v2/exports/page?search=VIN+001&status=failed&limit=20&offset=20', boundedRequest());
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/v2/history/preferences', boundedRequest());
expect(fetchMock).toHaveBeenNthCalledWith(3, '/api/v2/history/preferences', boundedRequest({
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
retentionDays: 30,
fieldViews: [{ id: 'location:交付核验', name: '交付核验', category: 'location', keys: ['speedKmh'] }]
})
}));
});
test('a protected 401 terminates the client session but login validation errors stay local', async () => {
window.sessionStorage.setItem('vehicle-platform.access-token', 'expired-token');
const unauthorized = vi.fn();
@@ -124,6 +211,11 @@ test('access APIs post one shared filter contract and version threshold updates'
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(unresolved)
}));
await api.claimAccessIdentity('identity 1', { vin: 'LNXNEGRR7SR318212', note: '来源证据核对通过' });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/unresolved-identities/identity%201/claim', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vin: 'LNXNEGRR7SR318212', note: '来源证据核对通过' })
}));
await api.updateAccessThresholds({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] });
expect(fetchMock).toHaveBeenLastCalledWith('/api/v2/access/thresholds', boundedRequest({
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 3, defaultThresholdSec: 300, delayThresholdSec: 30, longOfflineSec: 1800, protocols: [{ protocol: 'JT808', thresholdSec: 60 }] })
@@ -147,13 +239,14 @@ test('latest telemetry uses the encoded vehicle identity path', async () => {
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 controller = new AbortController();
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);
await api.syncVehicleProfiles(input, controller.signal);
expect(fetchMock).toHaveBeenCalledWith('/api/v2/vehicle-profiles/sync', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), signal: controller.signal
}));
});
@@ -188,10 +281,17 @@ test('high-volume history, track, and mileage requests forward AbortSignal', asy
await api.trackPlayback(params, controller.signal);
await api.historyData(params, controller.signal);
await api.historySeries(params, controller.signal);
await api.dailyMileage(params, controller.signal);
await api.mileageStatistics(params, controller.signal);
const mileageQuery = { dateFrom: '2026-07-01', dateTo: '2026-07-31', vins: ['VIN001'], protocols: ['GB32960'] };
await api.dailyMileage(mileageQuery, controller.signal);
await api.mileageStatistics(mileageQuery, controller.signal);
for (const [, init] of fetchMock.mock.calls) expect(init?.signal).toBeInstanceOf(AbortSignal);
expect(fetchMock).toHaveBeenNthCalledWith(4, '/api/mileage/daily', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mileageQuery)
}));
expect(fetchMock).toHaveBeenNthCalledWith(5, '/api/v2/statistics/mileage', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mileageQuery)
}));
});
test('route-scoped platform reads forward AbortSignal on navigation cleanup', async () => {
@@ -211,6 +311,10 @@ test('route-scoped platform reads forward AbortSignal on navigation cleanup', as
await api.alertEventsV2({ limit: 20, offset: 0 }, signal);
await api.alertEventV2('event-1', signal);
await api.alertRulesV2(signal);
await api.alertRuleLibraryV2(new URLSearchParams({ lifecycle: 'current', limit: '10', offset: '0' }), signal);
await api.alertRuleRevisionsV2('rule 1', signal);
await api.alertNotificationConfigV2(signal);
await api.alertNotificationDeliveryHealthV2(signal);
await api.alertNotificationsV2(new URLSearchParams({ limit: '100' }), signal);
await api.vehicleDetail(new URLSearchParams({ keyword: 'VIN001' }), signal);
await api.latestTelemetry('VIN001', signal);
@@ -218,10 +322,37 @@ test('route-scoped platform reads forward AbortSignal on navigation cleanup', as
await api.opsHealth(signal);
await api.sourceReadiness(signal);
expect(fetchMock).toHaveBeenCalledTimes(18);
expect(fetchMock).toHaveBeenCalledTimes(22);
for (const [, init] of fetchMock.mock.calls) expect(init?.signal).toBeInstanceOf(AbortSignal);
});
test('automation revision reads and rollbacks use encoded durable routes', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {} }) } as Response);
await api.alertRuleRevisionsV2('rule / 1');
await api.rollbackAlertRuleV2('rule / 1', { targetVersion: 2, currentVersion: 7 });
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/v2/alerts/rules/rule%20%2F%201/revisions', boundedRequest());
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/v2/alerts/rules/rule%20%2F%201/rollback', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ targetVersion: 2, currentVersion: 7 })
}));
});
test('automation governance uses server-side library and reversible lifecycle routes', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ data: {} }) } as Response);
const query = new URLSearchParams({ lifecycle: 'archived', keyword: '旧规则', status: 'disabled', protocol: 'GB32960', limit: '20', offset: '20' });
await api.alertRuleLibraryV2(query);
await api.archiveAlertRuleV2('rule / 1', { version: 7, reason: '旧规则已经由新版替代' });
await api.restoreAlertRuleV2('rule / 1', { version: 8, reason: '恢复后重新复核车辆范围' });
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/v2/alerts/rules/library?lifecycle=archived&keyword=%E6%97%A7%E8%A7%84%E5%88%99&status=disabled&protocol=GB32960&limit=20&offset=20', boundedRequest());
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/v2/alerts/rules/rule%20%2F%201/archive', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 7, reason: '旧规则已经由新版替代' })
}));
expect(fetchMock).toHaveBeenNthCalledWith(3, '/api/v2/alerts/rules/rule%20%2F%201/restore', boundedRequest({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: 8, reason: '恢复后重新复核车辆范围' })
}));
});
test('turns a stalled route query into a bounded retryable timeout', async () => {
vi.useFakeTimers();
const controller = new AbortController();
@@ -341,6 +472,25 @@ test('vehicleResolve sends keyword to the identity resolution endpoint', async (
expect(result.vin).toBe('LB9A32A24R0LS1426');
});
test('vehicleBusinessFilters reads permission-scoped filter options', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({
data: {
departments: [{ value: '40001', label: '业务一部', count: 3 }],
responsibleUsers: [], customers: [], statuses: []
},
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response);
const result = await api.vehicleBusinessFilters();
expect(fetchMock).toHaveBeenCalledWith('/api/vehicles/business-filters', boundedRequest());
expect(result.departments[0]).toEqual({ value: '40001', label: '业务一部', count: 3 });
});
test('vehicleServiceOverview sends keyword to the lightweight overview endpoint', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
@@ -5,15 +5,23 @@ import type {
AccessSummary,
AccessThresholdConfig,
AccessThresholdUpdate,
AccessIdentityClaimInput,
AccessIdentityClaimResult,
AccessUnresolvedIdentity,
AccessUnresolvedIdentityQuery,
AccessVehicleRow,
AlertAction,
AlertEvent,
AlertNotification,
AlertNotificationConfig,
AlertNotificationDeliveryHealth,
AlertNotificationRetryAudit,
AlertNotificationRetryResult,
AlertQuery,
AlertRule,
AlertRulePage,
AlertRuleInput,
AlertRuleRevision,
AlertSummary,
DailyMileageRow,
DashboardSummary,
@@ -21,7 +29,16 @@ import type {
HistoryDataResponse,
HistorySeriesResponse,
HistoryExportJob,
HistoryExportBatchAction, HistoryExportBatchResult,
HistoryExportCleanupPreview,
HistoryExportCleanupRecord,
HistoryExportCleanupResult,
HistoryExportCleanupAutomationState,
HistoryExportCleanupAutomationPolicyInput,
HistoryExportCleanupAutomationActionInput,
HistoryExportPage,
HistoryExportRequest,
HistoryPreferences,
HistoryMetricCatalog,
MetricCatalog,
LatestTelemetryResponse,
@@ -39,18 +56,29 @@ import type {
QualitySummary,
QualityIssueRow,
ReconciliationIssue,
ReconciliationAssignee,
ReconciliationAssignmentRequest,
ReconciliationBatchAssignmentRequest,
ReconciliationBatchActionRequest,
ReconciliationBatchActionResult,
ReconciliationQuery,
ReconciliationLifecycleRequest,
ReconciliationBatchLifecycleRequest,
ReconciliationSummary,
RawFrameRow,
RealtimeLocationRow,
SourceReadinessPlan,
SessionInfo,
LoginResponse,
OneOSExchangeResponse,
CustomerUserInput,
CustomerUserBatchItem,
CustomerUserBatchResult,
TrackPlaybackResponse,
VehicleRealtimeRow,
VehicleCoverageRow,
VehicleCoverageSummary,
VehicleBusinessFilters,
VehicleDetail,
VehicleProfile,
VehicleProfileInput,
@@ -85,6 +113,19 @@ export type VehicleOverviewBatchQuery = {
offset?: number;
};
export type MileageQuery = {
dateFrom: string;
dateTo: string;
keyword?: string;
vins?: string[];
vehicleScope?: 'bound';
protocol?: string;
protocols?: string[];
deduplicate?: boolean;
limit?: number;
offset?: number;
};
type ApiErrorEnvelope = {
error?: {
code?: string;
@@ -146,16 +187,18 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
}
}
async function requestBlob(path: string, signal?: AbortSignal) {
async function requestBlob(path: string, signal?: AbortSignal, requestInit?: RequestInit, fallbackFilename = 'history-export.csv') {
const token = getAccessToken();
const init: RequestInit = token ? { signal, headers: withAuthorization(undefined, token) } : { signal };
const init: RequestInit = token
? { ...requestInit, signal, headers: withAuthorization(requestInit?.headers, token) }
: { ...requestInit, signal };
const response = await fetch(path, init);
if (!response.ok) {
if (response.status === 401 && token) notifyUnauthorizedSession(token);
throw new Error(await responseErrorMessage(response));
}
const encodedFilename = response.headers.get('X-Export-Name')?.trim();
let filename = 'history-export.csv';
let filename = fallbackFilename;
if (encodedFilename) {
try {
filename = decodeURIComponent(encodedFilename);
@@ -221,6 +264,9 @@ export const api = {
login: (credentials: { username: string; password: string }) => request<LoginResponse>('/api/v2/auth/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials)
}),
exchangeOneOSTicket: (ticket: string) => request<OneOSExchangeResponse>('/api/v2/auth/oneos/exchange', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticket })
}),
logout: () => request<{ loggedOut: boolean }>('/api/v2/auth/logout', { method: 'POST' }),
changePassword: (input: { currentPassword: string; newPassword: string }) => request<{ changed: boolean }>('/api/v2/auth/password', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
@@ -229,6 +275,9 @@ export const api = {
createCustomerUser: (input: CustomerUserInput) => request<{ id: number }>('/api/v2/admin/users', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
batchCustomerUsers: (mode: 'preview' | 'create', items: CustomerUserBatchItem[]) => request<CustomerUserBatchResult>('/api/v2/admin/users/batch', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode, items })
}),
updateCustomerUser: (id: number, input: CustomerUserInput) => request<{ id: number }>(`/api/v2/admin/users/${id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
@@ -241,9 +290,42 @@ export const api = {
historyData: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistoryDataResponse>(`/api/v2/history/query?${params.toString()}`, signal ? { signal } : undefined),
historySeries: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistorySeriesResponse>(`/api/v2/history/series?${params.toString()}`, signal ? { signal } : undefined),
historyExports: (signal?: AbortSignal) => request<HistoryExportJob[]>('/api/v2/exports', withSignal(undefined, signal)),
historyExportPage: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistoryExportPage>(`/api/v2/exports/page?${params.toString()}`, withSignal(undefined, signal)),
historyExportCleanupPreview: (params = new URLSearchParams(), signal?: AbortSignal) => request<HistoryExportCleanupPreview>(`/api/v2/exports/cleanup/preview?${params.toString()}`, withSignal(undefined, signal)),
historyExportCleanupAudit: (signal?: AbortSignal) => request<HistoryExportCleanupRecord[]>('/api/v2/exports/cleanup/audit?limit=20', withSignal(undefined, signal)),
cleanupHistoryExports: (input: { olderThanDays: number; ownerScope: 'all' | 'mine'; previewToken: string }) => request<HistoryExportCleanupResult>('/api/v2/exports/cleanup', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
historyExportCleanupAutomation: (signal?: AbortSignal) => request<HistoryExportCleanupAutomationState>('/api/v2/exports/cleanup/automation', withSignal(undefined, signal)),
updateHistoryExportCleanupAutomation: (input: HistoryExportCleanupAutomationPolicyInput) => request<HistoryExportCleanupAutomationState>('/api/v2/exports/cleanup/automation', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
approveHistoryExportCleanupAutomation: (id: string, input: HistoryExportCleanupAutomationActionInput) => request<HistoryExportCleanupAutomationState>(`/api/v2/exports/cleanup/automation/${encodeURIComponent(id)}/approve`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
rejectHistoryExportCleanupAutomation: (id: string, input: HistoryExportCleanupAutomationActionInput) => request<HistoryExportCleanupAutomationState>(`/api/v2/exports/cleanup/automation/${encodeURIComponent(id)}/reject`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
retryHistoryExportCleanupAutomation: (id: string, input: HistoryExportCleanupAutomationActionInput) => request<HistoryExportCleanupAutomationState>(`/api/v2/exports/cleanup/automation/${encodeURIComponent(id)}/retry`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
updateHistoryExportCleanupProtection: (id: string, input: { protected: boolean; reason: string }) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/cleanup-protection`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
historyPreferences: (signal?: AbortSignal) => request<HistoryPreferences>('/api/v2/history/preferences', withSignal(undefined, signal)),
updateHistoryPreferences: (input: Pick<HistoryPreferences, 'retentionDays' | 'fieldViews'>) => request<HistoryPreferences>('/api/v2/history/preferences', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
createHistoryExport: (query: HistoryExportRequest) => request<HistoryExportJob>('/api/v2/exports', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}),
cancelHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/cancel`, { method: 'POST' }),
rebuildHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/rebuild`, { method: 'POST' }),
archiveHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/archive`, { method: 'POST' }),
restoreHistoryExport: (id: string) => request<HistoryExportJob>(`/api/v2/exports/${encodeURIComponent(id)}/restore`, { method: 'POST' }),
batchHistoryExports: (input: { ids: string[]; action: HistoryExportBatchAction }) => request<HistoryExportBatchResult>('/api/v2/exports/batch', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
downloadHistoryExport: (id: string, signal?: AbortSignal) => requestBlob(`/api/v2/exports/${encodeURIComponent(id)}/download`, signal),
accessSummary: (query: AccessQuery, signal?: AbortSignal) => request<AccessSummary>('/api/v2/access/summary', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
@@ -254,6 +336,9 @@ export const api = {
accessUnresolvedIdentities: (query: AccessUnresolvedIdentityQuery, signal?: AbortSignal) => request<Page<AccessUnresolvedIdentity>>('/api/v2/access/unresolved-identities', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}, signal)),
claimAccessIdentity: (id: string, input: AccessIdentityClaimInput) => request<AccessIdentityClaimResult>(`/api/v2/access/unresolved-identities/${encodeURIComponent(id)}/claim`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
accessThresholds: (signal?: AbortSignal) => request<AccessThresholdConfig>('/api/v2/access/thresholds', withSignal(undefined, signal)),
updateAccessThresholds: (update: AccessThresholdUpdate) => request<AccessThresholdConfig>('/api/v2/access/thresholds', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(update)
@@ -269,21 +354,39 @@ export const api = {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(action)
}),
alertRulesV2: (signal?: AbortSignal) => request<AlertRule[]>('/api/v2/alerts/rules', withSignal(undefined, signal)),
alertRuleLibraryV2: (params = new URLSearchParams(), signal?: AbortSignal) => request<AlertRulePage>(`/api/v2/alerts/rules/library?${params.toString()}`, withSignal(undefined, signal)),
alertRuleRevisionsV2: (id: string, signal?: AbortSignal) => request<AlertRuleRevision[]>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/revisions`, withSignal(undefined, signal)),
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)
}),
rollbackAlertRuleV2: (id: string, input: { targetVersion: number; currentVersion: number; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/rollback`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
archiveAlertRuleV2: (id: string, input: { version: number; reason: string; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/archive`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
restoreAlertRuleV2: (id: string, input: { version: number; reason: string; actor?: string }) => request<AlertRule>(`/api/v2/alerts/rules/${encodeURIComponent(id)}/restore`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
alertNotificationConfigV2: (signal?: AbortSignal) => request<AlertNotificationConfig>('/api/v2/alerts/notification-config', withSignal(undefined, signal)),
alertNotificationDeliveryHealthV2: (signal?: AbortSignal) => request<AlertNotificationDeliveryHealth>('/api/v2/alerts/notifications/health', withSignal(undefined, signal)),
alertNotificationsV2: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<AlertNotification>>(`/api/v2/alerts/notifications?${params.toString()}`, withSignal(undefined, signal)),
readAlertNotificationsV2: (ids: number[]) => request<{ updated: number }>('/api/v2/alerts/notifications/read', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids })
}),
retryAlertNotificationV2: (id: number, input: { expectedAttemptCount: number; reason: string; idempotencyKey: string }) => request<AlertNotificationRetryResult>(`/api/v2/alerts/notifications/${encodeURIComponent(id)}/retry`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
alertNotificationRetryAuditsV2: (id: number, signal?: AbortSignal) => request<AlertNotificationRetryAudit[]>(`/api/v2/alerts/notifications/${encodeURIComponent(id)}/retry-audit`, withSignal(undefined, signal)),
dashboardSummary: () => request<DashboardSummary>('/api/dashboard/summary'),
vehicles: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<VehicleRow>>(`/api/vehicles?${params.toString()}`, signal ? { signal } : undefined),
vehicleResolve: (params = new URLSearchParams()) => request<VehicleIdentityResolution>(`/api/vehicles/resolve?${params.toString()}`),
vehicleCoverage: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<VehicleCoverageRow>>(`/api/vehicles/coverage?${params.toString()}`, signal ? { signal } : undefined),
vehicleCoverageSummary: (params = new URLSearchParams()) => request<VehicleCoverageSummary>(`/api/vehicles/coverage/summary?${params.toString()}`),
vehicleBusinessFilters: (signal?: AbortSignal) => request<VehicleBusinessFilters>('/api/vehicles/business-filters', withSignal(undefined, signal)),
vehicleDetail: (params = new URLSearchParams(), signal?: AbortSignal) => request<VehicleDetail>(`/api/vehicle-service?${params.toString()}`, withSignal(undefined, signal)),
vehicleProfile: (vin: string) => request<VehicleProfile>(`/api/v2/vehicles/${encodeURIComponent(vin)}/profile`),
latestTelemetry: (vin: string, signal?: AbortSignal) => request<LatestTelemetryResponse>(`/api/v2/vehicles/${encodeURIComponent(vin)}/telemetry/latest`, withSignal(undefined, signal)),
@@ -320,6 +423,16 @@ export const api = {
'/api/v2/reconciliation/issues',
withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)
),
reconciliationAssignees: (search = '', signal?: AbortSignal) => request<ReconciliationAssignee[]>(
`/api/v2/reconciliation/assignees?${new URLSearchParams(search ? { search } : {}).toString()}`,
withSignal(undefined, signal)
),
downloadReconciliationIssues: (query: ReconciliationQuery, signal?: AbortSignal) => requestBlob(
'/api/v2/reconciliation/issues/export',
signal,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) },
'quality-issues.csv'
),
reconciliationIssue: (id: string, signal?: AbortSignal) => request<ReconciliationIssue>(
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}`,
withSignal(undefined, signal)
@@ -328,12 +441,32 @@ export const api = {
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}/actions`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
batchUpdateReconciliationIssues: (input: ReconciliationBatchActionRequest) => request<ReconciliationBatchActionResult>(
'/api/v2/reconciliation/issues/batch-actions',
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
assignReconciliationIssue: (id: string, input: ReconciliationAssignmentRequest) => request<ReconciliationIssue>(
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}/assignment`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
batchAssignReconciliationIssues: (input: ReconciliationBatchAssignmentRequest) => request<ReconciliationBatchActionResult>(
'/api/v2/reconciliation/issues/batch-assignments',
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
setReconciliationIssueArchived: (id: string, archived: boolean, input: ReconciliationLifecycleRequest) => request<ReconciliationIssue>(
`/api/v2/reconciliation/issues/${encodeURIComponent(id)}/${archived ? 'archive' : 'restore'}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
batchSetReconciliationIssuesArchived: (archived: boolean, input: ReconciliationBatchLifecycleRequest) => request<ReconciliationBatchActionResult>(
`/api/v2/reconciliation/issues/${archived ? 'batch-archive' : 'batch-restore'}`,
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }
),
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', {
syncVehicleProfiles: (input: VehicleProfileSyncRequest, signal?: AbortSignal) => request<VehicleProfileSyncResult>('/api/v2/vehicle-profiles/sync', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input)
}),
}, signal)),
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', {
@@ -351,8 +484,12 @@ export const api = {
body: JSON.stringify(query)
}),
mileageSummary: (params = new URLSearchParams()) => request<MileageSummary>(`/api/mileage/summary?${params.toString()}`),
dailyMileage: (params = new URLSearchParams(), signal?: AbortSignal) => request<Page<DailyMileageRow>>(`/api/mileage/daily?${params.toString()}`, signal ? { signal } : undefined),
mileageStatistics: (params = new URLSearchParams(), signal?: AbortSignal) => request<MileageStatistics>(`/api/v2/statistics/mileage?${params.toString()}`, signal ? { signal } : undefined),
dailyMileage: (query: MileageQuery, signal?: AbortSignal) => request<Page<DailyMileageRow>>('/api/mileage/daily', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}, signal)),
mileageStatistics: (query: MileageQuery, signal?: AbortSignal) => request<MileageStatistics>('/api/v2/statistics/mileage', withSignal({
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query)
}, signal)),
onlineStatisticsSummary: (params = new URLSearchParams()) => request<OnlineStatisticsSummary>(`/api/statistics/online-summary?${params.toString()}`),
onlineVehicleStatuses: (params = new URLSearchParams()) => request<Page<OnlineVehicleStatusRow>>(`/api/statistics/online-vehicles?${params.toString()}`),
qualitySummary: (params = new URLSearchParams()) => request<QualitySummary>(`/api/quality/summary?${params.toString()}`),
+202 -11
View File
@@ -15,6 +15,9 @@ export interface SessionInfo {
vehicleCount?: number;
customerRef?: string;
tenantRef?: string;
businessScopeLevel?: 'department' | 'responsible';
departmentIds?: string[];
responsibleUserId?: string;
authMode: 'disabled' | 'enforce';
}
@@ -24,6 +27,10 @@ export interface LoginResponse {
session: Omit<SessionInfo, 'authMode'>;
}
export interface OneOSExchangeResponse extends LoginResponse {
returnTo: string;
}
export interface AdminUser {
id: number;
username: string;
@@ -77,6 +84,27 @@ export interface CustomerUserInput {
vehicleGrants: CustomerVehicleGrantInput[];
}
export interface CustomerUserBatchItem {
row: number;
input: CustomerUserInput & { username: string; password: string };
}
export interface CustomerUserBatchResultItem {
row: number;
username: string;
displayName: string;
status: 'ready' | 'created' | 'invalid' | 'conflict' | 'failed';
code?: string;
message: string;
id?: number;
}
export interface CustomerUserBatchResult {
mode: 'preview' | 'create';
summary: { received: number; ready: number; created: number; failed: number };
items: CustomerUserBatchResultItem[];
}
export interface MonitorSummary {
totalVehicles: number;
locationVehicles?: number;
@@ -271,15 +299,33 @@ export interface MetricDefinition {
export interface HistoryDataCategory { key: 'location' | 'raw' | 'mileage' | string; label: string; }
export interface HistoryMetricDefinition { key: string; label: string; description?: string; unit: string; category: string; valueType: string; valueMappings?: MetricValueMapping[]; defaultVisible: boolean; }
export interface HistoryDataRow { id: string; vin: string; plate: string; protocol: string; deviceTime: string; serverTime: string; quality: string; qualityReason: 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 HistoryQueryVehicleResult { keyword: string; vin?: string; plate?: string; rowCount: number; status: 'matched' | 'no_data'; }
export interface HistoryDataSummary { resultRows: number; vehicleCount: number; requestedVehicleCount?: number; vehicles?: HistoryQueryVehicleResult[]; sources: string[]; queryDurationMs: number; }
export interface HistoryDataResponse { category: string; columns: HistoryMetricDefinition[]; segments?: LatestTelemetryCategory[]; segmentCountsExact?: boolean; allTotal?: number; 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; protocol?: string; keywords: string[]; metrics?: string[]; vehicleVins?: string[]; dateFrom?: string; dateTo?: string; ownerId?: string; ownerName?: string; ownerUsername?: string; ownerRole?: string; ownerUserType?: string; customerRef?: string; tenantRef?: string; rowCount: number; totalRows: number; processedRows: number; fileSizeBytes: number; error?: string; downloadUrl?: string; createdAt: string; updatedAt: string; completedAt?: string; evidence: string; }
export interface HistoryExportRequest { keywords: string[]; category: string; protocol?: string; dateFrom?: string; dateTo?: string; metrics: string[]; format: 'csv'; retentionDays?: number; }
export type HistoryExportStatus = 'queued' | 'running' | 'completed' | 'failed' | 'expired' | 'cancelled';
export interface HistoryExportJob { id: string; name: string; status: HistoryExportStatus; progress: number; format: string; category: string; protocol?: string; keywords: string[]; metrics?: string[]; vehicleVins?: string[]; dateFrom?: string; dateTo?: string; ownerId?: string; ownerName?: string; ownerUsername?: string; ownerRole?: string; ownerUserType?: string; customerRef?: string; tenantRef?: string; rowCount: number; totalRows: number; processedRows: number; fileSizeBytes: number; error?: string; downloadUrl?: string; createdAt: string; updatedAt: string; completedAt?: string; expiresAt?: string; expiredAt?: string; rebuiltFrom?: string; cancelledAt?: string; cancelledBy?: string; archivedAt?: string; archivedBy?: string; cleanupProtectedAt?: string; cleanupProtectedBy?: string; cleanupProtectionReason?: string; retentionDays?: number; evidence: string; }
export type HistoryExportBatchAction = 'cancel' | 'rebuild' | 'archive' | 'restore';
export interface HistoryExportBatchResult { action: HistoryExportBatchAction; requested: number; succeeded: HistoryExportJob[]; skipped: { id: string; code: string; message: string }[]; }
export interface HistoryExportSummary { total: number; active: number; completed: number; expiring: number; recoverable: number; cancelled: number; current: number; archived: number; }
export interface HistoryExportPage { items: HistoryExportJob[]; total: number; limit: number; offset: number; summary: HistoryExportSummary; }
export interface HistoryExportCleanupCandidate { id: string; name: string; ownerUsername: string; status: HistoryExportStatus; archivedAt: string; fileSizeBytes: number; protectedAt?: string; protectedBy?: string; protectionReason?: string; }
export interface HistoryExportCleanupPreview { olderThanDays: number; ownerScope: 'all' | 'mine'; cutoff: string; generatedAt: string; archivedCount: number; candidateCount: number; plannedCount: number; protectedCount: number; fileCount: number; fileSizeBytes: number; candidates: HistoryExportCleanupCandidate[]; protected: HistoryExportCleanupCandidate[]; previewToken: string; }
export interface HistoryExportCleanupRecord { id: string; actor: string; ownerScope: 'all' | 'mine'; olderThanDays: number; cutoff: string; requested: number; cleaned: number; protected: number; fileCount: number; fileSizeBytes: number; candidateDigest: string; executedAt: string; }
export interface HistoryExportCleanupResult { record: HistoryExportCleanupRecord; deleted: HistoryExportCleanupCandidate[]; }
export type HistoryExportCleanupAutomationRunStatus = 'awaiting_approval' | 'needs_review' | 'approved' | 'running' | 'completed' | 'failed' | 'rejected' | 'expired' | 'no_candidates';
export interface HistoryExportCleanupAutomationPolicy { enabled: boolean; olderThanDays: 30 | 90 | 180 | 365; intervalDays: 7 | 14 | 30; approvalWindowHours: 24 | 48 | 72; nextReviewAt?: string; revision: number; updatedAt?: string; updatedBy?: string; }
export interface HistoryExportCleanupAutomationRun { id: string; status: HistoryExportCleanupAutomationRunStatus; revision: number; policyRevision: number; olderThanDays: number; cutoff: string; candidateCount: number; plannedCount: number; protectedCount: number; fileCount: number; fileSizeBytes: number; previewToken: string; createdAt: string; approvalDeadline?: string; approvedAt?: string; approvedBy?: string; approvalReason?: string; rejectedAt?: string; rejectedBy?: string; rejectionReason?: string; startedAt?: string; completedAt?: string; leaseOwner?: string; leaseExpiresAt?: string; attemptCount: number; maxAttempts: number; nextRetryAt?: string; error?: string; cleanupRecordId?: string; cleanedCount?: number; executionWarning?: string; lastActionAt?: string; lastActionBy?: string; lastActionReason?: string; }
export interface HistoryExportCleanupAutomationState { policy: HistoryExportCleanupAutomationPolicy; runs: HistoryExportCleanupAutomationRun[]; serverTime: string; }
export interface HistoryExportCleanupAutomationPolicyInput { enabled: boolean; olderThanDays: number; intervalDays: number; approvalWindowHours: number; expectedRevision: number; }
export interface HistoryExportCleanupAutomationActionInput { expectedRevision: number; reason: string; }
export interface HistoryFieldView { id: string; name: string; category: string; keys: string[]; }
export interface HistoryPreferences { revision: number; retentionDays: number; fieldViews: HistoryFieldView[]; updatedAt?: 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; connectionState?: string; limit?: number; offset?: number; }
export interface AccessUnresolvedIdentityQuery { keyword?: string; protocol?: string; limit?: number; offset?: number; }
@@ -288,6 +334,11 @@ export interface AccessUnresolvedIdentity {
firstRegisteredAt: string; latestRegisteredAt: string; latestAuthenticatedAt: string; latestSeenAt: string;
freshnessSec: number; issueCode: string; recommendedAction: string;
}
export interface AccessIdentityClaimInput { vin: string; note?: string; actor?: string; }
export interface AccessIdentityClaimResult {
identityId: string; protocol: string; identifierMasked: string; vin: string; plate: string;
profileComplete: boolean; profileMissingFields: string[]; claimedBy: string; claimedAt: string; auditId: number;
}
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;
@@ -325,23 +376,52 @@ export interface AccessThresholdUpdate {
export type AlertSeverity = 'critical' | 'major' | 'minor';
export type AlertStatus = 'unprocessed' | 'processing' | 'recovered' | 'closed' | 'ignored';
export type AlertTriggerType = 'metric' | 'geofence' | 'stationary' | 'offline';
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;
id: string; eventType?: string; eventCategory?: string; executionState?: 'pending' | 'processing' | 'recovered' | 'completed' | 'ignored'; ruleId: string; ruleName: string; ruleVersion: number; severity: AlertSeverity; triggerType?: AlertTriggerType; 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;
id: string; name: string; description: string; triggerType?: AlertTriggerType; fenceName?: string; fenceLongitude?: number; fenceLatitude?: number; fenceRadiusM?: number;
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;
notificationChannels: string[]; notificationTargets?: AlertNotificationTarget[]; enabled: boolean; version: number; createdBy: string; updatedBy: string; createdAt: string; updatedAt: string;
archivedBy?: string; archivedAt?: string; archiveReason?: string;
}
export interface AlertRuleInput extends Omit<AlertRule, 'notificationTargets' | 'createdBy' | 'updatedBy' | 'createdAt' | 'updatedAt'> { notificationTargets?: AlertNotificationTarget[]; actor?: string; }
export interface AlertRuleLibrarySummary { current: number; enabled: number; disabled: number; archived: number; }
export interface AlertRulePage extends Page<AlertRule> { summary: AlertRuleLibrarySummary; }
export interface AlertNotificationTarget { channel: string; recipientId: string; label: string; }
export interface AlertNotificationTargetOption { id: string; label: string; channels: string[]; }
export interface AlertNotificationChannelCapability { channel: string; label: string; configured: boolean; }
export interface AlertNotificationConfig { targets: AlertNotificationTargetOption[]; channels: AlertNotificationChannelCapability[]; }
export interface AlertRuleRevision {
ruleId: string; version: number; actor: string; action: 'create' | 'update' | 'enable' | 'disable' | 'rollback' | 'archive' | 'restore' | string; reason?: string; createdAt: string; snapshot: AlertRule;
}
export interface AlertNotification {
id: number; eventId: string; title: string; content: string; severity: AlertSeverity; channel: string;
recipient?: string; recipientId?: string; deliveryStatus?: 'queued' | 'sent' | 'delivered' | 'failed' | string; attemptCount?: number; providerMessageId?: string;
vehiclePlate?: string; vehicleVin?: string; protocol?: string; read: boolean; createdAt: string; deliveredAt?: string; readAt: string;
lastError?: string; lastAttemptAt?: string; retryRequestedBy?: string; retryRequestedAt?: string; retryAvailable?: boolean; maxAttempts?: number;
}
export interface AlertNotificationChannelHealth {
channel: string; label: string; configured: boolean; queued: number; failed: number; deadLetter: number; activeLeases: number; oldestQueuedAt: string;
}
export interface AlertNotificationDeliveryHealth {
queued: number; failed: number; deadLetter: number; activeLeases: number; oldestQueuedAt: string; channels: AlertNotificationChannelHealth[]; asOf: string;
}
export interface AlertNotificationRetryAudit {
id: number; notificationId: number; actor: string; reason: string; previousStatus: string; nextStatus: string; attemptCount: number; requestedAt: string;
}
export interface AlertNotificationRetryResult {
notification: AlertNotification; receipt: AlertNotificationRetryAudit; idempotent: boolean;
}
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;
@@ -414,6 +494,19 @@ export interface VehicleCoverageSummary {
archiveMissingFields: ArchiveMissingFieldStat[];
}
export interface VehicleBusinessFilterOption {
value: string;
label: string;
count: number;
}
export interface VehicleBusinessFilters {
departments: VehicleBusinessFilterOption[];
responsibleUsers: VehicleBusinessFilterOption[];
customers: VehicleBusinessFilterOption[];
statuses: VehicleBusinessFilterOption[];
}
export interface VehicleIdentityResolution {
lookupKey: string;
resolved: boolean;
@@ -434,6 +527,7 @@ export interface VehicleDetail {
resolution?: VehicleIdentityResolution;
identity?: VehicleRow;
profile?: VehicleProfile;
businessRelation?: VehicleBusinessRelation;
realtimeSummary?: VehicleRealtimeRow;
serviceStatus?: VehicleServiceStatus;
serviceOverview?: VehicleServiceOverview;
@@ -447,6 +541,27 @@ export interface VehicleDetail {
quality: Page<QualityIssueRow>;
}
export interface VehicleBusinessRelation {
sourceSystem: string;
sourceVersion: string;
vehicleId: string;
vin: string;
plateNumber: string;
customerId: string;
customerName: string;
contractId: string;
contractCode: string;
projectName: string;
departmentId: string;
departmentName: string;
responsibleUserId: string;
responsibleUserName: string;
operationStatus: string;
scopeStartAt: string;
sourceUpdatedAt: string;
publishedAt: string;
}
export interface VehicleProfile {
vin: string;
brandName: string;
@@ -790,6 +905,9 @@ export interface DailyMileageRow {
startMileageKm: number;
endMileageKm: number;
dailyMileageKm: number;
pureHydrogenMileageKm?: number;
hydrogenConsumptionKg?: number | null;
hydrogenConsumptionKgPer100Km?: number | null;
source: string;
anomalySeverity?: string;
}
@@ -799,14 +917,26 @@ export interface MileageSummary {
recordCount: number;
sourceCount: number;
totalMileageKm: number;
totalPureHydrogenMileageKm: number;
averageMileagePerVin: number;
}
export interface MileageTrendPoint { date: string; mileageKm: number; vehicles: number; }
export interface MileageTrendPoint {
date: string;
mileageKm: number;
pureHydrogenMileageKm: number;
hydrogenMatchedMileageKm?: number;
hydrogenDataDays?: number;
hydrogenConsumptionKg?: number | null;
hydrogenConsumptionKgPer100Km?: number | null;
vehicles: number;
}
export interface MileageVehicleRank { vin: string; plate: string; mileageKm: number; latestMileageKm: number; activeDays: number; }
export interface MileageStatistics {
dateFrom: string; dateTo: string; vehicleCount: number; recordCount: number; sourceCount: number;
periodMileageKm: number; fleetLatestMileageKm: number; averageMileagePerVin: number; averageDailyMileageKm: number;
periodMileageKm: number; periodPureHydrogenMileageKm: number; hydrogenMatchedMileageKm?: number;
hydrogenDataDays?: number; periodHydrogenConsumptionKg?: number; hydrogenConsumptionKgPer100Km?: number | null;
fleetLatestMileageKm: number; averageMileagePerVin: number; averageDailyMileageKm: number;
trend: MileageTrendPoint[]; ranking: MileageVehicleRank[]; asOf: string; evidence: string;
}
@@ -970,10 +1100,13 @@ export interface RuntimeInfo {
export interface ReconciliationQuery {
keyword?: string;
scope?: 'current' | 'archived';
ruleCode?: string;
category?: string;
severity?: string;
status?: string;
owner?: string;
sla?: string;
limit?: number;
offset?: number;
}
@@ -1007,10 +1140,66 @@ export interface ReconciliationIssue {
recoveredAt: string;
resolutionNote: string;
resolvedBy: string;
assignee: string;
assignedBy: string;
assignedAt: string;
dueAt: string;
archivedAt: string;
archivedBy: string;
archiveReason: string;
version: number;
actions?: ReconciliationAction[];
}
export interface ReconciliationAssignmentRequest {
version: number;
assignee: string;
dueAt: string;
}
export interface ReconciliationAssignee {
name: string;
username?: string;
source: 'account' | 'history' | 'current';
activeCount: number;
lastAssignedAt?: string;
current?: boolean;
}
export interface ReconciliationBatchAssignmentRequest {
items: Array<{ id: string; version: number }>;
assignee: string;
dueAt: string;
}
export interface ReconciliationBatchActionRequest {
items: Array<{ id: string; version: number }>;
status: 'pending' | 'no_action' | 'fixed';
note: string;
}
export interface ReconciliationLifecycleRequest {
version: number;
reason: string;
}
export interface ReconciliationBatchLifecycleRequest {
items: Array<{ id: string; version: number }>;
reason: string;
}
export interface ReconciliationBatchActionFailure {
id: string;
code: string;
message: string;
}
export interface ReconciliationBatchActionResult {
requested: number;
succeeded: ReconciliationIssue[];
skipped: ReconciliationBatchActionFailure[];
}
export interface ReconciliationBucket {
name: string;
count: number;
@@ -1025,6 +1214,8 @@ export interface ReconciliationTrendPoint {
}
export interface ReconciliationSummary {
current: number;
archived: number;
active: number;
pending: number;
confirmed: number;