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) {