381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
export type User = { id: number; username: string; role: "admin" };
|
|
|
|
export type AuthState = {
|
|
setupRequired: boolean;
|
|
user: User | null;
|
|
csrfToken: string;
|
|
};
|
|
|
|
export type Availability<T> =
|
|
| { available: true; value: T }
|
|
| { available: false; reason: string };
|
|
|
|
export type HeadUnitStatus =
|
|
| {
|
|
available: true;
|
|
value: {
|
|
state: "connected";
|
|
identity: {
|
|
manufacturer: Availability<string>;
|
|
model: Availability<string>;
|
|
androidVersion: Availability<string>;
|
|
apiLevel: Availability<number>;
|
|
buildId: Availability<string>;
|
|
abi: Availability<string>;
|
|
};
|
|
display: {
|
|
size: Availability<{ widthPixels: number; heightPixels: number }>;
|
|
densityDpi: Availability<number>;
|
|
};
|
|
};
|
|
}
|
|
| {
|
|
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<number>;
|
|
};
|
|
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<string>;
|
|
};
|
|
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<number>;
|
|
gear: Availability<string>;
|
|
wheelAngleDegrees: Availability<number>;
|
|
batteryPercent: Availability<number>;
|
|
rangeKm: Availability<number>;
|
|
batteryVoltage: Availability<number>;
|
|
totalConsumptionKwh: Availability<number>;
|
|
chargingStatus: Availability<number>;
|
|
};
|
|
|
|
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<T>(response: Response): Promise<T> {
|
|
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<AuthState> {
|
|
return parseResponse<AuthState>(await fetch("/api/auth/state", { credentials: "same-origin" }));
|
|
}
|
|
|
|
export async function post<T>(path: string, body: unknown, csrfToken: string): Promise<T> {
|
|
return parseResponse<T>(
|
|
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<SystemStatus> {
|
|
return parseResponse<SystemStatus>(await fetch("/api/status", { credentials: "same-origin" }));
|
|
}
|
|
|
|
export async function getUpdateStatus(): Promise<UpdateStatus> {
|
|
return parseResponse<UpdateStatus>(await fetch("/api/system/update", { credentials: "same-origin" }));
|
|
}
|
|
|
|
export async function getNetworkStatus(): Promise<NetworkStatus> {
|
|
return parseResponse<NetworkStatus>(await fetch("/api/network", { credentials: "same-origin" }));
|
|
}
|
|
|
|
export async function getNetworkActivity(): Promise<NetworkActivity> {
|
|
return parseResponse<NetworkActivity>(await fetch("/api/network/activity", { credentials: "same-origin" }));
|
|
}
|
|
|
|
export async function getAdbPackages(): Promise<InstalledAdbPackage[]> {
|
|
const response = await parseResponse<{ packages: InstalledAdbPackage[] }>(
|
|
await fetch("/api/adb/packages", { credentials: "same-origin" })
|
|
);
|
|
return response.packages;
|
|
}
|
|
|
|
export async function getAdbShortcuts(): Promise<AdbShortcut[]> {
|
|
const response = await parseResponse<{ shortcuts: AdbShortcut[] }>(
|
|
await fetch("/api/adb/shortcuts", { credentials: "same-origin" })
|
|
);
|
|
return response.shortcuts;
|
|
}
|
|
|
|
export async function getMobileDevices(): Promise<MobileDevice[]> {
|
|
const response = await parseResponse<{ devices: MobileDevice[] }>(
|
|
await fetch("/api/mobile/devices", { credentials: "same-origin" })
|
|
);
|
|
return response.devices;
|
|
}
|
|
|
|
export async function getBluetoothStatus(): Promise<BluetoothStatus> {
|
|
return parseResponse<BluetoothStatus>(await fetch("/api/mobile/bluetooth", { credentials: "same-origin" }));
|
|
}
|
|
|
|
export async function getTrips(): Promise<TripSummary[]> {
|
|
const response = await parseResponse<{ trips: TripSummary[] }>(
|
|
await fetch("/api/trips", { credentials: "same-origin" })
|
|
);
|
|
return response.trips;
|
|
}
|
|
|
|
export async function getTrip(id: number): Promise<TripDetail> {
|
|
return parseResponse<TripDetail>(await fetch(`/api/trips/${id}`, { credentials: "same-origin" }));
|
|
}
|
|
|
|
export async function revokeMobileDevice(id: string, csrfToken: string): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
await parseResponse(
|
|
await fetch(`/api/adb/shortcuts/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "same-origin",
|
|
headers: { "X-CSRF-Token": csrfToken }
|
|
})
|
|
);
|
|
}
|
|
|
|
export async function getCarTelemetry(): Promise<CarTelemetry> {
|
|
return parseResponse<CarTelemetry>(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<CarStatistics> {
|
|
return parseResponse<CarStatistics>(await fetch("/api/car/statistics", { credentials: "same-origin" }));
|
|
}
|
|
|
|
export type ClearTelemetryMode = "history" | "all";
|
|
|
|
export async function clearCarStatistics(mode: ClearTelemetryMode, csrfToken: string): Promise<void> {
|
|
await post("/api/car/history/clear", { mode }, csrfToken);
|
|
}
|