From d56232684f633b83ca0a0a6bd8f2d9ffbbf40065 Mon Sep 17 00:00:00 2001 From: ascibisz Date: Wed, 14 Jan 2026 09:18:28 -0800 Subject: [PATCH 1/3] send JSON object in body instead of uploading edited recipe --- src/App.tsx | 50 +++++++++---------------------------------- src/constants/aws.ts | 12 +++++++---- src/utils/firebase.ts | 6 ------ 3 files changed, 18 insertions(+), 50 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c330ada3..37a21efa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,8 @@ import { useState } from "react"; -import { v4 as uuidv4 } from "uuid"; import { Layout, Typography } from "antd"; -import { getJobStatus, addRecipe } from "./utils/firebase"; +import { getJobStatus } from "./utils/firebase"; import { getFirebaseRecipe, jsonToString } from "./utils/recipeLoader"; import { getSubmitPackingUrl, JOB_STATUS } from "./constants/aws"; -import { FIRESTORE_FIELDS } from "./constants/firebase"; import { useJobId, useJobLogs, @@ -48,53 +46,25 @@ function App() { return !(jsonToString(originalRecipe) == recipeString); }; - const recipeToFirebase = ( - recipe: string, - path: string, - id: string - ): object => { - const recipeJson = JSON.parse(recipe); - if (recipeJson.bounding_box) { - const flattened_array = Object.assign({}, recipeJson.bounding_box); - recipeJson.bounding_box = flattened_array; - } - recipeJson[FIRESTORE_FIELDS.RECIPE_PATH] = path; - recipeJson[FIRESTORE_FIELDS.NAME] = id; - recipeJson[FIRESTORE_FIELDS.TIMESTAMP] = Date.now(); - return recipeJson; - }; - const submitRecipe = async ( recipeId: string, configId: string, recipeString: string ) => { - let firebaseRecipe = "firebase:recipes/" + recipeId; - const firebaseConfig = configId - ? "firebase:configs/" + configId - : undefined; const recipeChanged: boolean = await recipeHasChanged( recipeId, recipeString ); - if (recipeChanged) { - const recipeId = uuidv4(); - firebaseRecipe = "firebase:recipes_edited/" + recipeId; - const recipeJson = recipeToFirebase( - recipeString, - firebaseRecipe, - recipeId - ); - try { - await addRecipe(recipeId, recipeJson); - } catch (e) { - setJobStatus(JOB_STATUS.FAILED); - setJobLogs(String(e)); - return; - } - } + const firebaseRecipe = recipeChanged + ? undefined + : "firebase:recipes/" + recipeId; + const firebaseConfig = configId + ? "firebase:configs/" + configId + : undefined; + const url = getSubmitPackingUrl(firebaseRecipe, firebaseConfig); - const request: RequestInfo = new Request(url, { method: "POST" }); + const requestBody = recipeChanged ? recipeString : undefined; + const request: RequestInfo = new Request(url, { method: "POST", body: requestBody }); start = Date.now(); const response = await fetch(request); setJobStatus(JOB_STATUS.SUBMITTED); diff --git a/src/constants/aws.ts b/src/constants/aws.ts index 383f18bf..dfb00c9c 100644 --- a/src/constants/aws.ts +++ b/src/constants/aws.ts @@ -5,12 +5,16 @@ const SUBMIT_PACKING_ECS = "https://bda21vau5c.execute-api.us-west-2.amazonaws.c const S3_BASE_URL = "https://s3.us-west-2.amazonaws.com"; export const getSubmitPackingUrl = ( - recipe: string, + recipe?: string, config?: string, ) => { - let url = `${SUBMIT_PACKING_ECS}?recipe=${recipe}`; - if (config) { - url += `&config=${config}`; + let url = SUBMIT_PACKING_ECS; + if (recipe && config) { + url += `?recipe=${recipe}&config=${config}`; + } else if (recipe) { + url += `?recipe=${recipe}`; + } else if (config) { + url += `?config=${config}`; } return url; }; diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 14474031..d6c630cd 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -8,7 +8,6 @@ import { documentId, QuerySnapshot, DocumentData, - setDoc, doc, Timestamp, deleteDoc, @@ -228,10 +227,6 @@ const getDocsByIds = async (coll: string, ids: string[]) => { return docs; }; -const addRecipe = async (id: string, data: object) => { - await setDoc(doc(db, FIRESTORE_COLLECTIONS.EDITED_RECIPES, id), data); -}; - const docCleanup = async () => { const now = Date.now(); const collectionsToClean = [ @@ -275,7 +270,6 @@ export { queryDocumentById, getDocsByIds, getJobStatus, - addRecipe, docCleanup, getRecipeManifestFromFirebase, getRecipeDataFromFirebase, From 37f3e550dca5de6bd55acb23281f46be23b9cf3e Mon Sep 17 00:00:00 2001 From: Alli <111383930+ascibisz@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:52:36 -0800 Subject: [PATCH 2/3] Update Job Status Retention Policy (#179) * set retention for job status to 30 days, update job status timestamp on read, and misc bug fixes * code cleanup --- src/App.tsx | 15 ++++++++++++--- src/constants/firebase.ts | 2 +- src/utils/firebase.ts | 9 +++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 37a21efa..b3d8cf77 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Layout, Typography } from "antd"; -import { getJobStatus } from "./utils/firebase"; +import { getJobStatus, updateJobStatusTimestamp } from "./utils/firebase"; import { getFirebaseRecipe, jsonToString } from "./utils/recipeLoader"; import { getSubmitPackingUrl, JOB_STATUS } from "./constants/aws"; import { @@ -68,14 +68,15 @@ function App() { start = Date.now(); const response = await fetch(request); setJobStatus(JOB_STATUS.SUBMITTED); - const data = await response.json(); if (response.ok) { + const data = await response.json(); setJobId(data.jobId); setJobStatus(JOB_STATUS.STARTING); return data.jobId; } else { + const errorText = await response.text(); setJobStatus(JOB_STATUS.FAILED); - setJobLogs(JSON.stringify(data)); + setJobLogs(errorText); } }; @@ -92,6 +93,9 @@ function App() { const checkStatus = async (jobIdFromSubmit: string) => { const id = jobIdFromSubmit || jobId; let localJobStatus = await getJobStatus(id); + if (localJobStatus) { + setJobStatus(localJobStatus.status); + } while ( localJobStatus?.status !== JOB_STATUS.DONE && localJobStatus?.status !== JOB_STATUS.FAILED @@ -106,6 +110,11 @@ function App() { setJobStatus(newJobStatus.status); } } + + // Update the job status timestamp after reading the final status to + // ensure we have the most recent timestamp for retention policy + await updateJobStatusTimestamp(id); + const range = (Date.now() - start) / 1000; if (localJobStatus.status == JOB_STATUS.DONE) { setPackingResults({ diff --git a/src/constants/firebase.ts b/src/constants/firebase.ts index 7f5bff4f..a06200b8 100644 --- a/src/constants/firebase.ts +++ b/src/constants/firebase.ts @@ -44,7 +44,7 @@ export const FIRESTORE_FIELDS = { export const RETENTION_POLICY = { RETENTION_PERIODS: { RECIPES_EDITED: 24 * 60 * 60 * 1000, // 24 hours - JOB_STATUS: 24 * 60 * 60 * 1000, // 24 hours + JOB_STATUS: 30 * 24 * 60 * 60 * 1000, // 30 days }, TIMESTAMP_FIELD: "timestamp", diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index d6c630cd..caf24d96 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -11,6 +11,7 @@ import { doc, Timestamp, deleteDoc, + updateDoc } from "firebase/firestore"; import { sortBy } from "lodash-es"; import { @@ -125,6 +126,13 @@ const getJobStatus = async ( return docs[0] || undefined; }; +const updateJobStatusTimestamp = async (jobId: string) => { + const data = { + timestamp: Timestamp.now(), + }; + await updateDoc(doc(db, FIRESTORE_COLLECTIONS.JOB_STATUS, jobId), data); +}; + const getOutputsDirectory = async (jobId: string) => { const querySnapshot = await queryDocumentById( FIRESTORE_COLLECTIONS.JOB_STATUS, @@ -274,4 +282,5 @@ export { getRecipeManifestFromFirebase, getRecipeDataFromFirebase, getOutputsDirectory, + updateJobStatusTimestamp, }; From 00f3d240520d884a98968d898dacdc45d6f8711f Mon Sep 17 00:00:00 2001 From: Alli <111383930+ascibisz@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:00:52 -0800 Subject: [PATCH 3/3] Fix/recipe json cleanup (#181) * remove irrelevant gradient data from final recipe string * round radius to 2 decimal points * update test file to remove unnecessary gradients * copilot suggestions --- src/App.tsx | 4 +-- src/components/InputSwitch/index.tsx | 2 +- src/state/store.ts | 4 +-- src/test/recipeLoader.test.ts | 6 ++-- src/test/test-files/ER_peroxisome.json | 48 -------------------------- src/utils/recipeLoader.ts | 31 +++++++++++++++-- 6 files changed, 36 insertions(+), 59 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b3d8cf77..8d68e80f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Layout, Typography } from "antd"; import { getJobStatus, updateJobStatusTimestamp } from "./utils/firebase"; -import { getFirebaseRecipe, jsonToString } from "./utils/recipeLoader"; +import { getFirebaseRecipe, recipeToString } from "./utils/recipeLoader"; import { getSubmitPackingUrl, JOB_STATUS } from "./constants/aws"; import { useJobId, @@ -43,7 +43,7 @@ function App() { recipeString: string ): Promise => { const originalRecipe = await getFirebaseRecipe(recipeId); - return !(jsonToString(originalRecipe) == recipeString); + return !(recipeToString(originalRecipe) == recipeString); }; const submitRecipe = async ( diff --git a/src/components/InputSwitch/index.tsx b/src/components/InputSwitch/index.tsx index 952574bb..bc2e1332 100644 --- a/src/components/InputSwitch/index.tsx +++ b/src/components/InputSwitch/index.tsx @@ -49,7 +49,7 @@ const InputSwitch = (props: InputSwitchProps): JSX.Element => { } if (typeof value == "number") { value = value * conversion; - value = Number(value.toFixed(4)); + value = Number(value.toFixed(2)); } return value; }, [getCurrentValue, id, min, conversion, dataType]); diff --git a/src/state/store.ts b/src/state/store.ts index 48382aed..252dba26 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; import { isEmpty, isEqual, get as lodashGet } from "lodash-es"; import { PackingResult, RecipeData, RecipeManifest } from "../types"; -import { jsonToString } from "../utils/recipeLoader"; +import { recipeToString } from "../utils/recipeLoader"; import { getRecipeDataFromFirebase, getRecipeManifestFromFirebase, @@ -236,7 +236,7 @@ export const useRecipeStore = create()( edits ); if (!recipeObject) return; - const recipeString = jsonToString(recipeObject); + const recipeString = recipeToString(recipeObject); set({ isPacking: true }); try { await callback(s.selectedRecipeId, configId, recipeString); diff --git a/src/test/recipeLoader.test.ts b/src/test/recipeLoader.test.ts index e1e01c07..d8d95404 100644 --- a/src/test/recipeLoader.test.ts +++ b/src/test/recipeLoader.test.ts @@ -1,6 +1,6 @@ import { expect, test } from 'vitest'; import fs from 'fs'; -import { isFirebaseRef, getFirebaseRecipe, jsonToString } from '../utils/recipeLoader'; +import { isFirebaseRef, getFirebaseRecipe, recipeToString } from '../utils/recipeLoader'; test('isFirebaseRef detects Firebase references correctly', () => { expect(isFirebaseRef('firebase:recipes/some_id')).toBe(true); @@ -14,7 +14,7 @@ test('isFirebaseRef detects Firebase references correctly', () => { test('getFirebaseRecipe works as expected for ER_peroxisome_v_struct_gradient_370574', async () => { const recipeId = 'ER_peroxisome_v_struct_gradient_370574'; const recipeJson = await getFirebaseRecipe(recipeId); - const recipeString = jsonToString(recipeJson); + const recipeString = recipeToString(recipeJson); expect(recipeString).toBeDefined(); expect(typeof recipeString).toBe('string'); @@ -30,7 +30,7 @@ test('getFirebaseRecipe works as expected for ER_peroxisome_v_struct_gradient_37 test('getFirebaseRecipe works as expected for one_sphere', async () => { const recipeId = 'one_sphere_v_1.0.0'; const recipeJson = await getFirebaseRecipe(recipeId); - const recipeString = jsonToString(recipeJson); + const recipeString = recipeToString(recipeJson); expect(recipeString).toBeDefined(); expect(typeof recipeString).toBe('string'); diff --git a/src/test/test-files/ER_peroxisome.json b/src/test/test-files/ER_peroxisome.json index 96dad222..b41cb835 100644 --- a/src/test/test-files/ER_peroxisome.json +++ b/src/test/test-files/ER_peroxisome.json @@ -132,54 +132,6 @@ } }, "gradients": { - "apical_gradient": { - "description": "gradient based on distance from a plane", - "invert": null, - "mode": "vector", - "mode_settings": { - "center": [0, 0, 106.875], - "direction": [0, 0, 1] - }, - "name": "apical_gradient", - "pick_mode": "linear", - "reversed": false, - "weight_mode": "exponential", - "weight_mode_settings": { - "decay_length": 0.1 - } - }, - "membrane_gradient": { - "description": "gradient based on distance from the surface of the membrane mesh", - "invert": null, - "mode": "surface", - "mode_settings": { - "object": "membrane", - "scale_to_next_surface": false - }, - "name": "membrane_gradient", - "pick_mode": "linear", - "reversed": false, - "weight_mode": "exponential", - "weight_mode_settings": { - "decay_length": 0.01 - } - }, - "nucleus_gradient": { - "description": "gradient based on distance from the surface of the nucleus mesh", - "invert": null, - "mode": "surface", - "mode_settings": { - "object": "nucleus", - "scale_to_next_surface": false - }, - "name": "nucleus_gradient", - "pick_mode": "linear", - "reversed": false, - "weight_mode": "exponential", - "weight_mode_settings": { - "decay_length": 0.1 - } - }, "struct_gradient": { "name": "struct_gradient", "invert": null, diff --git a/src/utils/recipeLoader.ts b/src/utils/recipeLoader.ts index c133f037..a2eea95c 100644 --- a/src/utils/recipeLoader.ts +++ b/src/utils/recipeLoader.ts @@ -328,9 +328,34 @@ const getFirebaseRecipe = async (name: string): Promise => { return unpackedRecipe; } -const jsonToString = (json: ViewableRecipe): string => { - return JSON.stringify(json, null, 2); +const recipeToString = (rec: ViewableRecipe): string => { + // Deep copy recipe to avoid mutating original object + const recipe: ViewableRecipe = structuredClone(rec); + + // Collect a list of gradients that are referenced by objects the recipe + const referencedGradients: Set = new Set(); + if (recipe.objects) { + for (const obj of Object.values(recipe.objects)) { + if (obj.packing_mode === "gradient" && obj.gradient) { + referencedGradients.add(obj.gradient); + } else if (obj.packing_mode === "random" && obj.gradient) { + // If packing mode is random, gradient field is irrelevant + // and should be cleared + delete obj.gradient; + } + } + } + + // If the recipe has gradients that aren't referenced in any objects, delete them + if (recipe.gradients) { + for (const gradientName of Object.keys(recipe.gradients)) { + if (!referencedGradients.has(gradientName)) { + delete recipe.gradients[gradientName]; + } + } + } + return JSON.stringify(recipe, null, 2); } -export { getFirebaseRecipe, isFirebaseRef, jsonToString }; \ No newline at end of file +export { getFirebaseRecipe, isFirebaseRef, recipeToString }; \ No newline at end of file