diff --git a/src/App.js b/src/App.js index dac0888..0605288 100644 --- a/src/App.js +++ b/src/App.js @@ -24,6 +24,7 @@ import { log } from "./Utils"; import { toast, ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import { ALL_TOGGLES, getVisibleToggles } from "./MapToggles"; +import { HAS_EXTRA_DATA_SOURCE } from "./constants"; const MARKER_COLORS = [ "#EA4335", // Red @@ -414,6 +415,22 @@ class App extends React.Component { const data = await getUploadedData(index); log(`Dataset ${index}:`, data); if (data && data.rawLogs && Array.isArray(data.rawLogs) && data.rawLogs.length > 0) { + if (HAS_EXTRA_DATA_SOURCE && data.retentionDate) { + const retentionDate = new Date(data.retentionDate); + if (retentionDate <= new Date()) { + log(`Startup: Dataset ${index} expired. Deleting...`); + await deleteUploadedData(index); + log(`Dataset ${index + 1} has expired and was deleted.`, "error"); + return { status: null, index }; + } else { + const now = new Date(); + const timeLeftMs = retentionDate - now; + const daysLeft = timeLeftMs / (1000 * 60 * 60 * 24); + if (daysLeft <= 10) { + log(`Dataset ${index + 1} will expire in ${Math.ceil(daysLeft)} days.`, "warn"); + } + } + } return { status: "Uploaded", index }; } return { status: null, index }; @@ -786,8 +803,48 @@ class App extends React.Component { } }; + checkAndEnforceTTL = async (index) => { + if (!HAS_EXTRA_DATA_SOURCE) return "Valid"; + + try { + const data = await getUploadedData(index); + if (!data || !data.retentionDate) return "Valid"; + + const retentionDate = new Date(data.retentionDate); + const now = new Date(); + const timeLeftMs = retentionDate - now; + const daysLeft = timeLeftMs / (1000 * 60 * 60 * 24); + + if (timeLeftMs <= 0) { + log(`Dataset ${index} expired. Deleting...`); + await deleteUploadedData(index); + this.setState((prevState) => { + const newUploadedDatasets = [...prevState.uploadedDatasets]; + newUploadedDatasets[index] = null; + return { + uploadedDatasets: newUploadedDatasets, + activeDatasetIndex: prevState.activeDatasetIndex === index ? null : prevState.activeDatasetIndex, + }; + }); + log(`Dataset ${index + 1} has expired and was deleted (Retention limit reached).`, "error"); + return "Expired"; + } else if (daysLeft <= 10) { + log(`Dataset ${index + 1} will expire in ${Math.ceil(daysLeft)} days.`, "warn"); + return "Warning"; + } + return "Valid"; + } catch (e) { + console.error("Error verifying TTL:", e); + return "Error"; + } + }; + switchDataset = async (index) => { log(`Attempting to switch to dataset ${index}`); + + const ttlStatus = await this.checkAndEnforceTTL(index); + if (ttlStatus === "Expired") return; + if (this.state.uploadedDatasets[index] !== "Uploaded") { console.error(`Attempted to switch to dataset ${index}, but it's not uploaded or is empty`); return; diff --git a/src/DatasetLoading.js b/src/DatasetLoading.js index 3714348..984f662 100644 --- a/src/DatasetLoading.js +++ b/src/DatasetLoading.js @@ -6,11 +6,7 @@ import { log } from "./Utils"; import { toast, ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import { isTokenValid, fetchLogsWithToken, useCloudLoggingLogin, buildQueryFilter } from "./CloudLogging"; - -const DATA_SOURCES = { - CLOUD_LOGGING: "cloudLogging", - EXTRA: "extra", -}; +import { HAS_EXTRA_DATA_SOURCE } from "./constants"; const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => { const getStoredValue = (key, defaultValue = "") => localStorage.getItem(`datasetLoading_${key}`) || defaultValue; @@ -181,38 +177,37 @@ const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => { export default function DatasetLoading(props) { const [activeDataSource, setActiveDataSource] = useState( - localStorage.getItem("datasetLoading_dataSource") || - (ExtraDataSource.isAvailable() ? DATA_SOURCES.EXTRA : DATA_SOURCES.CLOUD_LOGGING) + localStorage.getItem("lastUsedDataSource") || (HAS_EXTRA_DATA_SOURCE ? "extra" : "cloudLogging") ); useEffect(() => { - localStorage.setItem("datasetLoading_dataSource", activeDataSource); + localStorage.setItem("lastUsedDataSource", activeDataSource); }, [activeDataSource]); - const isExtra = activeDataSource === DATA_SOURCES.EXTRA; + const isExtra = activeDataSource === "extra"; const ExtraFormComponent = isExtra ? ExtraDataSource.getFormComponent(props) : null; - const isExtraAvailable = ExtraDataSource.isAvailable(); + + const renderSourceSelection = () => { + if (!HAS_EXTRA_DATA_SOURCE) { + return ; + } + + return ( + <> + + + + ); + }; return ( <> -
- {isExtraAvailable ? ( - <> - - - - ) : ( - - )} -
+
{renderSourceSelection()}
{isExtra ? ( ExtraFormComponent diff --git a/src/constants.js b/src/constants.js index aa3a4fb..e8fe127 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,6 @@ // constants.js +import ExtraDataSource from "./ExtraDataSource"; export const DEFAULT_API_KEY = ""; export const DEFAULT_MAP_ID = "e6ead35a6ace8599"; +export const HAS_EXTRA_DATA_SOURCE = ExtraDataSource.isAvailable(); diff --git a/src/localStorage.js b/src/localStorage.js index 28b9080..c7aa0e7 100644 --- a/src/localStorage.js +++ b/src/localStorage.js @@ -337,6 +337,27 @@ export function ensureCorrectFormat(data) { if (!hasPoints) log("Bounds Calculation Failed: Could not find vehicle location data in any row."); + // Calculate retention date + let oldestTimestamp = Infinity; + logsArray.forEach((row) => { + const ts = new Date(row.timestamp || row.insertTimestamp || row.jsonPayload?.timestamp).getTime(); + if (!isNaN(ts) && ts < oldestTimestamp) { + oldestTimestamp = ts; + } + }); + + let retentionDateIdentifier = null; + if (oldestTimestamp !== Infinity) { + const fiftyFiveDaysMs = 55 * 24 * 60 * 60 * 1000; + const oneHourMs = 60 * 60 * 1000; + const expirationBasedOnLogs = oldestTimestamp + fiftyFiveDaysMs; + const minimumRetention = Date.now() + oneHourMs; + retentionDateIdentifier = new Date(Math.max(expirationBasedOnLogs, minimumRetention)).toISOString(); + } else { + // If no valid timestamps found, default to 1 hour from now for safety + retentionDateIdentifier = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + } + return { APIKEY: DEFAULT_API_KEY, vehicle: "", @@ -345,6 +366,7 @@ export function ensureCorrectFormat(data) { solutionType: solutionType, rawLogs: fullyNormalizedLogs, bounds: hasPoints ? bounds : null, + retentionDate: retentionDateIdentifier, }; } diff --git a/src/localStorage.test.js b/src/localStorage.test.js index dc233be..fb86119 100644 --- a/src/localStorage.test.js +++ b/src/localStorage.test.js @@ -208,3 +208,53 @@ describe("ensureCorrectFormat realistic merge scenarios", () => { expect(finalPayload.request.vehicle.name).toBe("test-vehicle"); }); }); + +describe("ensureCorrectFormat TTL Logic", () => { + const ONE_DAY_MS = 24 * 60 * 60 * 1000; + const FIFTY_FIVE_DAYS_MS = 55 * ONE_DAY_MS; + const ONE_HOUR_MS = 60 * 60 * 1000; + + it("should set retentionDate to Now + 1h for very old logs (Grace Period)", () => { + const oldTimestamp = new Date(Date.now() - 100 * ONE_DAY_MS).toISOString(); + const mockLogs = [{ timestamp: oldTimestamp, jsonPayload: { test: 1 } }]; + const result = ensureCorrectFormat(mockLogs); + + const retention = new Date(result.retentionDate).getTime(); + const expectedMin = Date.now() + ONE_HOUR_MS; + + // Allow small delta for execution time + expect(retention).toBeGreaterThanOrEqual(expectedMin - 1000); + expect(retention).toBeLessThanOrEqual(expectedMin + 5000); + }); + + it("should set retentionDate to Log + 55d for recent logs", () => { + const tenDaysAgo = new Date(Date.now() - 10 * ONE_DAY_MS).getTime(); + const mockLogs = [{ timestamp: new Date(tenDaysAgo).toISOString(), jsonPayload: { test: 1 } }]; + const result = ensureCorrectFormat(mockLogs); + + const retention = new Date(result.retentionDate).getTime(); + const expected = tenDaysAgo + FIFTY_FIVE_DAYS_MS; + + expect(retention).toBe(expected); + }); + + it("should default to Now + 1h if no timestamps found", () => { + const mockLogs = [{ jsonPayload: { no_timestamp: true } }]; + const result = ensureCorrectFormat(mockLogs); + + const retention = new Date(result.retentionDate).getTime(); + const expectedMin = Date.now() + ONE_HOUR_MS; + + expect(retention).toBeGreaterThanOrEqual(expectedMin - 1000); + expect(retention).toBeLessThanOrEqual(expectedMin + 5000); + }); + + it("should store retentionDate as a valid ISO string", () => { + const mockLogs = [{ timestamp: new Date().toISOString(), jsonPayload: { test: 1 } }]; + const result = ensureCorrectFormat(mockLogs); + + expect(typeof result.retentionDate).toBe("string"); + // Simple regex to check ISO format YYYY-MM-DDTHH:mm:ss.sssZ + expect(result.retentionDate).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/); + }); +});