/** * FleetAI probe — run this BEFORE building anything on top of the tracker. * * node scripts/fleetai-probe.mjs * * Reads FLEETAI_* from .env. Read-only: authenticates, lists vehicles, * pulls one fleet-wide position snapshot, prints what came back. * * It exists to settle three assumptions that the whole rep check-in * design rests on and that no amount of reading the PDF can answer: * * 1. Does each rep have a dedicated vehicle, under a registration we * can map to a WFDASH user? If reps share a bakkie, * `users.tracker_vehicle_id` is the wrong model. * 2. Does the response carry `GPSActualTime` (the real GPS fix) or * only `Datetime` / `timestamp` (when the platform received it)? * That decides how honest the check-in staleness warning can be. * 3. Is FLEETAI_COMPANY_NAME spelled exactly as the platform has it? * A mismatch returns "No Company Found For Given User", which * otherwise reads as "no vehicles today". * * Throwaway by design — delete it once the answers are known. */ import { readFileSync } from "node:fs"; function loadEnv() { let raw = ""; try { raw = readFileSync(".env", "utf8"); } catch { console.warn("No .env found — falling back to process.env only.\n"); } const fromFile = Object.fromEntries( raw .split("\n") .filter((line) => line.includes("=") && !line.trim().startsWith("#")) .map((line) => { const i = line.indexOf("="); return [line.slice(0, i).trim(), line.slice(i + 1).trim()]; }) ); return { ...fromFile, ...process.env }; } const env = loadEnv(); const BASE = (env.FLEETAI_BASE_URL || "https://fleetai.netstar.co.za").replace(/\/$/, ""); const USERNAME = env.FLEETAI_USERNAME; const PASSWORD = env.FLEETAI_PASSWORD; const COMPANY = env.FLEETAI_COMPANY_NAME; const PROJECT_ID = env.FLEETAI_PROJECT_ID || "37"; if (!USERNAME || !PASSWORD) { console.error( "Set FLEETAI_USERNAME and FLEETAI_PASSWORD in .env first.\n" + "The account must be Company or Company Sub-Account level —\n" + "FleetAI rejects Admin and Reseller logins outright." ); process.exit(1); } /** * FleetAI returns HTTP 200 for errors too, so every response body has to * be inspected rather than trusting the status. */ async function post(endpoint, payload, headers = {}) { const url = `${BASE}/webservice?token=${endpoint}`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify(payload), }); const text = await res.text(); let body; try { body = JSON.parse(text); } catch { console.error(`\n✗ ${endpoint} returned non-JSON (HTTP ${res.status}):`); console.error(text.slice(0, 400)); process.exit(1); } const failed = String(body?.RESULT ?? body?.result ?? "").toUpperCase() === "FAIL" || body?.root?.error; if (failed) { console.error(`\n✗ ${endpoint} failed:`); console.error(JSON.stringify(body, null, 2).slice(0, 800)); process.exit(1); } return body; } console.log(`FleetAI probe → ${BASE}\n`); /* 1. Authenticate ---------------------------------------------------- */ const auth = await post("generateAccessToken", { username: USERNAME, password: PASSWORD, }); const token = auth?.token; if (!token) { console.error("✗ No token in the response:"); console.error(JSON.stringify(auth, null, 2).slice(0, 800)); console.error( "\nMost likely cause: the account is Admin or Reseller level.\n" + "FleetAI only accepts Company or Company Sub-Account logins." ); process.exit(1); } console.log("✓ Authenticated (token is 30-minute; refresh at 25)\n"); /* 2. Vehicle registry ------------------------------------------------ */ const registry = await post("viewVehicle", { project_id: PROJECT_ID }, { "auth-code": token }); const vehicles = Array.isArray(registry?.data) ? registry.data : []; console.log(`── QUESTION 1: which vehicles exist? (${vehicles.length} found)`); if (vehicles.length === 0) { console.log(" (none — check project_id, currently " + PROJECT_ID + ")"); } for (const v of vehicles) { console.log( ` ${String(v.plate_no ?? "?").padEnd(14)} imei=${String(v.imei_no ?? "?").padEnd(18)} ${v.device_name ?? ""}` ); } console.log( "\n → Can each of Chris / Tiaan / George / Calvin be matched to exactly\n" + " one registration above? If they share vehicles, tell me — the\n" + " rep→vehicle model changes.\n" ); /* 3. Fleet-wide live snapshot ---------------------------------------- */ if (!COMPANY) { console.log("── QUESTION 3: FLEETAI_COMPANY_NAME is not set, skipping the fleet poll."); console.log(" The poller needs it. Set it and re-run to verify the spelling.\n"); process.exit(0); } const live = await post( `getTokenBaseLiveData&ProjectId=${encodeURIComponent(PROJECT_ID)}`, { company_names: COMPANY, format: "json" }, { "auth-code": token } ); const rows = live?.root?.VehicleData ?? live?.data ?? []; const list = Array.isArray(rows) ? rows : [rows]; console.log(`── QUESTION 3: company name "${COMPANY}" accepted ✓ (${list.length} rows)\n`); console.log("── QUESTION 2: which timestamp fields come back?\n"); const first = list[0]; if (!first) { console.log(" (no rows — vehicles may all be INACTIVE)"); } else { for (const key of ["Vehicle_No", "Status", "Speed", "Latitude", "Longitude", "IGN"]) { console.log(` ${key.padEnd(16)} ${JSON.stringify(first[key])}`); } console.log(""); for (const key of ["GPSActualTime", "Datetime", "timestamp"]) { const present = key in first ? JSON.stringify(first[key]) : "— ABSENT —"; console.log(` ${key.padEnd(16)} ${present}`); } console.log( "\n → GPSActualTime present means the staleness warning reflects the\n" + " vehicle. Only Datetime means it reflects the network instead.\n" + " → Timestamps should read as UTC+2 (SAST). If they look like UTC,\n" + " set FLEETAI_TZ_OFFSET_MINUTES=0.\n" ); console.log("── Full first row, for reference:\n"); console.log(JSON.stringify(first, null, 2)); }