import type { ApiEnvelope, AdminUser, AccessQuery, AccessSummary, AccessThresholdConfig, AccessThresholdUpdate, AccessIdentityClaimInput, AccessIdentityClaimResult, AccessUnresolvedIdentity, AccessUnresolvedIdentityQuery, AccessVehicleRow, AlertAction, AlertEvent, AlertNotification, AlertNotificationConfig, AlertNotificationDeliveryHealth, AlertNotificationRetryAudit, AlertNotificationRetryResult, AlertQuery, AlertRule, AlertRulePage, AlertRuleInput, AlertRuleRevision, AlertSummary, DailyMileageRow, HydrogenDailyEvidence, DashboardSummary, HistoryLocationRow, HistoryDataResponse, HistorySeriesResponse, HistoryExportJob, HistoryExportBatchAction, HistoryExportBatchResult, HistoryExportCleanupPreview, HistoryExportCleanupRecord, HistoryExportCleanupResult, HistoryExportCleanupAutomationState, HistoryExportCleanupAutomationPolicyInput, HistoryExportCleanupAutomationActionInput, HistoryExportPage, HistoryExportRequest, HistoryPreferences, HistoryMetricCatalog, MetricCatalog, LatestTelemetryResponse, MileageSummary, MileageStatistics, MapReverseGeocode, MonitorMapResponse, MonitorWorkspaceResponse, MonitorSummary, OnlineStatisticsSummary, OnlineVehicleStatusRow, OpsHealth, QualityNotificationPlan, Page, 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, VehicleProfileSyncRequest, VehicleProfileSyncResult, VehicleIdentityResolution, VehicleServiceOverview, VehicleServiceSummary, VehicleSourceDiagnostic, VehicleSourceEvidence, VehicleSourcePolicyUpdate, VehicleRow } from './types'; import { getAccessToken, notifyUnauthorizedSession } from '../v2/auth/session'; export type RawFrameQuery = { keyword?: string; vin?: string; protocol?: string; dateFrom?: string; dateTo?: string; fields?: string[]; includeFields?: boolean; limit?: number; offset?: number; }; export type VehicleOverviewBatchQuery = { keywords: string[]; protocol?: string; limit?: number; 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; message?: string; detail?: string; }; traceId?: string; }; export const API_QUERY_TIMEOUT_MS = 15_000; function queryTimeout(init: RequestInit | undefined) { const upstream = init?.signal; const controller = new AbortController(); let timedOut = false; const abortFromUpstream = () => controller.abort(upstream?.reason); if (upstream?.aborted) abortFromUpstream(); else upstream?.addEventListener('abort', abortFromUpstream, { once: true }); const timer = window.setTimeout(() => { timedOut = true; controller.abort(); }, API_QUERY_TIMEOUT_MS); return { init: { ...init, signal: controller.signal }, timedOut: () => timedOut, cleanup: () => { window.clearTimeout(timer); upstream?.removeEventListener('abort', abortFromUpstream); } }; } function requestTimeoutError() { const error = new Error(`请求超过 ${API_QUERY_TIMEOUT_MS / 1_000} 秒仍未完成,请检查网络后重试`); error.name = 'ApiRequestTimeoutError'; return error; } async function request(path: string, init?: RequestInit): Promise { const token = getAccessToken(); const requestInit = token ? { ...init, headers: withAuthorization(init?.headers, token) } : init; const timeout = queryTimeout(requestInit); try { const response = await fetch(path, timeout?.init ?? requestInit); if (!response.ok) { if (response.status === 401 && token && path !== '/api/v2/session') notifyUnauthorizedSession(token); throw new Error(await responseErrorMessage(response)); } const envelope = (await response.json()) as ApiEnvelope; if (timeout?.timedOut()) throw requestTimeoutError(); return envelope.data; } catch (error) { if (timeout?.timedOut() && (!(error instanceof Error) || error.name !== 'ApiRequestTimeoutError')) { throw requestTimeoutError(); } throw error; } finally { timeout?.cleanup(); } } async function requestBlob(path: string, signal?: AbortSignal, requestInit?: RequestInit, fallbackFilename = 'history-export.csv') { const token = getAccessToken(); 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 = fallbackFilename; if (encodedFilename) { try { filename = decodeURIComponent(encodedFilename); } catch { filename = encodedFilename; } } return { blob: await response.blob(), filename }; } function withAuthorization(headers: HeadersInit | undefined, token: string) { const authorized = new Headers(headers); authorized.set('Authorization', `Bearer ${token}`); return authorized; } function withSignal(init: RequestInit | undefined, signal?: AbortSignal) { return signal ? { ...init, signal } : init; } function normalizeOpsHealth(value: OpsHealth): OpsHealth { return { ...value, linkHealth: Array.isArray(value.linkHealth) ? value.linkHealth : [], capacityFindings: Array.isArray(value.capacityFindings) ? value.capacityFindings : [] }; } function normalizeSourceReadiness(value: SourceReadinessPlan): SourceReadinessPlan { return { ...value, sources: Array.isArray(value.sources) ? value.sources : [] }; } async function responseErrorMessage(response: Response) { try { const envelope = (await response.json()) as ApiErrorEnvelope; const message = envelope.error?.message?.trim(); const detail = envelope.error?.detail?.trim(); const traceID = envelope.traceId?.trim(); if (message && detail) { return withTraceID(`${message}: ${detail}`, traceID); } if (message) { return withTraceID(message, traceID); } } catch { // Fall back to status when the server does not return the platform envelope. } return `request failed ${response.status}`; } function withTraceID(message: string, traceID?: string) { return traceID ? `${message} (traceId: ${traceID})` : message; } export const api = { session: (signal?: AbortSignal) => request('/api/v2/session', withSignal(undefined, signal)), login: (credentials: { username: string; password: string }) => request('/api/v2/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }), exchangeOneOSTicket: (ticket: string) => request('/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) }), adminUsers: (signal?: AbortSignal) => request('/api/v2/admin/users', withSignal(undefined, signal)), 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('/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) }), monitorSummary: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/monitor/summary?${params.toString()}`, signal ? { signal } : undefined), monitorMap: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/monitor/map?${params.toString()}`, signal ? { signal } : undefined), monitorWorkspace: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/monitor/workspace?${params.toString()}`, signal ? { signal } : undefined), trackPlayback: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/tracks?${params.toString()}`, signal ? { signal } : undefined), metricCatalog: (signal?: AbortSignal) => request('/api/v2/metrics', withSignal(undefined, signal)), historyMetricCatalog: (signal?: AbortSignal) => request('/api/v2/history/metrics', withSignal(undefined, signal)), historyData: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/history/query?${params.toString()}`, signal ? { signal } : undefined), historySeries: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/history/series?${params.toString()}`, signal ? { signal } : undefined), historyExports: (signal?: AbortSignal) => request('/api/v2/exports', withSignal(undefined, signal)), historyExportPage: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/exports/page?${params.toString()}`, withSignal(undefined, signal)), historyExportCleanupPreview: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/exports/cleanup/preview?${params.toString()}`, withSignal(undefined, signal)), historyExportCleanupAudit: (signal?: AbortSignal) => request('/api/v2/exports/cleanup/audit?limit=20', withSignal(undefined, signal)), cleanupHistoryExports: (input: { olderThanDays: number; ownerScope: 'all' | 'mine'; previewToken: string }) => request('/api/v2/exports/cleanup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), historyExportCleanupAutomation: (signal?: AbortSignal) => request('/api/v2/exports/cleanup/automation', withSignal(undefined, signal)), updateHistoryExportCleanupAutomation: (input: HistoryExportCleanupAutomationPolicyInput) => request('/api/v2/exports/cleanup/automation', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), approveHistoryExportCleanupAutomation: (id: string, input: HistoryExportCleanupAutomationActionInput) => request(`/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(`/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(`/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(`/api/v2/exports/${encodeURIComponent(id)}/cleanup-protection`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), historyPreferences: (signal?: AbortSignal) => request('/api/v2/history/preferences', withSignal(undefined, signal)), updateHistoryPreferences: (input: Pick) => request('/api/v2/history/preferences', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), createHistoryExport: (query: HistoryExportRequest) => request('/api/v2/exports', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }), cancelHistoryExport: (id: string) => request(`/api/v2/exports/${encodeURIComponent(id)}/cancel`, { method: 'POST' }), rebuildHistoryExport: (id: string) => request(`/api/v2/exports/${encodeURIComponent(id)}/rebuild`, { method: 'POST' }), archiveHistoryExport: (id: string) => request(`/api/v2/exports/${encodeURIComponent(id)}/archive`, { method: 'POST' }), restoreHistoryExport: (id: string) => request(`/api/v2/exports/${encodeURIComponent(id)}/restore`, { method: 'POST' }), batchHistoryExports: (input: { ids: string[]; action: HistoryExportBatchAction }) => request('/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('/api/v2/access/summary', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)), accessVehicles: (query: AccessQuery, signal?: AbortSignal) => request>('/api/v2/access/vehicles', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)), accessUnresolvedIdentities: (query: AccessUnresolvedIdentityQuery, signal?: AbortSignal) => request>('/api/v2/access/unresolved-identities', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)), claimAccessIdentity: (id: string, input: AccessIdentityClaimInput) => request(`/api/v2/access/unresolved-identities/${encodeURIComponent(id)}/claim`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), accessThresholds: (signal?: AbortSignal) => request('/api/v2/access/thresholds', withSignal(undefined, signal)), updateAccessThresholds: (update: AccessThresholdUpdate) => request('/api/v2/access/thresholds', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(update) }), alertSummaryV2: (query: AlertQuery, signal?: AbortSignal) => request('/api/v2/alerts/summary', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)), alertEventsV2: (query: AlertQuery, signal?: AbortSignal) => request>('/api/v2/alerts/events', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)), alertEventV2: (id: string, signal?: AbortSignal) => request(`/api/v2/alerts/events/${encodeURIComponent(id)}`, withSignal(undefined, signal)), actOnAlertV2: (id: string, action: Pick & { version: number; actor?: string }) => request(`/api/v2/alerts/events/${encodeURIComponent(id)}/actions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(action) }), alertRulesV2: (signal?: AbortSignal) => request('/api/v2/alerts/rules', withSignal(undefined, signal)), alertRuleLibraryV2: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/v2/alerts/rules/library?${params.toString()}`, withSignal(undefined, signal)), alertRuleRevisionsV2: (id: string, signal?: AbortSignal) => request(`/api/v2/alerts/rules/${encodeURIComponent(id)}/revisions`, withSignal(undefined, signal)), saveAlertRuleV2: (input: AlertRuleInput) => request(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(`/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(`/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(`/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(`/api/v2/alerts/rules/${encodeURIComponent(id)}/restore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), alertNotificationConfigV2: (signal?: AbortSignal) => request('/api/v2/alerts/notification-config', withSignal(undefined, signal)), alertNotificationDeliveryHealthV2: (signal?: AbortSignal) => request('/api/v2/alerts/notifications/health', withSignal(undefined, signal)), alertNotificationsV2: (params = new URLSearchParams(), signal?: AbortSignal) => request>(`/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(`/api/v2/alerts/notifications/${encodeURIComponent(id)}/retry`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), alertNotificationRetryAuditsV2: (id: number, signal?: AbortSignal) => request(`/api/v2/alerts/notifications/${encodeURIComponent(id)}/retry-audit`, withSignal(undefined, signal)), dashboardSummary: () => request('/api/dashboard/summary'), vehicles: (params = new URLSearchParams(), signal?: AbortSignal) => request>(`/api/vehicles?${params.toString()}`, signal ? { signal } : undefined), vehicleResolve: (params = new URLSearchParams()) => request(`/api/vehicles/resolve?${params.toString()}`), vehicleCoverage: (params = new URLSearchParams(), signal?: AbortSignal) => request>(`/api/vehicles/coverage?${params.toString()}`, signal ? { signal } : undefined), vehicleCoverageSummary: (params = new URLSearchParams()) => request(`/api/vehicles/coverage/summary?${params.toString()}`), vehicleBusinessFilters: (signal?: AbortSignal) => request('/api/vehicles/business-filters', withSignal(undefined, signal)), vehicleDetail: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/vehicle-service?${params.toString()}`, withSignal(undefined, signal)), vehicleProfile: (vin: string) => request(`/api/v2/vehicles/${encodeURIComponent(vin)}/profile`), latestTelemetry: (vin: string, signal?: AbortSignal) => request(`/api/v2/vehicles/${encodeURIComponent(vin)}/telemetry/latest`, withSignal(undefined, signal)), vehicleSourceEvidence: (vin: string, date?: string, signal?: AbortSignal) => { const params = new URLSearchParams(); if (date) params.set('date', date); const suffix = params.toString() ? `?${params.toString()}` : ''; return request(`/api/v2/vehicles/${encodeURIComponent(vin)}/source-evidence${suffix}`, withSignal(undefined, signal)); }, vehicleSourceDiagnostic: (vin: string, signal?: AbortSignal) => request( `/api/v2/operations/vehicles/${encodeURIComponent(vin)}/sources`, withSignal(undefined, signal) ), updateVehicleSourcePolicy: (vin: string, update: VehicleSourcePolicyUpdate) => request( `/api/v2/operations/vehicles/${encodeURIComponent(vin)}/sources/${encodeURIComponent(update.sourceRef)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: update.version, providerName: update.providerName, providerEvidence: update.providerEvidence, enabled: update.enabled, priority: update.priority, remark: update.remark }) } ), reconciliationSummary: (days = 30, signal?: AbortSignal) => request( `/api/v2/reconciliation/summary?days=${days}`, withSignal(undefined, signal) ), reconciliationIssues: (query: ReconciliationQuery, signal?: AbortSignal) => request>( '/api/v2/reconciliation/issues', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal) ), reconciliationAssignees: (search = '', signal?: AbortSignal) => request( `/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( `/api/v2/reconciliation/issues/${encodeURIComponent(id)}`, withSignal(undefined, signal) ), updateReconciliationIssue: (id: string, input: { version: number; status: string; note: string }) => request( `/api/v2/reconciliation/issues/${encodeURIComponent(id)}/actions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) } ), batchUpdateReconciliationIssues: (input: ReconciliationBatchActionRequest) => request( '/api/v2/reconciliation/issues/batch-actions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) } ), assignReconciliationIssue: (id: string, input: ReconciliationAssignmentRequest) => request( `/api/v2/reconciliation/issues/${encodeURIComponent(id)}/assignment`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) } ), batchAssignReconciliationIssues: (input: ReconciliationBatchAssignmentRequest) => request( '/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( `/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( `/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(`/api/v2/vehicles/${encodeURIComponent(vin)}/profile`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }), syncVehicleProfiles: (input: VehicleProfileSyncRequest, signal?: AbortSignal) => request('/api/v2/vehicle-profiles/sync', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }, signal)), vehicleServiceSummary: () => request('/api/vehicle-service/summary'), vehicleServiceOverview: (params = new URLSearchParams()) => request(`/api/vehicle-service/overview?${params.toString()}`), vehicleServiceOverviews: (query: VehicleOverviewBatchQuery) => request>('/api/vehicle-service/overviews', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }), vehicleRealtime: (params = new URLSearchParams(), signal?: AbortSignal) => request>(`/api/realtime/vehicles?${params.toString()}`, signal ? { signal } : undefined), realtimeLocations: (params = new URLSearchParams()) => request>(`/api/realtime/locations?${params.toString()}`), historyLocations: (params = new URLSearchParams()) => request>(`/api/history/locations?${params.toString()}`), rawFrames: (params = new URLSearchParams()) => request>(`/api/history/raw-frames?${params.toString()}`), rawFramesQuery: (query: RawFrameQuery) => request>('/api/history/raw-frames/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }), mileageSummary: (params = new URLSearchParams()) => request(`/api/mileage/summary?${params.toString()}`), dailyMileage: (query: MileageQuery, signal?: AbortSignal) => request>('/api/mileage/daily', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)), hydrogenDailyEvidence: (vin: string, date: string, signal?: AbortSignal) => request( `/api/v2/vehicles/${encodeURIComponent(vin)}/hydrogen-evidence?date=${encodeURIComponent(date)}`, withSignal(undefined, signal) ), mileageStatistics: (query: MileageQuery, signal?: AbortSignal) => request('/api/v2/statistics/mileage', withSignal({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(query) }, signal)), onlineStatisticsSummary: (params = new URLSearchParams()) => request(`/api/statistics/online-summary?${params.toString()}`), onlineVehicleStatuses: (params = new URLSearchParams()) => request>(`/api/statistics/online-vehicles?${params.toString()}`), qualitySummary: (params = new URLSearchParams()) => request(`/api/quality/summary?${params.toString()}`), qualityIssues: (params = new URLSearchParams()) => request>(`/api/quality/issues?${params.toString()}`), qualityNotificationPlan: (params = new URLSearchParams()) => request(`/api/quality/notification-plan?${params.toString()}`), alertEventSummary: (params = new URLSearchParams()) => request(`/api/alert-events/summary?${params.toString()}`), alertEvents: (params = new URLSearchParams()) => request>(`/api/alert-events?${params.toString()}`), alertEventNotificationPlan: (params = new URLSearchParams()) => request(`/api/alert-events/notification-plan?${params.toString()}`), reverseGeocode: (params = new URLSearchParams(), signal?: AbortSignal) => request(`/api/map/reverse-geocode?${params.toString()}`, withSignal(undefined, signal)), opsHealth: (signal?: AbortSignal) => request('/api/ops/health', withSignal(undefined, signal)).then(normalizeOpsHealth), sourceReadiness: (signal?: AbortSignal) => request('/api/ops/source-readiness', withSignal(undefined, signal)).then(normalizeSourceReadiness) };