Add GPS trip reports and route maps

This commit is contained in:
2026-08-01 00:56:19 +02:00
parent cb0ac06091
commit e501895e56
12 changed files with 817 additions and 21 deletions
+46 -14
View File
@@ -8,6 +8,7 @@ import type { CarTelemetryRecorder } from "./car-telemetry.js";
import type { CompanionDatabase } from "./database.js";
import { hashToken, randomToken } from "./security.js";
import { collectSystemStatus } from "./status.js";
import { TripStore } from "./trips.js";
const pairingSchema = z.object({
code: z.string().regex(/^\d{6}$/),
@@ -71,6 +72,8 @@ export function registerMobileRoutes(
carTelemetry: CarTelemetryRecorder,
requireWebUser: (request: FastifyRequest, reply: FastifyReply) => boolean
): void {
const trips = new TripStore(database);
app.post("/api/mobile/pairing", async (request, reply) => {
if (!requireWebUser(request, reply)) return;
const code = String(randomInt(100_000, 1_000_000));
@@ -94,6 +97,22 @@ export function registerMobileRoutes(
return { devices };
});
app.get("/api/trips", async (request, reply) => {
if (!requireWebUser(request, reply)) return;
return trips.list();
});
app.get<{ Params: { id: string } }>("/api/trips/:id", async (request, reply) => {
if (!requireWebUser(request, reply)) return;
const id = Number(request.params.id);
if (!Number.isInteger(id) || id < 1) {
return reply.code(400).send({ error: { code: "INVALID_TRIP", message: "That trip is invalid" } });
}
const trip = trips.detail(id);
if (!trip) return reply.code(404).send({ error: { code: "TRIP_NOT_FOUND", message: "Trip not found" } });
return trip;
});
app.get("/api/mobile/bluetooth", async (request, reply) => {
if (!requireWebUser(request, reply)) return;
return readBluetoothStatus();
@@ -170,20 +189,33 @@ export function registerMobileRoutes(
const parsed = locationSchema.safeParse(request.body);
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_PHONE_LOCATION", message: "The phone location was invalid" } });
const value = parsed.data;
database.prepare(
`INSERT INTO mobile_phone_state
(device_id, received_at, captured_at, latitude, longitude, accuracy_meters, altitude_meters,
speed_kph, bearing_degrees, battery_percent, network_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(device_id) DO UPDATE SET
received_at = excluded.received_at, captured_at = excluded.captured_at,
latitude = excluded.latitude, longitude = excluded.longitude, accuracy_meters = excluded.accuracy_meters,
altitude_meters = excluded.altitude_meters, speed_kph = excluded.speed_kph,
bearing_degrees = excluded.bearing_degrees, battery_percent = excluded.battery_percent,
network_type = excluded.network_type`
).run(device.id, new Date().toISOString(), value.capturedAt, value.latitude, value.longitude, value.accuracyMeters,
value.altitudeMeters ?? null, value.speedKph ?? null, value.bearingDegrees ?? null,
value.batteryPercent ?? null, value.networkType ?? null);
return { accepted: true };
const trip = database.transaction(() => {
database.prepare(
`INSERT INTO mobile_phone_state
(device_id, received_at, captured_at, latitude, longitude, accuracy_meters, altitude_meters,
speed_kph, bearing_degrees, battery_percent, network_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(device_id) DO UPDATE SET
received_at = excluded.received_at, captured_at = excluded.captured_at,
latitude = excluded.latitude, longitude = excluded.longitude, accuracy_meters = excluded.accuracy_meters,
altitude_meters = excluded.altitude_meters, speed_kph = excluded.speed_kph,
bearing_degrees = excluded.bearing_degrees, battery_percent = excluded.battery_percent,
network_type = excluded.network_type`
).run(device.id, new Date().toISOString(), value.capturedAt, value.latitude, value.longitude, value.accuracyMeters,
value.altitudeMeters ?? null, value.speedKph ?? null, value.bearingDegrees ?? null,
value.batteryPercent ?? null, value.networkType ?? null);
return trips.record({
deviceId: device.id,
capturedAt: value.capturedAt,
latitude: value.latitude,
longitude: value.longitude,
accuracyMeters: value.accuracyMeters,
altitudeMeters: value.altitudeMeters ?? null,
speedKph: value.speedKph ?? null,
bearingDegrees: value.bearingDegrees ?? null,
phoneBatteryPercent: value.batteryPercent ?? null
});
})();
return { accepted: true, trip };
});
app.post<{ Params: { id: string } }>("/internal/mobile/shortcuts/:id/launch", async (request, reply) => {