171 lines
6 KiB
JavaScript
171 lines
6 KiB
JavaScript
|
|
/**
|
||
|
|
* 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));
|
||
|
|
}
|