feat: expand vehicle data platform capabilities
This commit is contained in:
@@ -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()}`),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user