From 58f59e26da17ebafbc269b83661faab0163000fe Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Wed, 11 Feb 2026 10:31:23 -0800
Subject: [PATCH 01/10] fix: rename "GPS Accuracy" to "Location Accuracy"
---
src/MapToggles.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/MapToggles.js b/src/MapToggles.js
index 443d988..7040180 100644
--- a/src/MapToggles.js
+++ b/src/MapToggles.js
@@ -6,7 +6,7 @@ import Utils, { log } from "./Utils";
export const ALL_TOGGLES = [
{
id: "showGPSBubbles",
- name: "GPS Accuracy",
+ name: "Location Accuracy",
docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/GPSAccuracy.md",
columns: ["lastlocation.rawlocationaccuracy", "lastlocation.locationsensor"],
solutionTypes: ["ODRD", "LMFS"],
From af12bb67cff729b8e006c8275e2d573e7862d771 Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Wed, 11 Feb 2026 14:41:32 -0800
Subject: [PATCH 02/10] feat: Add Google Sheet Export and Import
---
src/App.js | 26 ++++
src/DatasetLoading.js | 88 ++++++++++++
src/GoogleSheets.js | 304 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 418 insertions(+)
create mode 100644 src/GoogleSheets.js
diff --git a/src/App.js b/src/App.js
index ef1e15d..e76629e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -17,6 +17,7 @@ import {
saveDatasetAsJson,
saveToIndexedDB,
} from "./localStorage";
+import { exportToGoogleSheet, requestSheetsToken } from "./GoogleSheets";
import _ from "lodash";
import { getQueryStringValue, setQueryStringValue } from "./queryString";
import "./global.css";
@@ -527,6 +528,28 @@ class App extends React.Component {
}
};
+ const handleGoogleSheetExport = async (e) => {
+ e.stopPropagation();
+ log(`Google Sheet export initiated for dataset ${index}`);
+ this.setState({ activeMenuIndex: null });
+
+ try {
+ const token = await requestSheetsToken();
+ const sheetUrl = await exportToGoogleSheet(index, token);
+ toast.success(
+
+ Exported to{" "}
+
+ Google Sheet
+
+
+ );
+ } catch (error) {
+ log(`Error exporting to Google Sheet: ${error.message}`, error);
+ toast.error(`Google Sheet export failed: ${error.message}`);
+ }
+ };
+
const handlePruneClick = async (e) => {
e.stopPropagation();
log(`Prune initiated for dataset ${index}`);
@@ -683,6 +706,9 @@ class App extends React.Component {
Export
+
+ Google Sheet
+
Prune
diff --git a/src/DatasetLoading.js b/src/DatasetLoading.js
index d882b48..1e96eb4 100644
--- a/src/DatasetLoading.js
+++ b/src/DatasetLoading.js
@@ -5,12 +5,16 @@ import ExtraDataSource from "./ExtraDataSource";
import { log } from "./Utils";
import { toast } from "react-toastify";
import { isTokenValid, fetchLogsWithToken, useCloudLoggingLogin, buildQueryFilter } from "./CloudLogging";
+import { useSheetsLogin, isSheetsTokenValid, getSheetsToken, importFromGoogleSheet } from "./GoogleSheets";
import { HAS_EXTRA_DATA_SOURCE } from "./constants";
const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
const getStoredValue = (key, defaultValue = "") => localStorage.getItem(`datasetLoading_${key}`) || defaultValue;
const [fetching, setFetching] = useState(false);
+ const [sheetFormVisible, setSheetFormVisible] = useState(false);
+ const [sheetUrl, setSheetUrl] = useState(localStorage.getItem("datasetLoading_sheetUrl") || "");
+ const [sheetLoading, setSheetLoading] = useState(false);
const [queryParams, setQueryParams] = useState({
projectId: getStoredValue("projectId"),
vehicleId: getStoredValue("vehicleId"),
@@ -65,6 +69,52 @@ const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
}
};
+ const handleSheetImport = (token) => {
+ setSheetLoading(true);
+ setLocalError(null);
+ localStorage.setItem("datasetLoading_sheetUrl", sheetUrl);
+
+ importFromGoogleSheet(sheetUrl, token)
+ .then((logs) => {
+ log(`Received ${logs.length} logs from Google Sheet`);
+ if (logs.length > 0) {
+ onLogsReceived(logs);
+ } else {
+ toast.warning("No logs found in the spreadsheet.");
+ }
+ })
+ .catch((err) => {
+ setLocalError(`Sheet import error: ${err.message}`);
+ toast.error(`Sheet import error: ${err.message}`);
+ })
+ .finally(() => setSheetLoading(false));
+ };
+
+ const sheetsLogin = useSheetsLogin(
+ (token) => {
+ log("Sheets login successful, importing...");
+ handleSheetImport(token);
+ },
+ (err) => {
+ log("Sheets login failed.", err);
+ setLocalError(`Auth Error: ${err.error || "Unknown"}`);
+ setSheetLoading(false);
+ }
+ );
+
+ const handleSheetLoadClick = () => {
+ if (!sheetUrl.trim()) {
+ setLocalError("Please enter a spreadsheet URL or ID.");
+ return;
+ }
+ setLocalError(null);
+ if (isSheetsTokenValid()) {
+ handleSheetImport(getSheetsToken());
+ } else {
+ sheetsLogin();
+ }
+ };
+
return (
Fleet Engine Logs Loading
@@ -165,11 +215,49 @@ const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
+
+ {sheetFormVisible && (
+
+
+
+
+
+ {sheetLoading && (
+
+
Loading from Google Sheet...
+
+
+ )}
+
+ )}
);
};
diff --git a/src/GoogleSheets.js b/src/GoogleSheets.js
new file mode 100644
index 0000000..111275f
--- /dev/null
+++ b/src/GoogleSheets.js
@@ -0,0 +1,304 @@
+// src/GoogleSheets.js
+import { useGoogleLogin } from "@react-oauth/google";
+import { getUploadedData } from "./localStorage";
+import { log } from "./Utils";
+import _ from "lodash";
+
+const SHEETS_API_BASE = "https://sheets.googleapis.com/v4/spreadsheets";
+const CLIENT_ID = "829183678942-eq2c9cd7pjdm39l2um5thgbrvgva07e7.apps.googleusercontent.com";
+const SCOPES = "https://www.googleapis.com/auth/spreadsheets";
+const TOKEN_EXPIRY_BUFFER = 5 * 60 * 1000;
+
+const API_TYPE_REGEX_MAP = [
+ { name: "createVehicle", regex: /createVehicle/i },
+ { name: "getVehicle", regex: /getVehicle/i },
+ { name: "updateVehicle", regex: /updateVehicle/i },
+ { name: "createDeliveryVehicle", regex: /createDeliveryVehicle/i },
+ { name: "getDeliveryVehicle", regex: /getDeliveryVehicle/i },
+ { name: "updateDeliveryVehicle", regex: /updateDeliveryVehicle/i },
+ { name: "createTrip", regex: /createTrip/i },
+ { name: "getTrip", regex: /getTrip/i },
+ { name: "updateTrip", regex: /updateTrip/i },
+ { name: "createTask", regex: /createTask/i },
+ { name: "getTask", regex: /getTask/i },
+ { name: "updateTask", regex: /updateTask/i },
+];
+
+function getApiType(logEntry) {
+ const typeField = logEntry["@type"] || logEntry.jsonpayload?.["@type"] || "";
+ for (const { name, regex } of API_TYPE_REGEX_MAP) {
+ if (regex.test(typeField)) return name;
+ }
+ return "other";
+}
+
+// --- Token Management ---
+
+export const isSheetsTokenValid = () => {
+ const token = sessionStorage.getItem("sheets_token");
+ if (!token) return false;
+ const expiry = parseInt(sessionStorage.getItem("sheets_token_expiry") || "0", 10);
+ return expiry > Date.now() + TOKEN_EXPIRY_BUFFER;
+};
+
+export const getSheetsToken = () => sessionStorage.getItem("sheets_token");
+
+const storeSheetsToken = (accessToken, expiresIn) => {
+ const expiryTime = Date.now() + (expiresIn || 3600) * 1000;
+ sessionStorage.setItem("sheets_token", accessToken);
+ sessionStorage.setItem("sheets_token_expiry", expiryTime.toString());
+};
+
+/**
+ * Acquires an OAuth token for Google Sheets using the GIS token client.
+ * Works outside of React context / GoogleOAuthProvider.
+ */
+export function requestSheetsToken() {
+ return new Promise((resolve, reject) => {
+ if (isSheetsTokenValid()) {
+ resolve(getSheetsToken());
+ return;
+ }
+
+ if (!window.google?.accounts?.oauth2) {
+ reject(new Error("Google Identity Services not loaded. Please refresh and try again."));
+ return;
+ }
+
+ const tokenClient = window.google.accounts.oauth2.initTokenClient({
+ client_id: CLIENT_ID,
+ scope: SCOPES,
+ callback: (tokenResponse) => {
+ if (tokenResponse.error) {
+ reject(new Error(`Auth error: ${tokenResponse.error}`));
+ return;
+ }
+ storeSheetsToken(tokenResponse.access_token, tokenResponse.expires_in);
+ resolve(tokenResponse.access_token);
+ },
+ error_callback: (error) => {
+ reject(new Error(`Auth error: ${error.message || "Unknown"}`));
+ },
+ });
+
+ tokenClient.requestAccessToken();
+ });
+}
+
+/**
+ * Hook for use inside GoogleOAuthProvider (DatasetLoading dialog).
+ */
+export const useSheetsLogin = (onSuccess, onError) => {
+ return useGoogleLogin({
+ onSuccess: (tokenResponse) => {
+ log("Google Sheets OAuth login successful");
+ storeSheetsToken(tokenResponse.access_token, tokenResponse.expires_in);
+ if (onSuccess) onSuccess(tokenResponse.access_token);
+ },
+ onError: (error) => {
+ log("Google Sheets OAuth login error", error);
+ if (onError) onError(error);
+ },
+ flow: "implicit",
+ scope: SCOPES,
+ });
+};
+
+// --- Flatten / Unflatten ---
+
+function flattenObject(obj, prefix = "") {
+ const result = {};
+ for (const key of Object.keys(obj)) {
+ const fullPath = prefix ? `${prefix}.${key}` : key;
+ const value = obj[key];
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
+ Object.assign(result, flattenObject(value, fullPath));
+ } else {
+ result[fullPath] = value;
+ }
+ }
+ return result;
+}
+
+function collectAllKeys(logs) {
+ const keysSet = new Set();
+ for (const entry of logs) {
+ const flat = flattenObject(entry);
+ for (const key of Object.keys(flat)) {
+ keysSet.add(key);
+ }
+ }
+ const keys = Array.from(keysSet);
+ keys.sort();
+ const tsIndex = keys.indexOf("timestamp");
+ if (tsIndex > 0) {
+ keys.splice(tsIndex, 1);
+ keys.unshift("timestamp");
+ }
+ return keys;
+}
+
+function toCellValue(value) {
+ if (value === null || value === undefined) return "";
+ if (Array.isArray(value)) return JSON.stringify(value);
+ if (typeof value === "object") return JSON.stringify(value);
+ return value;
+}
+
+function fromCellValue(value) {
+ if (value === "" || value === null || value === undefined) return undefined;
+ if (typeof value === "string") {
+ if (value.startsWith("[") || value.startsWith("{")) {
+ try {
+ return JSON.parse(value);
+ } catch {
+ return value;
+ }
+ }
+ const num = Number(value);
+ if (!isNaN(num) && value.trim() !== "") return num;
+ }
+ return value;
+}
+
+// --- Sheets API Helpers ---
+
+async function sheetsApiFetch(url, token, options = {}) {
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ ...(options.headers || {}),
+ },
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(`Sheets API error (${response.status}): ${errorData.error?.message || JSON.stringify(errorData)}`);
+ }
+ return response.json();
+}
+
+// --- Export ---
+
+export async function exportToGoogleSheet(index, token) {
+ log(`Exporting dataset ${index} to Google Sheet`);
+ const data = await getUploadedData(index);
+
+ if (!data?.rawLogs?.length) {
+ throw new Error("No data available to export");
+ }
+
+ const grouped = {};
+ for (const entry of data.rawLogs) {
+ const apiType = getApiType(entry);
+ if (!grouped[apiType]) grouped[apiType] = [];
+ grouped[apiType].push(entry);
+ }
+
+ const sheetNames = Object.keys(grouped).sort();
+ log(`Grouped logs into ${sheetNames.length} API types: ${sheetNames.join(", ")}`);
+
+ const sheets = sheetNames.map((name) => ({ properties: { title: name } }));
+
+ const date = new Date().toISOString().split("T")[0];
+ const spreadsheet = await sheetsApiFetch(SHEETS_API_BASE, token, {
+ method: "POST",
+ body: JSON.stringify({
+ properties: { title: `Fleet Debugger Export - ${date}` },
+ sheets,
+ }),
+ });
+
+ const spreadsheetId = spreadsheet.spreadsheetId;
+ log(`Created spreadsheet: ${spreadsheetId}`);
+
+ const valueRanges = [];
+ for (const sheetName of sheetNames) {
+ const logs = grouped[sheetName];
+ const headers = collectAllKeys(logs);
+ const rows = [headers];
+
+ for (const entry of logs) {
+ const flat = flattenObject(entry);
+ const row = headers.map((h) => toCellValue(flat[h]));
+ rows.push(row);
+ }
+
+ valueRanges.push({
+ range: `'${sheetName}'!A1`,
+ values: rows,
+ });
+ }
+
+ await sheetsApiFetch(`${SHEETS_API_BASE}/${spreadsheetId}/values:batchUpdate`, token, {
+ method: "POST",
+ body: JSON.stringify({
+ valueInputOption: "RAW",
+ data: valueRanges,
+ }),
+ });
+
+ const sheetUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}`;
+ log(`Export complete: ${sheetUrl}`);
+ return sheetUrl;
+}
+
+// --- Import ---
+
+function extractSpreadsheetId(input) {
+ const trimmed = input.trim();
+ const urlMatch = trimmed.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
+ if (urlMatch) return urlMatch[1];
+ if (/^[a-zA-Z0-9-_]+$/.test(trimmed)) return trimmed;
+ throw new Error("Invalid spreadsheet URL or ID");
+}
+
+export async function importFromGoogleSheet(spreadsheetInput, token) {
+ const spreadsheetId = extractSpreadsheetId(spreadsheetInput);
+ log(`Importing from spreadsheet: ${spreadsheetId}`);
+
+ const metadata = await sheetsApiFetch(`${SHEETS_API_BASE}/${spreadsheetId}?fields=sheets.properties.title`, token);
+
+ const sheetNames = metadata.sheets.map((s) => s.properties.title);
+ log(`Found ${sheetNames.length} tabs: ${sheetNames.join(", ")}`);
+
+ const ranges = sheetNames.map((name) => `'${name}'!A:ZZ`);
+ const batchResult = await sheetsApiFetch(
+ `${SHEETS_API_BASE}/${spreadsheetId}/values:batchGet?${ranges.map((r) => `ranges=${encodeURIComponent(r)}`).join("&")}`,
+ token
+ );
+
+ const allLogs = [];
+ for (const valueRange of batchResult.valueRanges) {
+ const rows = valueRange.values;
+ if (!rows || rows.length < 2) continue;
+
+ const headers = rows[0];
+ for (let i = 1; i < rows.length; i++) {
+ const row = rows[i];
+ const obj = {};
+ for (let j = 0; j < headers.length; j++) {
+ const value = fromCellValue(j < row.length ? row[j] : undefined);
+ if (value !== undefined) {
+ _.set(obj, headers[j], value);
+ }
+ }
+ allLogs.push(obj);
+ }
+ }
+
+ log(`Imported ${allLogs.length} total log entries`);
+
+ if (allLogs.length === 0) {
+ throw new Error("No log data found in the spreadsheet");
+ }
+
+ allLogs.sort((a, b) => {
+ const tsA = a.timestamp || "";
+ const tsB = b.timestamp || "";
+ return tsA < tsB ? -1 : tsA > tsB ? 1 : 0;
+ });
+
+ return allLogs;
+}
From 9e758671a5c1c0d85a1616b8b8c584facef40d25 Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Wed, 11 Feb 2026 15:27:26 -0800
Subject: [PATCH 03/10] fix: google sheet column names in reversed order
---
src/GoogleSheets.js | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/GoogleSheets.js b/src/GoogleSheets.js
index 111275f..115a7f2 100644
--- a/src/GoogleSheets.js
+++ b/src/GoogleSheets.js
@@ -106,6 +106,8 @@ export const useSheetsLogin = (onSuccess, onError) => {
// --- Flatten / Unflatten ---
+const reversePath = (path) => path.split(".").reverse().join(".");
+
function flattenObject(obj, prefix = "") {
const result = {};
for (const key of Object.keys(obj)) {
@@ -216,12 +218,13 @@ export async function exportToGoogleSheet(index, token) {
const valueRanges = [];
for (const sheetName of sheetNames) {
const logs = grouped[sheetName];
- const headers = collectAllKeys(logs);
- const rows = [headers];
+ const originalHeaders = collectAllKeys(logs);
+ const displayHeaders = originalHeaders.map(reversePath);
+ const rows = [displayHeaders];
for (const entry of logs) {
const flat = flattenObject(entry);
- const row = headers.map((h) => toCellValue(flat[h]));
+ const row = originalHeaders.map((h) => toCellValue(flat[h]));
rows.push(row);
}
@@ -274,14 +277,15 @@ export async function importFromGoogleSheet(spreadsheetInput, token) {
const rows = valueRange.values;
if (!rows || rows.length < 2) continue;
- const headers = rows[0];
+ const displayHeaders = rows[0];
+ const originalHeaders = displayHeaders.map(reversePath);
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
const obj = {};
- for (let j = 0; j < headers.length; j++) {
+ for (let j = 0; j < originalHeaders.length; j++) {
const value = fromCellValue(j < row.length ? row[j] : undefined);
if (value !== undefined) {
- _.set(obj, headers[j], value);
+ _.set(obj, originalHeaders[j], value);
}
}
allLogs.push(obj);
From 7f55d9aaf105732e4e87704af9a549f31eb0c19c Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Wed, 11 Feb 2026 15:38:53 -0800
Subject: [PATCH 04/10] fix: save files and google sheets with vehicleid
---
src/GoogleSheets.js | 5 +++--
src/localStorage.js | 16 ++++++++++++++--
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/GoogleSheets.js b/src/GoogleSheets.js
index 115a7f2..1fafafa 100644
--- a/src/GoogleSheets.js
+++ b/src/GoogleSheets.js
@@ -1,6 +1,6 @@
// src/GoogleSheets.js
import { useGoogleLogin } from "@react-oauth/google";
-import { getUploadedData } from "./localStorage";
+import { getUploadedData, getVehicleIdFromLogs } from "./localStorage";
import { log } from "./Utils";
import _ from "lodash";
@@ -203,11 +203,12 @@ export async function exportToGoogleSheet(index, token) {
const sheets = sheetNames.map((name) => ({ properties: { title: name } }));
+ const vehicleId = getVehicleIdFromLogs(data.rawLogs);
const date = new Date().toISOString().split("T")[0];
const spreadsheet = await sheetsApiFetch(SHEETS_API_BASE, token, {
method: "POST",
body: JSON.stringify({
- properties: { title: `Fleet Debugger Export - ${date}` },
+ properties: { title: `Fleet Debugger - ${vehicleId} - ${date}` },
sheets,
}),
});
diff --git a/src/localStorage.js b/src/localStorage.js
index 2dae3c0..50be26a 100644
--- a/src/localStorage.js
+++ b/src/localStorage.js
@@ -89,6 +89,18 @@ export async function uploadCloudLogs(logs, index) {
}
}
+function getVehicleIdFromLogs(rawLogs) {
+ for (const entry of rawLogs) {
+ const jsonPayload = entry.jsonpayload || {};
+ const response = jsonPayload.response || {};
+ const vehicleId = response.vehicleid || response.deliveryvehicleid;
+ if (vehicleId) return vehicleId;
+ }
+ return "unknown";
+}
+
+export { getVehicleIdFromLogs };
+
export async function saveDatasetAsJson(index) {
try {
log(`Attempting to save dataset ${index} as JSON`);
@@ -106,9 +118,9 @@ export async function saveDatasetAsJson(index) {
const link = document.createElement("a");
link.href = url;
- // Set the filename based on the dataset number and current date
+ const vehicleId = getVehicleIdFromLogs(data.rawLogs);
const date = new Date().toISOString().split("T")[0];
- link.download = `dataset_${index + 1}_${date}.json`;
+ link.download = `Fleet Debugger - ${vehicleId} - ${date}.json`;
document.body.appendChild(link);
link.click();
From 160a7de6ea61cbac6497a14ae9348a547d2fcb00 Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Fri, 13 Feb 2026 17:24:41 -0800
Subject: [PATCH 05/10] fix: Add Google Sign In library early to support Google
Sheets
---
public/index.html | 1 +
src/GoogleSheets.js | 59 ++++++++++++++++++++++++++++-----------------
2 files changed, 38 insertions(+), 22 deletions(-)
diff --git a/public/index.html b/public/index.html
index f1eed25..44e81d6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -24,6 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
+
Fleet Debugger
diff --git a/src/GoogleSheets.js b/src/GoogleSheets.js
index 1fafafa..64556fc 100644
--- a/src/GoogleSheets.js
+++ b/src/GoogleSheets.js
@@ -60,28 +60,43 @@ export function requestSheetsToken() {
return;
}
- if (!window.google?.accounts?.oauth2) {
- reject(new Error("Google Identity Services not loaded. Please refresh and try again."));
- return;
- }
-
- const tokenClient = window.google.accounts.oauth2.initTokenClient({
- client_id: CLIENT_ID,
- scope: SCOPES,
- callback: (tokenResponse) => {
- if (tokenResponse.error) {
- reject(new Error(`Auth error: ${tokenResponse.error}`));
- return;
- }
- storeSheetsToken(tokenResponse.access_token, tokenResponse.expires_in);
- resolve(tokenResponse.access_token);
- },
- error_callback: (error) => {
- reject(new Error(`Auth error: ${error.message || "Unknown"}`));
- },
- });
-
- tokenClient.requestAccessToken();
+ const checkGis = () => {
+ if (window.google?.accounts?.oauth2) {
+ const tokenClient = window.google.accounts.oauth2.initTokenClient({
+ client_id: CLIENT_ID,
+ scope: SCOPES,
+ callback: (tokenResponse) => {
+ if (tokenResponse.error) {
+ reject(new Error(`Auth error: ${tokenResponse.error}`));
+ return;
+ }
+ storeSheetsToken(tokenResponse.access_token, tokenResponse.expires_in);
+ resolve(tokenResponse.access_token);
+ },
+ error_callback: (error) => {
+ reject(new Error(`Auth error: ${error.message || "Unknown"}`));
+ },
+ });
+
+ tokenClient.requestAccessToken();
+ return true;
+ }
+ return false;
+ };
+
+ if (checkGis()) return;
+
+ // Wait up to 5 seconds for GIS to load
+ let attempts = 0;
+ const interval = setInterval(() => {
+ attempts++;
+ if (checkGis()) {
+ clearInterval(interval);
+ } else if (attempts > 50) {
+ clearInterval(interval);
+ reject(new Error("Google Identity Services not loaded. Please refresh and try again."));
+ }
+ }, 100);
});
}
From a8297de50ac4589f3ecbd259711210aa8d4068f5 Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Fri, 13 Feb 2026 17:41:06 -0800
Subject: [PATCH 06/10] fix: add Google Client ID to constants.js
---
src/DatasetLoading.js | 4 ++--
src/GoogleSheets.js | 3 ++-
src/constants.js | 1 +
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/DatasetLoading.js b/src/DatasetLoading.js
index 1e96eb4..58cd0fb 100644
--- a/src/DatasetLoading.js
+++ b/src/DatasetLoading.js
@@ -6,7 +6,7 @@ import { log } from "./Utils";
import { toast } from "react-toastify";
import { isTokenValid, fetchLogsWithToken, useCloudLoggingLogin, buildQueryFilter } from "./CloudLogging";
import { useSheetsLogin, isSheetsTokenValid, getSheetsToken, importFromGoogleSheet } from "./GoogleSheets";
-import { HAS_EXTRA_DATA_SOURCE } from "./constants";
+import { HAS_EXTRA_DATA_SOURCE, GOOGLE_CLIENT_ID } from "./constants";
const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
const getStoredValue = (key, defaultValue = "") => localStorage.getItem(`datasetLoading_${key}`) || defaultValue;
@@ -298,7 +298,7 @@ export default function DatasetLoading(props) {
{isExtra ? (
ExtraFormComponent
) : (
-
+
)}
diff --git a/src/GoogleSheets.js b/src/GoogleSheets.js
index 64556fc..35ba34f 100644
--- a/src/GoogleSheets.js
+++ b/src/GoogleSheets.js
@@ -2,10 +2,11 @@
import { useGoogleLogin } from "@react-oauth/google";
import { getUploadedData, getVehicleIdFromLogs } from "./localStorage";
import { log } from "./Utils";
+import { GOOGLE_CLIENT_ID } from "./constants";
import _ from "lodash";
const SHEETS_API_BASE = "https://sheets.googleapis.com/v4/spreadsheets";
-const CLIENT_ID = "829183678942-eq2c9cd7pjdm39l2um5thgbrvgva07e7.apps.googleusercontent.com";
+const CLIENT_ID = GOOGLE_CLIENT_ID;
const SCOPES = "https://www.googleapis.com/auth/spreadsheets";
const TOKEN_EXPIRY_BUFFER = 5 * 60 * 1000;
diff --git a/src/constants.js b/src/constants.js
index e8fe127..08d81ad 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -4,3 +4,4 @@ import ExtraDataSource from "./ExtraDataSource";
export const DEFAULT_API_KEY = "";
export const DEFAULT_MAP_ID = "e6ead35a6ace8599";
export const HAS_EXTRA_DATA_SOURCE = ExtraDataSource.isAvailable();
+export const GOOGLE_CLIENT_ID = "829183678942-eq2c9cd7pjdm39l2um5thgbrvgva07e7.apps.googleusercontent.com";
From d22f25589a3b119d2007decf4891f1b090214f7b Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Fri, 13 Feb 2026 18:21:41 -0800
Subject: [PATCH 07/10] build: allow manual demo builds on forks
---
.github/workflows/build-demos.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build-demos.yml b/.github/workflows/build-demos.yml
index 7aec843..1af08ac 100644
--- a/.github/workflows/build-demos.yml
+++ b/.github/workflows/build-demos.yml
@@ -16,7 +16,7 @@ permissions:
jobs:
build-and-deploy:
- if: github.repository == 'googlemaps/fleet-debugger'
+ if: github.repository == 'googlemaps/fleet-debugger' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
From a9db4275d871286b965cb112a2986136660f1477 Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Fri, 13 Feb 2026 19:30:38 -0800
Subject: [PATCH 08/10] fix: clean up css
---
src/DatasetLoading.js | 16 ++++++----------
src/global.css | 30 +++++++++++++++++++++++-------
2 files changed, 29 insertions(+), 17 deletions(-)
diff --git a/src/DatasetLoading.js b/src/DatasetLoading.js
index 58cd0fb..7f8e3aa 100644
--- a/src/DatasetLoading.js
+++ b/src/DatasetLoading.js
@@ -212,18 +212,14 @@ const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
)}
-
@@ -245,7 +241,7 @@ const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
type="button"
onClick={handleSheetLoadClick}
disabled={sheetLoading}
- className="primary-button"
+ className="fetch-logs-button"
style={{ marginTop: "8px" }}
>
{sheetLoading ? "Loading..." : isSheetsTokenValid() ? "Load Sheet" : "Sign in and Load Sheet"}
diff --git a/src/global.css b/src/global.css
index c0b636e..fe6e34d 100644
--- a/src/global.css
+++ b/src/global.css
@@ -169,43 +169,59 @@
.cloud-logging-buttons {
display: flex;
- gap: 10px;
+ gap: 8px;
margin-bottom: 15px;
align-items: stretch;
}
-.primary-button {
+.fetch-logs-button {
background-color: #4285f4;
color: white;
- padding: 10px 15px;
+ padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
- font-weight: bold;
+ font-family: Roboto, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 1.2;
flex: 3;
display: flex;
align-items: center;
justify-content: center;
+ text-align: center;
}
-.primary-button:disabled {
+.fetch-logs-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
-.secondary-button {
+.sideload-logs-button {
background-color: #34A853;
color: white;
- padding: 10px 15px;
+ padding: 12px 6px;
border: none;
border-radius: 4px;
cursor: pointer;
+ font-family: Roboto, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 1.2;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
+ text-align: center;
}
+.google-sheet-form {
+ margin-top: 10px;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background-color: #fafafa;
+}
.cloud-logging-fetch-button {
background-color: #4285f4;
From f5cb76a1a5e6cfc453e4c8280f03c9a405c5fcca Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:23:50 -0800
Subject: [PATCH 09/10] fix: clean up export names
---
src/App.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/App.js b/src/App.js
index e76629e..f742c3a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -704,10 +704,10 @@ class App extends React.Component {
{isMenuOpen && (
- Export
+ Export File
- Google Sheet
+ Export GSheet
Prune
From 4d93312ce848e4296b73cec82cfb62024df7a1f9 Mon Sep 17 00:00:00 2001
From: regeter <2320305+regeter@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:33:37 -0800
Subject: [PATCH 10/10] feat: filename and google sheet name should contain the
date of the logs
---
src/GoogleSheets.js | 4 ++--
src/localStorage.js | 19 ++++++++++++++++---
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/src/GoogleSheets.js b/src/GoogleSheets.js
index 35ba34f..33bcd53 100644
--- a/src/GoogleSheets.js
+++ b/src/GoogleSheets.js
@@ -1,6 +1,6 @@
// src/GoogleSheets.js
import { useGoogleLogin } from "@react-oauth/google";
-import { getUploadedData, getVehicleIdFromLogs } from "./localStorage";
+import { getUploadedData, getVehicleIdFromLogs, getFirstLogDate } from "./localStorage";
import { log } from "./Utils";
import { GOOGLE_CLIENT_ID } from "./constants";
import _ from "lodash";
@@ -220,7 +220,7 @@ export async function exportToGoogleSheet(index, token) {
const sheets = sheetNames.map((name) => ({ properties: { title: name } }));
const vehicleId = getVehicleIdFromLogs(data.rawLogs);
- const date = new Date().toISOString().split("T")[0];
+ const date = getFirstLogDate(data.rawLogs);
const spreadsheet = await sheetsApiFetch(SHEETS_API_BASE, token, {
method: "POST",
body: JSON.stringify({
diff --git a/src/localStorage.js b/src/localStorage.js
index 50be26a..bff3676 100644
--- a/src/localStorage.js
+++ b/src/localStorage.js
@@ -99,7 +99,15 @@ function getVehicleIdFromLogs(rawLogs) {
return "unknown";
}
-export { getVehicleIdFromLogs };
+function getFirstLogDate(rawLogs) {
+ const oldestTimestamp = getOldestTimestamp(rawLogs);
+ if (oldestTimestamp === Infinity) {
+ return new Date().toISOString().split("T")[0];
+ }
+ return new Date(oldestTimestamp).toISOString().split("T")[0];
+}
+
+export { getVehicleIdFromLogs, getFirstLogDate };
export async function saveDatasetAsJson(index) {
try {
@@ -119,7 +127,7 @@ export async function saveDatasetAsJson(index) {
link.href = url;
const vehicleId = getVehicleIdFromLogs(data.rawLogs);
- const date = new Date().toISOString().split("T")[0];
+ const date = getFirstLogDate(data.rawLogs);
link.download = `Fleet Debugger - ${vehicleId} - ${date}.json`;
document.body.appendChild(link);
@@ -241,7 +249,7 @@ function isRestrictedLog(row) {
return row.jsonPayload?.["@type"]?.includes("Restricted") || false;
}
-function calculateRetentionDate(logsArray) {
+function getOldestTimestamp(logsArray) {
let oldestTimestamp = Infinity;
logsArray.forEach((row) => {
const ts = new Date(
@@ -251,6 +259,11 @@ function calculateRetentionDate(logsArray) {
oldestTimestamp = ts;
}
});
+ return oldestTimestamp;
+}
+
+function calculateRetentionDate(logsArray) {
+ const oldestTimestamp = getOldestTimestamp(logsArray);
let retentionDateIdentifier = null;
if (oldestTimestamp !== Infinity) {