export type User = { id: number; username: string; role: "admin" }; export type AuthState = { setupRequired: boolean; user: User | null; csrfToken: string; }; export type Availability = | { available: true; value: T } | { available: false; reason: string }; export type HeadUnitStatus = | { available: true; value: { state: "connected"; identity: { manufacturer: Availability; model: Availability; androidVersion: Availability; apiLevel: Availability; buildId: Availability; abi: Availability; }; display: { size: Availability<{ widthPixels: number; heightPixels: number }>; densityDpi: Availability; }; }; } | { available: false; state: "disabled" | "unavailable" | "offline" | "unauthorized" | "ambiguous"; reason: string; }; export type SystemStatus = { collectedAt: string; hostname: string; uptimeSeconds: number; operatingSystem: string; cpu: { model: string; loadAverage: number[]; temperatureCelsius: Availability; }; memory: { totalBytes: number; usedBytes: number; availableBytes: number }; disk: Availability<{ totalBytes: number; usedBytes: number; availableBytes: number; mount: string }>; network: { interfaces: Array<{ name: string; address: string; family: string }>; interfaceReason: string | null; wifi: Availability; }; service: { state: "healthy"; processUptimeSeconds: number }; headUnit: HeadUnitStatus; }; export type UpdateStatus = { state: "idle" | "checking" | "available" | "building" | "current" | "success" | "failed"; message: string; fromRevision: string | null; toRevision: string | null; updatedAt: string; installedRevision: string | null; supported: boolean; }; export type NetworkStatus = { supported: boolean; enabled: boolean; mode: "disabled" | "starting" | "dual_radio" | "built_in_upstream" | "probing_built_in" | "offline_hotspot" | "degraded" | "error"; message: string; updatedAt: string; lastTransitionAt: string; probeDeadline: string | null; lastFailure: string | null; connectivity: "full" | "limited" | "portal" | "none" | "unknown"; hotspotSsid: string | null; hotspot: NetworkAdapterStatus; upstream: NetworkAdapterStatus; }; export type NetworkAdapterStatus = { interface: string; available: boolean; connected: boolean; connection: string | null; address: string | null; }; export type NetworkActivity = { supported: boolean; collectedAt: string; clients: Array<{ ipAddress: string; macAddress: string; hostname: string | null; leaseExpiresAt: string | null; state: string; connected: boolean }>; dnsQueries: Array<{ timestamp: string; clientAddress: string; type: string; name: string }>; flows: Array<{ protocol: "tcp" | "udp"; state: string; sourceAddress: string; sourcePort: number; destinationAddress: string; destinationPort: number; packets: number | null; bytes: number | null }>; dnsAvailable: boolean; flowsAvailable: boolean; messages: string[]; }; export type InstalledAdbPackage = { packageName: string; apkPath: string; system: boolean; }; export type AdbTarget = { type: "component" | "package"; value: string }; export type AdbShortcut = AdbTarget & { id: number; name: string; createdAt: string; }; export type MobileDevice = { id: string; name: string; createdAt: string; lastSeenAt: string; revokedAt: string | null; }; export type BluetoothStatus = { supported: boolean; powered: boolean; discoverable: boolean; pairable: boolean; message: string; }; export type TripSummary = { id: number; deviceId: string; deviceName: string; startedAt: number; endedAt: number | null; active: boolean; durationSeconds: number; movingSeconds: number; distanceKm: number; pointCount: number; averageSpeedKph: number | null; peakSpeedKph: number | null; vehicleBatteryStartPercent: number | null; vehicleBatteryEndPercent: number | null; batteryConsumedPercent: number | null; energyConsumedKwh: number | null; phoneBatteryStartPercent: number | null; phoneBatteryEndPercent: number | null; start: { latitude: number; longitude: number }; end: { latitude: number; longitude: number }; }; export type TripPoint = { capturedAt: number; latitude: number; longitude: number; accuracyMeters: number; altitudeMeters: number | null; speedKph: number | null; bearingDegrees: number | null; vehicleBatteryPercent: number | null; }; export type TripPin = { type: "start" | "end" | "peak-speed"; latitude: number; longitude: number; capturedAt: number | null; speedKph?: number | null; }; export type TripDetail = TripSummary & { points: TripPoint[]; pins: TripPin[] }; export type CarTelemetry = { available: true; collectedAt: string; speedKph: Availability; gear: Availability; wheelAngleDegrees: Availability; batteryPercent: Availability; rangeKm: Availability; batteryVoltage: Availability; totalConsumptionKwh: Availability; chargingStatus: Availability; }; export type TelemetryPeriod = "24h" | "7d" | "30d" | "1y" | "all"; export type TelemetryHistoryPoint = { timestamp: number; speedKph: number | null; peakSpeedKph: number | null; batteryPercent: number | null; rangeKm: number | null; chargingPowerKw: number | null; }; export type CarStatistics = { latest: { recordedAt: number; speedKph: number | null; wheelAngleDegrees: number | null; batteryPercent: number | null; rangeKm: number | null; batteryVoltage: number | null; totalConsumptionKwh: number | null; chargingStatus: number | null; chargingPowerKw: number | null; } | null; records: { peakSpeedKph: number | null; peakSpeedAt: number | null; peakChargingPowerKw: number | null; peakChargingPowerAt: number | null; maxObservedRangeKm: number | null; maxObservedRangeAt: number | null; }; lifetime: { firstSampleAt: number | null; lastSampleAt: number | null; sampleCount: number; trackedDistanceKm: number; drivingSeconds: number; chargingSeconds: number; chargeSessions: number; socUsedPercent: number; averageMovingSpeedKph: number | null; ownRangeEstimateKm: number | null; }; storage: { rawSamples: number; hourlyRollups: number; oldestRawAt: number | null; oldestRollupAt: number | null; estimatedBytes: number; rawRetentionDays: number; rollupRetentionYears: number; rawSampleCap: number; }; }; type ErrorResponse = { error?: { code?: string; message?: string } }; export class ApiError extends Error { constructor( message: string, public readonly status: number, public readonly code: string ) { super(message); } } async function parseResponse(response: Response): Promise { const data = (await response.json().catch(() => ({}))) as T & ErrorResponse; if (!response.ok) { throw new ApiError(data.error?.message ?? "The request failed", response.status, data.error?.code ?? "REQUEST_FAILED"); } return data; } export async function getAuthState(): Promise { return parseResponse(await fetch("/api/auth/state", { credentials: "same-origin" })); } export async function post(path: string, body: unknown, csrfToken: string): Promise { return parseResponse( await fetch(path, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, body: JSON.stringify(body) }) ); } export async function getStatus(): Promise { return parseResponse(await fetch("/api/status", { credentials: "same-origin" })); } export async function getUpdateStatus(): Promise { return parseResponse(await fetch("/api/system/update", { credentials: "same-origin" })); } export async function getNetworkStatus(): Promise { return parseResponse(await fetch("/api/network", { credentials: "same-origin" })); } export async function getNetworkActivity(): Promise { return parseResponse(await fetch("/api/network/activity", { credentials: "same-origin" })); } export async function getAdbPackages(): Promise { const response = await parseResponse<{ packages: InstalledAdbPackage[] }>( await fetch("/api/adb/packages", { credentials: "same-origin" }) ); return response.packages; } export async function getAdbShortcuts(): Promise { const response = await parseResponse<{ shortcuts: AdbShortcut[] }>( await fetch("/api/adb/shortcuts", { credentials: "same-origin" }) ); return response.shortcuts; } export async function getMobileDevices(): Promise { const response = await parseResponse<{ devices: MobileDevice[] }>( await fetch("/api/mobile/devices", { credentials: "same-origin" }) ); return response.devices; } export async function getBluetoothStatus(): Promise { return parseResponse(await fetch("/api/mobile/bluetooth", { credentials: "same-origin" })); } export async function getTrips(): Promise { const response = await parseResponse<{ trips: TripSummary[] }>( await fetch("/api/trips", { credentials: "same-origin" }) ); return response.trips; } export async function getTrip(id: number): Promise { return parseResponse(await fetch(`/api/trips/${id}`, { credentials: "same-origin" })); } export async function revokeMobileDevice(id: string, csrfToken: string): Promise { await parseResponse(await fetch(`/api/mobile/devices/${encodeURIComponent(id)}`, { method: "DELETE", credentials: "same-origin", headers: { "X-CSRF-Token": csrfToken } })); } export async function uploadApk(file: File, csrfToken: string): Promise { await parseResponse( await fetch("/api/adb/install", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/vnd.android.package-archive", "X-CSRF-Token": csrfToken }, body: file }) ); } export async function deleteAdbShortcut(id: number, csrfToken: string): Promise { await parseResponse( await fetch(`/api/adb/shortcuts/${id}`, { method: "DELETE", credentials: "same-origin", headers: { "X-CSRF-Token": csrfToken } }) ); } export async function getCarTelemetry(): Promise { return parseResponse(await fetch("/api/car/telemetry", { credentials: "same-origin" })); } export async function getCarHistory(period: TelemetryPeriod): Promise<{ period: TelemetryPeriod; points: TelemetryHistoryPoint[] }> { return parseResponse(await fetch(`/api/car/history?period=${period}`, { credentials: "same-origin" })); } export async function getCarStatistics(): Promise { return parseResponse(await fetch("/api/car/statistics", { credentials: "same-origin" })); } export type ClearTelemetryMode = "history" | "all"; export async function clearCarStatistics(mode: ClearTelemetryMode, csrfToken: string): Promise { await post("/api/car/history/clear", { mode }, csrfToken); }