feat: модуль GPS-мониторинга (карта Яндекс.Карт, Traccar, геозоны, маршруты, группы)

This commit is contained in:
2026-08-24 19:30:11 +03:00
parent c0e2ddd9c8
commit 104b3ca422
33 changed files with 4108 additions and 4 deletions

View File

@@ -0,0 +1,95 @@
/**
* Клиент Traccar REST API.
* Настройка через env: TRACCAR_API_URL, TRACCAR_USER, TRACCAR_PASSWORD.
* Если переменные не заданы — все методы тихо no-op с логом,
* чтобы GPS-модуль работал и без Traccar (например, только PWA-трекинг).
*/
const TRACCAR_API_URL = process.env.TRACCAR_API_URL;
const TRACCAR_USER = process.env.TRACCAR_USER;
const TRACCAR_PASSWORD = process.env.TRACCAR_PASSWORD;
function isConfigured(): boolean {
return Boolean(TRACCAR_API_URL && TRACCAR_USER && TRACCAR_PASSWORD);
}
function authHeaders(): Record<string, string> {
const basic = Buffer.from(`${TRACCAR_USER}:${TRACCAR_PASSWORD}`).toString("base64");
return {
Authorization: `Basic ${basic}`,
"Content-Type": "application/json",
};
}
interface TraccarDevice {
id: number;
uniqueId: string;
name: string;
}
/** Найти устройство по uniqueId; возвращает null, если не найдено */
async function findDevice(uniqueId: string): Promise<TraccarDevice | null> {
const url = `${TRACCAR_API_URL}/api/devices?uniqueId=${encodeURIComponent(uniqueId)}`;
const res = await fetch(url, { headers: authHeaders() });
if (!res.ok) {
throw new Error(`Traccar GET /api/devices: HTTP ${res.status}`);
}
const devices = (await res.json()) as TraccarDevice[];
return devices.find((d) => d.uniqueId === uniqueId) ?? null;
}
/**
* Создаёт устройство в Traccar, если его ещё нет.
* Возвращает id устройства или null при ошибке/отсутствии конфигурации.
*/
export async function ensureDevice(uniqueId: string, name: string): Promise<number | null> {
if (!isConfigured()) {
console.log("[GPS] Traccar не настроен (TRACCAR_API_URL/USER/PASSWORD), ensureDevice пропущен");
return null;
}
try {
const existing = await findDevice(uniqueId);
if (existing) return existing.id;
const res = await fetch(`${TRACCAR_API_URL}/api/devices`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ uniqueId, name }),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Traccar POST /api/devices: HTTP ${res.status} ${text}`);
}
const device = (await res.json()) as TraccarDevice;
console.log(`[GPS] Traccar: создано устройство '${name}' (${uniqueId}), id=${device.id}`);
return device.id;
} catch (err) {
console.error("[GPS] Traccar ensureDevice error:", err);
return null;
}
}
/** Удаляет устройство из Traccar по uniqueId (если существует) */
export async function deleteDevice(uniqueId: string): Promise<void> {
if (!isConfigured()) {
console.log("[GPS] Traccar не настроен, deleteDevice пропущен");
return;
}
try {
const existing = await findDevice(uniqueId);
if (!existing) return;
const res = await fetch(`${TRACCAR_API_URL}/api/devices/${existing.id}`, {
method: "DELETE",
headers: authHeaders(),
});
if (!res.ok && res.status !== 404) {
throw new Error(`Traccar DELETE /api/devices/${existing.id}: HTTP ${res.status}`);
}
console.log(`[GPS] Traccar: удалено устройство ${uniqueId}`);
} catch (err) {
console.error("[GPS] Traccar deleteDevice error:", err);
}
}